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:
@@ -203,8 +203,14 @@ with no trigger list.
|
||||
|
||||
Verified end-to-end rather than assumed: `plugins/bin/.apm/skills/zoom-out/SKILL.md:4` carries the
|
||||
flag, apm passes it through verbatim to both `.claude/skills/zoom-out/SKILL.md:4` and the flat mirror
|
||||
at `plugins/bin/skills/zoom-out/SKILL.md:4`, and `zoom-out` is the one installed skill absent from
|
||||
the model-visible skill listing in a live session. It remains invocable as `/zoom-out`.
|
||||
at `plugins/bin/skills/zoom-out/SKILL.md:4`, and `zoom-out` was — at the time of that check, when it
|
||||
was the only carrier — the one installed skill absent from the model-visible skill listing in a live
|
||||
session. It remains invocable as `/zoom-out`. `caveman` has since taken the flag as well, so the
|
||||
corpus now has **two** carriers. Do not read a carrier list off this page; re-derive it:
|
||||
|
||||
```
|
||||
grep -l '^disable-model-invocation: true' plugins/*/.apm/skills/*/SKILL.md
|
||||
```
|
||||
|
||||
### Merging siblings
|
||||
|
||||
@@ -219,10 +225,13 @@ rather than the core job.
|
||||
skills still exist separately, and this change made the split deeper rather than shallower: retrofit
|
||||
to the dispatch pattern took `skill-audit` from 3 reference files to 7 and `agent-audit` from 4 to 8,
|
||||
and their two same-named `references/description-quality.md` files now differ on 100 of ~120 lines
|
||||
after normalising `skill`/`agent`, where before they were closer. The merge stays the decision; it
|
||||
reopens ADR-0008 (agent-audit's single-file invocation contract) and touches every call site in
|
||||
`skill-author`, `agent-author` and `forge`, which is why it is its own change and not a rider on
|
||||
this one. Recorded here rather than dropped, so the gap between the rule and the tree is deliberate
|
||||
after normalising `skill`/`agent`, where before they were closer. It has kept deepening since: the
|
||||
#99 retrofit added `finding-criteria.md` to `skill-audit`, drawing it level with `agent-audit`. Both
|
||||
figures move with the next retrofit, so measure rather than quote —
|
||||
`ls plugins/kyberforge/.apm/skills/<name>/references/ | grep -c '\.md$'`. The merge stays the
|
||||
decision; it reopens ADR-0008 (agent-audit's single-file invocation contract) and touches every call
|
||||
site in `skill-author`, `agent-author` and `forge`, which is why it is its own change and not a rider
|
||||
on this one. Recorded here rather than dropped, so the gap between the rule and the tree is deliberate
|
||||
and dated instead of discovered later.
|
||||
|
||||
### Enforcement and rollout
|
||||
@@ -233,11 +242,13 @@ which tier each rule is in, because the failure this ADR is most exposed to is a
|
||||
|
||||
| Check | Applies to | Tier | Home |
|
||||
|---|---|---|---|
|
||||
| description characters (250 SUGGESTION / 400 FAIL) | skills, agents | deterministic | `scripts/skill-size-check.sh`; constants mirrored in `skill-audit/scripts/validate.sh` and `agent-audit/scripts/validate.sh` |
|
||||
| description characters (250 SUGGESTION † / 400 FAIL) | skills, agents | deterministic | `scripts/skill-size-check.sh`; constants mirrored in `skill-audit/scripts/validate.sh` and `agent-audit/scripts/validate.sh` |
|
||||
| body-only words (600 SUGGESTION / 900 FAIL) | skills | deterministic | `skill-size-check.sh`, `skill-audit/scripts/validate.sh` |
|
||||
| description present and non-empty (ERROR) | skills, agents | deterministic | same |
|
||||
| boundary target resolves to a real skill or agent (ERROR when written as `/name` or `-> name`, or when its own sentence names another target that resolves; SUGGESTION otherwise) | skills, agents | deterministic | same |
|
||||
| boundary clause absent (SUGGESTION) | skills, agents | deterministic | same |
|
||||
| boundary target resolves to a real skill or agent (ERROR when written in route notation — `/name`, or any arrow form; or when a *terminal* bare name's own sentence names another target that resolves; SUGGESTION otherwise) | skills, agents | deterministic | same |
|
||||
| boundary clause absent — `absent` (SUGGESTION) † | skills, agents | deterministic | same |
|
||||
| an arrow clause is present but no target can be read out of it — `unparsed` (SUGGESTION) † | skills, agents | deterministic | same |
|
||||
| one arrow clause naming two or more targets, of which only the first is resolved (SUGGESTION, issue #107) † | skills, agents | deterministic | same |
|
||||
| Gotchas entry count over five (SUGGESTION) | skills | deterministic | same |
|
||||
| Gotchas over 25% of the body (SUGGESTION) | skills | deterministic | same |
|
||||
| every `references/<file>.md` a body names exists (ERROR) | skills | deterministic | same |
|
||||
@@ -254,6 +265,21 @@ that guessed at them would be a worse gate than no gate, because it would be bel
|
||||
enforced, they are reviewed, and this table exists so that distinction is written down rather than
|
||||
inferred from whether a validator happens to have been written yet.
|
||||
|
||||
**† These four, and only these four, are lifted for a hand-invoked file** — one whose frontmatter
|
||||
carries `disable-model-invocation: true`, read as a boolean by `hand_invoked()` in all three scripts.
|
||||
No validator knew the field existed (issue **#108**), so every routing SUGGESTION above fired on
|
||||
exactly the shape the *Invocation as a design axis* section mandates, and the boundary-clause
|
||||
remedy — "so the router knows where NOT to send this skill" — was addressed to a router that cannot
|
||||
see the skill at all. An author who took the advice made the file worse.
|
||||
|
||||
What does **not** lift is the point of the carve-out. Both body word tiers stand: the body is still
|
||||
loaded on invocation and still competes with the caller's live conversation. The 400-character
|
||||
description FAIL stands: that description is not preloaded, but it is the one line a user reads when
|
||||
choosing from the `/` menu, and the ceiling is an outlier stop rather than a routing-quality budget —
|
||||
which is exactly why the 250-character *target* is the tier that lifts. And a target the description
|
||||
does happen to name is still resolved and can still dangle as a blocking ERROR. Mechanics, and the
|
||||
reason the field is read as a boolean rather than as a mention of the key: `docs/spec/gates.md`.
|
||||
|
||||
Two of the deterministic rows are tuned for **false positives over recall**, and what they decline to
|
||||
see is part of the contract. On target extraction: a bare hyphenated name counts only inside a
|
||||
boundary sentence, and a single-word name is never matchable bare — `research`, `triage`, `forge`,
|
||||
@@ -264,7 +290,8 @@ raise an error: one followed by an ordinary lowercase noun is a compound **modif
|
||||
confirm-only — it still resolves and still counts as a route when the name exists, but it can never
|
||||
dangle. Only a *terminal* target can. The compressed arrow form `→ <name>` is exempt from that
|
||||
follower test and is always error-eligible, because nothing reads as a compound modifier after an
|
||||
arrow; a `/slash` target reached through a route verb is **not** exempt and takes the same test. The
|
||||
arrow; a `/slash` target reached through a route verb is **not** exempt and takes the same test.
|
||||
*Amended 2026-08-31 — the `/slash` half is reversed: it is exempt too. See the amendment below.* The
|
||||
simpler rule — "only marked targets may dangle" — was available and would have been wrong here: both
|
||||
live true positives are bare, `research`'s "(use neuledge-context)" and the `gitea-labels-` /
|
||||
`milestones` fold. On the body-shape checks: a `## Gotchas` heading must *end* in "gotchas", not
|
||||
@@ -296,6 +323,46 @@ Three pre-existing contradictions are fixed in the same change, because they are
|
||||
- `description-quality.md:45-50` has no FAIL condition for internal-mechanics content, which is why
|
||||
`skill-author/SKILL.md:102` never bit.
|
||||
|
||||
## Amendment (2026-08-31): route notation short-circuits the follower test, `/name` included
|
||||
|
||||
The Enforcement section above exempts the arrow form from the follower test and then withholds the
|
||||
same exemption from `/name`: "a `/slash` target reached through a route verb is **not** exempt and
|
||||
takes the same test." That half is reversed. **Both spellings of route notation are exempt, and the
|
||||
exemption is decided before the follower test rather than weighed against it.**
|
||||
|
||||
Three things make the original call wrong rather than merely strict.
|
||||
|
||||
**It contradicted the promise the same paragraph makes.** Route notation is offered to an author as
|
||||
the way to get a target checked unconditionally — the SUGGESTION text on an unpromoted target says
|
||||
so in as many words: "write it as `/name` or `-> name` and it will be checked properly." Under the
|
||||
original rule that was true of one of the two spellings. `-> name` reached `_add()` with
|
||||
`strict=True` from both its call sites; `/name` did not, so it fell through to `_terminal()` and any
|
||||
follower outside `FOLLOWER_OK` demoted it. `Do not use for Y — use /no-such-skill afterwards.` exited
|
||||
0 — and, before the companion visibility fix, in total silence.
|
||||
|
||||
**The follower test's own justification does not reach `/name`.** That test exists for *prose*: a
|
||||
bare hyphenated token followed by an ordinary lowercase noun is a compound modifier, "pre-commit
|
||||
hooks" and "pull-request template". A leading slash is Claude Code's invocation syntax and occurs in
|
||||
no English compound, so there is no attributive reading to protect. The exemption was withheld from
|
||||
the one shape the rule it protects against cannot describe.
|
||||
|
||||
**`FOLLOWER_OK` is a closed whitelist of roughly eighty words, and a closed list is the wrong thing
|
||||
to hang a blocking gate on.** Leaving `/name` under it made *whether a commit is blocked* depend on
|
||||
whether someone had thought to enumerate the next word — the gate failing open on its own
|
||||
unfamiliarity. The bare-target path keeps the follower test precisely because it needs a brake it can
|
||||
justify; the notation path asked for one and was given the same brake by accident.
|
||||
|
||||
What is unchanged: the **corroboration** branch. A *bare* terminal name still earns its blocking
|
||||
ERROR only from a resolving sibling in the same sentence, and a compound modifier still cannot
|
||||
dangle at all. The conservative tuning that decision rests on is untouched — this amendment moves one
|
||||
explicitly-marked spelling out from under it, not the prose path.
|
||||
|
||||
Verified on fixtures inside a synthetic plugin tree: `… Do not use for Y — use /no-such-skill
|
||||
afterwards.` exits 1, while the same sentence with the bare `no-such-skill` exits 0 at SUGGESTION,
|
||||
and rises to a blocking ERROR the moment a resolving sibling joins it. The reasoning is recorded at
|
||||
the point of enforcement in `_add()`'s docstring in `scripts/skill-size-check.sh` and its two
|
||||
mirrored copies, and the verdict table in `docs/spec/gates.md` states the corrected shape.
|
||||
|
||||
## Consequences
|
||||
|
||||
**Editing any non-compliant skill now requires retrofitting it first.** At decision time, 30 of 39
|
||||
@@ -342,8 +409,8 @@ and `git-*` families — where every sibling shares a keyword and boundary claus
|
||||
— are the ones most likely to sit at the FAIL tier permanently. If the retrofit shows that family
|
||||
routing degrades, the tier is the first thing to revisit.
|
||||
|
||||
**Four broken routing targets were found; two are fixed here and two are live.** Tracked as issue
|
||||
#100.
|
||||
**Four broken routing targets were found; two were fixed here and two shortly after.** Tracked as
|
||||
issue #100.
|
||||
|
||||
- `skill-audit` routed to `/skill-improve` twice in its description plus `README.md:10`, and no such
|
||||
skill exists — the real target is `skill-author`. **Fixed here**, as a side effect of retrofitting
|
||||
@@ -353,14 +420,24 @@ routing degrades, the tier is the first thing to revisit.
|
||||
detectable by the resolvable-target check and never will be: "examine agent files manually" names
|
||||
no target, and a check that resolves names cannot see a name that is absent. A misroute to nowhere
|
||||
is a review finding, not a gate finding.
|
||||
- `research` routes to `neuledge-context`, which exists only inside that string. **Live.**
|
||||
- `research` routes to `neuledge-context`, which exists only inside that string. Was **live**;
|
||||
**fixed under #99** — the retrofitted description names no such target.
|
||||
- `gitea-issues` carries the literal string `gitea-labels- milestones` in its folded description, a
|
||||
stray space introduced by YAML wrapping mid-token, breaking the skill name in preloaded text.
|
||||
**Live** — the check reports it as a dangling `gitea-labels`.
|
||||
stray space introduced by YAML wrapping mid-token, breaking the skill name in preloaded text. Was
|
||||
**live**, reported as a dangling `gitea-labels`; **fixed under #99** — the name now folds intact.
|
||||
|
||||
So the check fires on 3 of the 4 against the base commit and on 2 at the tip of this change, and
|
||||
`tests/test-skill-size-check.sh` probes exactly those three by name rather than asserting a count, so
|
||||
it degrades to SKIP as #100 lands rather than going stale.
|
||||
So the check fired on 3 of the 4 against the base commit and on 2 at the tip of the change that
|
||||
carried this ADR. **The corpus dangling set is now empty**, and that is asserted rather than
|
||||
observed: `tests/test-adr0020-targets.sh` pins the set as empty, so a new boundary clause naming a
|
||||
non-existent skill fails the suite instead of joining a backlog. `tests/test-skill-size-check.sh`
|
||||
probed the three original names rather than asserting a count; as each was retrofitted its probe was
|
||||
**removed, not skipped**, because a `pass "SKIP: …"` branch is an assertion-free result counted in
|
||||
the totals and makes the suite look one test stronger than it is. That file's commentary survives the
|
||||
probes and states the rule. Re-derive the current set — never read it off this page:
|
||||
|
||||
```
|
||||
bash scripts/skill-size-check.sh plugins/*/.apm/skills/*/SKILL.md | grep 'does not resolve'
|
||||
```
|
||||
|
||||
**Duplication between `skill-author` and `agent-author` survives un-gated.** The merge rule
|
||||
deliberately excludes the author pair, so the commit-verification argument in four near-copies, the
|
||||
|
||||
Reference in New Issue
Block a user