fix(skill-frontmatter): check every file, scope checks to frontmatter
The hook is `entry: bash` with `args: ['-c', <script>]`. pre-commit appends filenames after the script string, so the first becomes `$0` and never enters `"$@"` — on a single-file commit, the common case, the loop body never ran and the hook reported Passed having measured nothing. ADR-0022 leans on this hook as the enforcement for a mandatory `metadata.version`, so the vacuous green was the whole gate. Implementation notes: - An arg0 placeholder absorbs `$0` so every filename lands in `"$@"`. - Checks now run against the YAML frontmatter block only, extracted with awk. The old `grep -A10 "^metadata:"` matched a `metadata:` inside a body code fence, spanned past the block into a following `source:` entry's `version:`, accepted any indentation, and missed a `version:` more than ten lines in. An unreadable frontmatter block is now an error, never a pass. - The value is asserted against three-part semver. `write-docs` carried "1.0" through the entire ADR-0022 retrofit undetected, which a presence-only check cannot catch. Impact: `tests/test-skill-frontmatter.sh` is the first test this hook has ever had. It drives the real `entry`/`args` composition read out of the config rather than a copy of the script, which is the only shape that catches the arg0 bug; against the pre-fix hook it scores 7/20. gates.md described the hook wrongly in both directions and is rewritten, with a carve-out explaining why this one stays a shell parser next to the "python3 and PyYAML are hard requirements" reasoning that argues otherwise. Refs: #127 ADR: 0022 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EeH8SCbcrCAQrtymkNuhKP
This commit is contained in:
@@ -128,12 +128,43 @@ 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 short shell loop: for each file, `grep -q "^name:"` and
|
||||
`grep -q "^description:"`, failing with "missing required frontmatter fields" if either is absent.
|
||||
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:
|
||||
|
||||
**It 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:
|
||||
| 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` |
|
||||
|---|---|---|
|
||||
@@ -145,10 +176,34 @@ against, and it is the only one of the two that objects to a quoted key. Neither
|
||||
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 grep does add is the `name:` key, which **no** ADR-0020 check reads: a `SKILL.md` with no
|
||||
`name:` passes `skill-size-check` at exit 0. That is its real and only unique coverage, and the
|
||||
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):
|
||||
@@ -413,6 +468,15 @@ 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.
|
||||
|
||||
## Agent files take the description gates, not the body gate
|
||||
|
||||
`check-apm-agents-valid` runs agent-audit's `validate.sh` over every real
|
||||
|
||||
Reference in New Issue
Block a user