docs: amend ADR-0020 and correct the gate reference to match what ships

The ADR said a /slash target behind a route verb takes the follower test; the
implementation decides notation first and skips it. Recorded as a dated amendment
rather than a silent edit, per the ADR-0016/0017 convention.

Its Enforcement table called itself exhaustive 'because the failure this ADR is
most exposed to is a rule filed under Enforcement that no validator implements'.
Three shipped behaviours were missing, including the disable-model-invocation
carve-out that removes two rows. Also de-pins the 'zoom-out is the one carrier'
claim, which caveman falsified, and the stale dangling-target statuses.

gates.md's ERROR row made terminality a conjunct for route notation, telling an
author a form is safe that exits 1. It now documents the references/ Vale blind
spot and its two independent causes -- AGENTS.md trimmed to the operative rule
per the split gates.md itself states -- plus the undocumented skill-frontmatter
hook and the second scope exclusion. CONTEXT.md glosses 'routing target'.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EJJrm5YmacbwMdzZpXcoti
This commit is contained in:
2026-08-31 19:47:04 +00:00
parent 4011d149bc
commit 971e148e19
4 changed files with 194 additions and 23 deletions

View File

@@ -98,11 +98,57 @@ loudly (`Error: jq is required but not installed`).
## Skill and agent context gates (ADR-0020)
The `skill-size-check` pre-commit hook, scoped to `^plugins/[^/]+/\.apm/skills/[^/]+/SKILL\.md$`,
runs `scripts/skill-size-check.sh`. That scope means it never lints the
`plugins/kyberforge/docs/research/examples/` reference skills. It is also shipped to external repos
as `kyberforge-skill-size-check` (see
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)).
**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/`:
- the `plugins/kyberforge/docs/research/examples/` reference skills, which are vendored upstream
corpus and not this repo's to gate;
- `plugins/kyberforge/.apm/skills/skill-author/assets/templates/SKILL.md` — inside `.apm/skills/`,
but two directories deeper. It is the `FILL IN:` scaffold `skill-author` copies, so its
`description: >` is a comment block rather than a description and every ADR-0020 measurement over
it would be meaningless. A reader adjusting the pattern needs to know it is there.
Everything else it matches exactly, with nothing over- or under-caught. Re-derive both halves:
```
git ls-files | grep -cE '^plugins/[^/]+/\.apm/skills/[^/]+/SKILL\.md$' # the real skills
git ls-files | grep -E '^plugins/[^/]+/\.apm/skills/.*SKILL\.md$' \
| grep -vE '^plugins/[^/]+/\.apm/skills/[^/]+/SKILL\.md$' # the scaffold only
```
The first count equals the number of skill directories (`ls -d plugins/*/.apm/skills/*/ | wc -l`);
the second returns exactly the template. The remaining unmatched `SKILL.md` files in the tree are the
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 short shell loop: for each file, `grep -q "^name:"` and
`grep -q "^description:"`, failing with "missing required frontmatter fields" if either is absent.
**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:
| 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 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
reason not to fold it into the size gate on the grounds of redundancy.
### Two independent gate families, neither replaced the other
**Family 1 — agentskills.io spec backstop** (unchanged, conformance not quality):
@@ -141,7 +187,7 @@ A boundary-clause target that resolves to no skill or agent has **three** possib
| Verdict | When |
|---|---|
| **SUGGESTION** — the default | the target does not resolve and neither promotion condition below holds |
| **blocking ERROR** | the target is **terminal** (not a compound modifier) **and** either written in route notation (`/name` for any name; a bare `-> name` only when the name is hyphenated, a backticked `` -> `name` `` for any — see the gap below) **or** corroborated by another target in the same sentence that *does* resolve |
| **blocking ERROR** | the target is written in **route notation** — `/name` for any name, or any arrow form (a bare `-> name` only when the name is hyphenated, a backticked `` -> `name` `` for any — see the gap below); **or** it is a bare **terminal** name (not a compound modifier) **corroborated** by another target in the same sentence that *does* resolve |
| **INFO, "DID NOT RUN"** | no skill universe could be determined for the path at all — the targets are named and left unchecked, exit 0 |
The default is deliberately soft because a hyphenated word in a boundary clause is as likely to be a
@@ -149,6 +195,17 @@ tool, a file format or an English compound as a route: "pre-commit hooks" is pro
never reaches the check at all, being a compound modifier rather than a terminal name. The
SUGGESTION text says how to opt in — write it as `/name` or `-> name` and it gets checked properly.
**The two promotion conditions are not symmetric, and the order matters.** `_add()` decides
**notation first**: when the name is written `/name`, or reached through any arrow form, the target
is marked error-eligible there and the terminal test is never run. Terminality gates only the *bare*
path — a name in prose earns its error from corroboration, and a compound modifier can never dangle.
Reading the row as "terminal AND (notation OR corroborated)" gets the notation half backwards: it
predicts that `` … Do not use for Y — use /no-such-skill afterwards. `` is a SUGGESTION, because
`afterwards` is a follower outside `FOLLOWER_OK`. It exits 1. That was the defect — `-> name` reached
`_add()` with `strict=True` from both its call sites and `/name` did not, so the one spelling
ADR-0020 offers an author who wants a route checked unconditionally was the one spelling a stray
follower could silence.
**Known gap: a BARE arrow target must be hyphenated.** Target extraction is built on `NAME_HYPH` in
`scripts/skill-size-check.sh`, which requires at least one hyphen, and `ARROW_BOUNDARY` inherits
that. So `Not X -> gitea-prs` is extracted and checked, while `Not X -> triage` yields no target.
@@ -558,6 +615,35 @@ passing one explicit file per invocation. The two manifests scope **differently
Narrowing a `.vale.ini` glob to a `plugins/`-shaped path to "tighten" it breaks the consumer case,
and `check-vale-style-sync`'s probe set is built to catch exactly that.
### The blind spot: `references/` is unlinted, for two independent reasons
Every `references/*.md` file in the corpus is outside the prose gate. Count them with
`git ls-files | grep -cE '^plugins/[^/]+/\.apm/skills/[^/]+/references/.*\.md$'` rather than reading
a figure here; it moves with every retrofit. This is the gap that matters most, because the context
contract's own remedy for an over-long body is to move prose **into** `references/` — the gate pushes
text across its own boundary and then stops watching it.
**Closing either cause alone changes nothing.** There are two, and they are independent:
| Cause | Where | Effect on a `references/` file |
|---|---|---|
| the `Kyberforge` style is scoped `[**/SKILL.md]` | `skill-audit/assets/vale/.vale.ini` | matches no section, so Vale lints 0 files and exits 0 |
| the hook's `files:` regex is `^plugins/[^/]+/\.apm/skills/[^/]+/SKILL\.md$` | `vale-audit-prefilter-skill` in `.pre-commit-config.yaml` | the file is never handed to Vale at all |
Verified both ways. Handing skill-audit's `vale-wrap.sh` a reference file directly — bypassing
pre-commit entirely, so only the style scope is in play — prints `0 errors … in 0 files` and exits 0,
where the same wrapper on a `SKILL.md` reports `in 1 file`. And the hook's `files:` regex, applied to
`git ls-files`, selects only the skill-directory `SKILL.md` files scoped at the top of this page, so
pre-commit never hands Vale a reference file to begin with. Widening the glob to `[**/*.md]` would
still lint nothing through the hook; widening the hook's `files:` alone would hand Vale files its own
config declines to match, which is the [0-file NOT RUN](#a-0-file-vale-run-is-not-run) shape — a
green run that measured nothing. **Issue #117** records the style-scope half; the hook half has to
land in the same change or the fix is cosmetic.
The consumer manifest is a third axis and does not rescue this either: `.pre-commit-hooks.yaml`'s
`(^|/)SKILL\.md$` is layout-agnostic but still filename-shaped, so an external repo running
`kyberforge-vale-audit-skill` has the same gap.
### `vale-wrap.sh`, never bare `vale`
Both audit skills' Step 1 and both pre-commit hooks call **each copy's own**