chore: fold skill-frontmatter into skill-size-check

skill-frontmatter was a 62-line bash script inlined in
.pre-commit-config.yaml, re-parsing SKILL.md frontmatter with grep and
awk to check for name/description/metadata.version fields.
skill-size-check.sh already parses the same frontmatter block with
PyYAML for its ADR-0020 checks, so the two checks belonged in one
script.

Adds a ~20-line required-frontmatter check (name, description,
metadata.version as three-part semver) to scripts/skill-size-check.sh.
Removes the inline skill-frontmatter hook from .pre-commit-config.yaml
and deletes tests/test-skill-frontmatter.sh (366 lines). Removes the
79-line "the other hook on that scope" discussion from
docs/spec/gates.md and its now-dangling cross-reference, replacing
both with a one-line note of the fold, and updates the pre-push hook
counts there.

Updates fixture builders in test-skill-size-check.sh,
test-adr0020-body-checks.sh, test-adr0020-targets.sh,
test-adr0020-differential.sh, and test-vale-hooks-consumer.sh to carry
valid metadata.version so the new check doesn't spuriously fail
existing fixtures that predate it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YR2CjVumUbEGWcMikcoXBD
This commit is contained in:
2026-09-12 20:00:28 +00:00
parent e647f14535
commit c8a7c9ea87
10 changed files with 62 additions and 537 deletions

View File

@@ -99,7 +99,10 @@ not installed`).
The `skill-size-check` pre-commit hook, scoped to `^plugins/[^/]+/\.apm/skills/[^/]+/SKILL\.md$`,
runs `scripts/skill-size-check.sh`. It is also shipped to external repos as
`kyberforge-skill-size-check` (see
[External consumers](#external-consumers-the-root-pre-commit-hooksyaml)).
[External consumers](#external-consumers-the-root-pre-commit-hooksyaml)). Besides the ADR-0020
gates below, it also asserts required frontmatter is present: `name`, a non-empty `description`, and
a `metadata.version` matching three-part semver (`1.0.0`) — folded in from a formerly standalone
`skill-frontmatter` hook that parsed the same fields with a shell script.
**Two things fall outside that scope, both deliberately.** The `[^/]+/SKILL\.md$` tail admits only a
`SKILL.md` sitting directly in a skill directory under `.apm/skills/`:
@@ -124,85 +127,6 @@ the second returns exactly the template. The remaining unmatched `SKILL.md` file
generated flat mirror, which is excluded by the `.apm/` segment on purpose — a mirror edit is drift,
not an authoring change.
### `skill-frontmatter`, the other hook on that scope
A second `repo: local` pre-commit hook, `skill-frontmatter`, runs on the **same** `files:` pattern at
the same stage. It is a shell loop that, **for the YAML frontmatter block only** — everything between
the opening `---` and the next `---` — asserts four things per file:
| Check | Rejects with |
|---|---|
| a `^name:` line is present | "missing required frontmatter fields (name: …)" |
| a `^description:` line is present | "missing required frontmatter fields (description: …)" |
| `metadata:` contains a `^ version:` key, anchored, scanning to the next top-level key | "missing required frontmatter fields (metadata.version)" |
| that version's value is three-part semver (`1.0.0`, quoted or not) | "has a malformed frontmatter metadata.version (…)" |
Every one of those qualifiers is load-bearing, and each replaced a defect that let the hook report
Passed having measured nothing. `tests/test-skill-frontmatter.sh` pins all of them:
- **Frontmatter-scoped, not whole-file.** The checks used to `grep` the entire file, so a `metadata:`
or `name:` block quoted in a **body code fence** satisfied them — `skill-author`'s own docs quote
exactly such a block.
- **Bounded by the next top-level key, not by `-A10`.** The version check was
`grep -A10 "^metadata:" | grep -q " version:"`, which ran ten lines past the end of the block: a
`version:` belonging to a following `source:` list entry counted (`write-docs` and `research` both
have a `source:` list immediately after `metadata:`), while a `metadata:` block with more than ten
lines before its `version:` was reported missing.
- **`^ version:` anchored.** `" version:"` was an unanchored substring, so a deeper-nested
` version:` matched too.
- **The value is asserted, not just the key.** `plugins/bin/.apm/skills/write-docs/SKILL.md` carried
`version: "1.0"` — present, correctly nested, and not a version — through an entire PR under a
presence-only check. Two-part `1.0` is a YAML float, not a version string.
- **The call shape is pinned.** `entry: bash` with `args: ['-c', <script>, …]` needs an explicit
arg0 placeholder after the script: without it `bash -c` puts pre-commit's **first** filename in
`$0`, where `for f in "$@"` never sees it. A single-file commit — the normal case — therefore ran
the loop body zero times and exited 0. The third `args` entry (`skill-frontmatter`) exists solely
to absorb `$0`; do not remove it.
- **An unreadable file is an error, not a pass.** A file with no closing `---` fails with "no closing
YAML frontmatter block" rather than falling through to a green.
**It still overlaps ADR-0020's "description present and non-empty" FAIL, and the overlap is not
clean.** The ADR (`:95-101`) requires that question be decided on the **YAML-folded value** and
nowhere else, precisely because a line regex gets it wrong in both directions. Measured on fixtures:
| Frontmatter | `skill-frontmatter` | `skill-size-check` |
|---|---|---|
| `description:` with no value, then `model: sonnet` | passes — the key is on a line | ERROR, "missing or empty" |
| `"description": …` (quoted key, valid YAML) | **fails** — `^description:` does not match | passes, description read normally |
So the grep is not a second opinion on presence. It is blind to the shape ADR-0020 was written
against, and it is the only one of the two that objects to a quoted key. Neither disagreement is
currently live in the corpus, and the honest reading is that presence is `skill-size-check`'s
question — the grep's contribution to it is noise on one shape and silence on the other.
What the hook adds that **no** ADR-0020 check reads is two keys: `name:` and `metadata.version`. A
`SKILL.md` missing either passes `skill-size-check` at exit 0. That is its unique coverage, and the
reason not to fold it into the size gate on the grounds of redundancy.
#### Why this one stays a shell parser
[`python3` and PyYAML are hard requirements](#python3-and-pyyaml-are-hard-requirements) below records
that a hand-rolled frontmatter reader on this exact `files:` scope was **deliberately deleted**,
because "a reader that mis-parses an unfamiliar scalar shape reports a clean pass on a file it never
measured." That reasoning is about `skill-size-check` and does **not** transfer here. Do not delete
this hook citing it. Three differences:
1. **It answers a strictly narrower question.** `skill-size-check` must know the *folded value* of a
`>`-block scalar to count its characters, which is where a line reader diverges from a parser —
one corpus description measured 270 characters parsed and 412 unparsed. This hook asks only
whether a key is on a line and whether one short **plain scalar** matches `N.N.N`. There is no
folding, no multi-line value, and no measurement to get subtly wrong.
2. **It is frontmatter-scoped.** The failure mode that killed the old fallback was silently reading
past or short of the block. This one extracts the block explicitly and errors out when it cannot
find a closing marker, so "could not parse" is a red, never a green.
3. **It is pinned by tests.** `tests/test-skill-frontmatter.sh` drives the hook through pre-commit's
real `bash -c <script> <arg0> <files…>` invocation and asserts each defect class above. The
deleted fallback had no such suite; that is how its disagreement with a real parser survived.
The trade it buys is that the hook stays repo-local. Moving it to a script would change the
externally exposed `.pre-commit-hooks.yaml` contract for consumers, for a check that has no need of a
YAML parser.
### Two independent gate families, neither replaced the other
**Family 1 — agentskills.io spec backstop** (unchanged, conformance not quality):
@@ -467,14 +391,9 @@ reader that mis-parses an unfamiliar scalar shape reports a clean pass on a file
which is the exact vacuous-green failure the `python3` check exists to avoid. `pip install pyyaml`
(or `python3 -m pip install PyYAML`, or the distro's `python3-yaml`) if the hook reports it missing.
**Neither requirement generalises to every hook on this scope, and one deliberate exception sits
right next to it.** [`skill-frontmatter`](#skill-frontmatter-the-other-hook-on-that-scope) runs on the
same `files:` pattern as a **shell** parser, on purpose — it asks only whether a key is on a line and
whether one short plain scalar matches `N.N.N`, with no folding to get wrong, and moving it to a
script would change the externally exposed `.pre-commit-hooks.yaml` contract for consumers. That
section carries the full argument. A reader arriving here first should not read this one as
condemning it. `check-rtk-prefix` needs `python3` but **not** PyYAML: it reads the markdown body and
never touches frontmatter, so it has no scalar to fold.
**Neither requirement generalises to every hook in this repo.** `check-rtk-prefix` needs `python3`
but **not** PyYAML: it reads the markdown body and never touches frontmatter, so it has no scalar to
fold.
## Agent files take the description gates, not the body gate