Skip to content

feat(skill): read and write SKILL.md, the Agent Skills format - #435

Merged
ion-alpha-dev merged 5 commits into
mainfrom
skill-packs
Jul 31, 2026
Merged

feat(skill): read and write SKILL.md, the Agent Skills format#435
ion-alpha-dev merged 5 commits into
mainfrom
skill-packs

Conversation

@ion-alpha-dev

Copy link
Copy Markdown
Collaborator

What

Adds skill/skillmd: a codec for the Agent Skills SKILL.md format. Parse, Format, Validate, ValidateName, and a Doc carrying every field the specification defines (name, description, license, compatibility, metadata, allowed-tools). Registers the package on the rigor gate's FuzzRequired list.

Why

SKILL.md is a governed open standard (agentskills.io), conformed to by Codex CLI, Cursor, Copilot, Gemini CLI and others. A readable and writable codec makes the external ecosystem a supported input and makes our output usable by harnesses we do not control. This lands the codec half; the fs.FS loader and directory writer follow separately.

How to verify

go test ./skill/skillmd/... covers the grammar (plain, quoted and block scalars, CRLF, BOM, bodies containing their own ---, one nesting level under metadata), every rejection path, a lossless round-trip property, a second-round-trip fixed-point property over awkward values, and a fuzz target over the frontmatter parser.

Notes for reviewers

Design decisions, with reasons so they are not relitigated:

  • Parse is tolerant, Validate is strict. Import swallows files from clients with no shared validator; export emits what a conformant reader cannot reject. Undefined keys are preserved in Doc.Unknown in original order so re-exporting a foreign pack does not rewrite it. Validate is the only place that judges them.
  • No YAML dependency. The frontmatter is five scalars and one flat string map. A general YAML engine would bring anchors, aliases, merge keys and recursive expansion to the job of reading files fetched from public registries. The hand-written grammar refuses everything outside the format by construction, and keeps the package free of a transitive dependency.
  • allowed-tools is parsed as a claim, never a grant. Honouring it from an unauthored pack would let a downloaded text file widen its own permissions. The admission decision belongs elsewhere.
  • MaxDocSize of 1 MiB is ours: the format sets no ceiling and a loader over an untrusted tree needs one. Malformed frontmatter fails loudly rather than producing an empty skill.

Adds skill/skillmd, a codec for the Agent Skills specification
(agentskills.io): the file format now conformed to by Claude Code,
Codex, Cursor, Copilot, Gemini CLI and roughly forty more clients.
This is the seam that makes the existing ecosystem an input and makes
our own skills usable by harnesses we do not control.

Parse is tolerant and Validate is strict, which is the central design
choice. Import has to swallow files written by clients with no shared
validator, against a specification that is silent on what an
unrecognised frontmatter key means; export has to emit something a
conformant reader cannot reject. So Parse keeps undefined keys in
Doc.Unknown rather than dropping or refusing them, preserving their
original order so re-exporting a foreign pack does not rewrite it,
and Validate applies the specification's constraints to documents we
are about to publish.

No YAML dependency. The frontmatter is a closed set of six keys, five
scalars and one flat string map, and a general YAML engine would bring
anchors, aliases, merge keys and implicit coercion to the job of
reading files fetched from public registries. The grammar here covers
what the format uses and refuses the rest by construction.

Extensions go where the specification sanctions them: metadata for
"properties not defined by the Agent Skills spec", and compatibility
for runtime requirements. No custom top-level keys.

allowed-tools is parsed but documented as a claim, never a grant.
It is a skill declaring which tools it would like pre-approved, it is
marked experimental, and the specification says nothing about trust,
so honouring it from an unauthored pack would let a downloaded text
file widen its own permissions.

Tested with a table over the grammar, a round-trip property, a
second-round-trip fixed-point property, and a fuzz target; skillmd is
added to the rigor gate's fuzz-required list. The property test caught
an undefined key with an empty value being silently dropped. 10.6M
fuzz executions found no further round-trip violation.

Signed-off-by: Ion Alpha <damienrch@gmail.com>
Two defects in the capture path, both of which would surface the moment
a bundled craft pack shares a scope with the learning loop.

Curate keyed the upsert on (scope, slug) and only read the previous
record to carry uses/wins forward. A distilled lesson whose title
slugified onto an authored skill's slug therefore replaced that skill's
body, check and tags with no signal that curated content had just been
lost. Capture now looks the slug up once, and refuses to store when an
existing skill in the scope lacks the learned-provenance tag, recording
the refusal in Captured.Skipped so the caller can tell "learned nothing
new" from "would have clobbered an authored skill". A learned skill
re-captures itself as before, since it carries the tag. The guard runs
before the verifier, so a capture that will be skipped costs no sandbox
time.

slugify did not bound length, so a long lesson title produced a slug
over the Agent Skills name limit and a title with no alphanumerics fell
back to the literal "skill". Neither is a conformant name, so a learned
skill was not exportable as a SKILL.md, quietly breaking the claim that
authored and learned skills are one record type. slugify now caps at
skillmd.MaxNameLen where the name is minted, and a property test asserts
every slug it produces passes skillmd.ValidateName.

Signed-off-by: Ion Alpha <damienrch@gmail.com>
Signed-off-by: Ion Alpha <contact@ionalpha.io>
@codecov

codecov Bot commented Jul 26, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

Bring skill/skillmd to full statement coverage: an edge table for the
block-scalar chomping, interior blank lines, the double-quote escapes
and the metadata-block boundaries, a rejection table for the bare and
never-closed openers, an inline metadata value, and the quote failures,
plus a direct check that Validate runs the name rule.

Rewrite indentOf as a branch-free leading-space count. The all-spaces
return was unreachable through Parse, since every all-whitespace line is
filtered as skippable before indentOf sees it; the one-liner is identical
in behaviour and carries no dead branch.

Signed-off-by: Ion Alpha <contact@ionalpha.io>
The mutation gate kept posting a red advisory check on this PR. The cause was
not the code under test: every skill/skillmd mutant was being KILLED. gremlins
gives each mutant a test timeout of (baseline suite duration x
timeout-coefficient), and the default coefficient let a single INCREMENT_DECREMENT
mutant - which flips a loop counter's ++ to --, turning the suite into an
infinite loop - stall the run for ~70s. Several such runaways ran in parallel and
starved the runner, which was reclaimed mid-run (~63% through the file). A
runner-level cancellation reports the job's check as failed regardless of exit
status, and it kills the shell before the step's `|| echo` guard can swallow it,
so the advisory signal went red for a result it never produced.

Bound it three ways: cap timeout-coefficient to 3 so a runaway dies at a small
multiple of the suite time, cap workers to 2 so timed-out mutants can't pile up
and exhaust the runner, and add MUTATE_TIMEOUT so dev/mutate wraps gremlins in
`timeout`. The wrapper ends an over-long run ourselves with a catchable non-zero
status the `|| echo` swallows, instead of leaving the runner to be reclaimed - the
only failure mode the existing guard could not catch.

Signed-off-by: Ion Alpha <contact@ionalpha.io>
@ion-alpha-dev
ion-alpha-dev merged commit 7fe4b95 into main Jul 31, 2026
26 checks passed
@ion-alpha-dev
ion-alpha-dev deleted the skill-packs branch July 31, 2026 17:49
@github-actions github-actions Bot locked and limited conversation to collaborators Jul 31, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant