Skills for AI Agents
Today I added a set of AI skills to Testo — small instructions that an agent (Claude Code, Codex, and friends) loads on demand when it spots a matching task. They live in the skills/ folder.
What's inside
Nine skills, one per scenario:
testo-write-tests— write a regular #[Test]#[Test()]Explicitly marks a method, function, or class as a test. class withAssert/Expect/ lifecycle hooks.testo-data-driven— parameterize a test: #[DataSet]#[DataSet(array $arguments, ?string $name = null)]Declares a set of arguments for a parameterized test. Can be used multiple times — each attribute creates a separate test run., #[DataProvider]#[DataProvider(callable|string $provider)]Provides data for a parameterized test from a method or callable., #[DataUnion]#[DataUnion(DataProviderAttribute ...$providers)]Merges data from multiple providers into a single sequential set., #[DataZip]#[DataZip(DataProviderAttribute ...$providers)]Pairs up providers element by element., #[DataCross]#[DataCross(DataProviderAttribute ...$providers)]Creates all possible combinations from providers (cartesian product)..testo-flaky-tests— #[Retry]#[Retry(int $maxAttempts = 3, bool $markFlaky = true)]Declares a retry policy for a test on failure. vs #[Repeat]#[Repeat(int $times = 2, int $maxFailures = 0, bool $markFlaky = true)]Runs a test a fixed number of times and decides the outcome by a failure threshold.: believe it or not, they're not the same thing.testo-inline-tests— #[TestInline]#[TestInline(array $arguments, mixed $result = null)]Declares an inline test on a method or function. right on methods insrc.testo-benchmarks— #[Bench]#[Bench(array $callables, array $arguments = [], int $warmup = 1, int $calls = 1_000, int $iterations = 10)]Declares a benchmark comparing the method's performance against alternative implementations. and how to read Mean / Median / RStDev.testo-coverage— setting upCodecovPlugin, #[Covers]#[Covers(string $classOrFunction, ?string $method = null)]Restricts which source code counts toward coverage for this test., Clover / Cobertura / PHPUnit XML reports.testo-migrate-from-phpunit— migrating tests from PHPUnit — a crowd favorite.testo-plugin-author— write your own Testo plugin.testo-configure— assemble or fix uptesto.php.
Why skills at all if there's already llms.txt?
llms.txt tells the agent what the API offers. Skills tell it when to use what and where the pitfalls are. They're short, activated by triggers (phrases from the user), and each one sends the agent off to read llms.txt for the details. That way the documentation isn't duplicated, and the skills don't go stale alongside the API.
But copying them into every project is a chore
Right now, for an agent to actually see these skills, you have to drop them into .claude/skills/ (or wherever your agent is configured to look). Which means either copy-pasting from vendor/testo/testo/skills/, setting up symlinks, or… giving up and not using them at all.
So I built a separate package — llm/skills.
llm/skills — a Composer plugin for skills
The idea is simple: a Composer package declares in its composer.json that it's a skill "donor":
{
"extra": {
"skills": {
"source": "skills"
}
}
}A consumer project installs llm/skills, and on composer install the skills from trusted packages automatically end up in .agents/skills/ (or wherever you point it).
No manual copying, no symlinks, no more "oh no, I forgot to update SKILL.md after composer update".
Come help test it
Just shipped llm/skills v1.0.0. I have no idea how much demand there'll be for it, so I deliberately didn't pile on features — just a minimal viable mechanism:
- Two commands:
composer skills:updatedoes the sync,composer skills:showis a read-only inspector that tells you what's getting synced, what's skipped, and why.updatealso has a--dry-runflag for previewing without writing. - Declaring the skills folder via
extra.skills.sourcein a dependency'scomposer.json. - Auto-discovery: skills are picked up from a
skillsfolder at the package root, even without anextra.skillsdeclaration. - Trusted-vendor whitelist:
extra.skills.trustedplus--trust=PATTERN, with wildcard support (acme/*,*) and a built-in list of already-trusted packages. - A "named it, trust it" shortcut:
composer skills:update acme/foobypasses the trust list for the duration of the command and turns on auto-discovery for that package along the way. - Transactional: if two donors declare a skill with the same name, the sync fails before touching any files. No half-applied state.
- Non-destructive merge: local edits in
target/<skill>/survive a sync — only files the donor actually ships get overwritten. You can drop alocal.mdnext to someone else's skill and it'll stay.
If you install it and run into something — file an issue on the repo. I'm especially curious to hear about scenarios I didn't think of myself: other agents, unusual layouts, security policies in larger teams.
The package is 95% vibe-coded, but that's nothing to worry about: it's all covered by tests with a high MSI.
Quick start
Install
llm/skills(and bring Testo up to date while you're at it):bashcomposer require --dev llm/skillsTweak
composer.jsonif you want a different target folder (default is.agents/skills) or want to extend the trusted-vendor list:json{ "extra": { "skills": { "target": ".claude/skills", "trusted": ["my-vendor/*"] } } }See what skills are available:
bashcomposer skills:show --discoverPull skills into the project.
Everything from trusted vendors:
bashcomposer skills:update --discoverOr specific vendors:
bashcomposer skills:update testo/*Wire up auto-update in
composer.json:json{ "scripts": { "post-install-cmd": ["@composer skills:update"], "post-update-cmd": ["@composer skills:update"] } }
That's it — the Testo skills are now sitting in .claude/skills/, and Claude Code will pick them up on its next run. Using a different agent? Just point target at whatever path it reads from.
composer skills:show previews what's about to land where, without touching the disk. Handy to run before your first update.
