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
Add the settings to
composer.json:json{ "extra": { "skills": { "target": ".agents/skills", "aliases": [".claude/skills", ".cursor/skills"], "trusted": ["my-vendor/*"], "discovery": true, "auto-sync": true } } }target— the real skills directory (default.agents/skills).aliases— extra mirror paths (Windows junctions or POSIX symlinks) pointing attarget. Handy when a project hosts several agents at once: Claude Code, Cursor & friends read the same skills through their own conventional paths, no duplicated files.trusted— trusted-vendor patterns (vendor/*orvendor/package). Testo and a few others are already in the built-in whitelist, so this is usually where you list your own additions.discovery— pick up skills from packages that don't declareextra.skillsbut ship askills/folder at the root.falseby default.auto-sync— runskills:updateautomatically aftercomposer install/update.
Install the plugin — it'll pick up the skills and lay them out right away:
bashcomposer require --dev llm/skillsComposer will ask for permission to run the plugin (
allow-plugins) — say yes.See what else is available to sync.
composer skills:showis a read-only inspector that tells you what's syncing, what's skipped, and why. Useful flags:bashcomposer skills:show # current layout composer skills:show --discovery # + packages with a skills/ folder but no extra.skills composer skills:show --trust='acme/*' # + what would unlock if you extended trustSpot a
[skip] not trustednext to something interesting? Add the vendor totrustedand the skills will land on the next sync. For a one-off sync, name the package right in the command:skills:update acme/foo.
You can install the plugin globally — then composer skills:show / skills:update work in any project, while the per-project settings (target, aliases, trusted) are still read from the local composer.json:
composer global require llm/skills