fix(gates): check body-level routing targets, not just descriptions
The ADR-0020 boundary resolver (boundary_targets()/unresolved_targets()) only ever read a SKILL.md's description. A target named in the BODY -- a dispatch table row, a "run X" step, both routine in a 900-word procedure -- was checked by nothing. Two real instances shipped before either was caught by reading rather than by a gate: bin/write-docs routed twice to a deleted `to-prd` skill, and bin/triage told an agent to run a nonexistent `/setup-matt-pocock-skills` (both fixed in 03abcff; that fix was the symptom, this gate is the actual ask per #124). Added a separate, narrower extractor -- body_targets() / unresolved_body_targets() in the shared lib-boundary-resolver.sh -- rather than reusing the description resolver at wider scope. The description gate's sentence-level heuristics (BOUNDARY_MARKER, the follower test, in-sentence corroboration) are tuned for a one-to-three-sentence routing clause and misfire on dispatch-table/procedure prose in both directions, so the body gate reads only explicit route notation (`/name`, backticked-or-slash-prefixed `-> name` / `-> name`), already the description gate's own unconditionally-blocking tier. Three guards were added after running the extractor over the real 39-skill corpus and reading every hit rather than assuming the design was correct: - a target must be hyphenated, even in notation -- single-word citations like `/fork` (forge, citing Claude Code's own /fork command) and `/name` (skill-author, a placeholder) are not routes. - a bare hyphenated word after any arrow is not notation -- only ARROW_MARKED (backticked/slash-prefixed) is used, not NOTATION_ARROW's bare form, so ordinary process-chain prose ("prop -> new ref -> re-render", caveman) is not read as a route. - a name immediately preceded by `<` is a closing tag (`</what-to-do>`, grill-with-docs), not /name notation. Wired into both consumers that must agree by contract: scripts/ skill-size-check.sh (the pre-commit hook) and factory-audit's lib-checks-skill.sh (the audit). Verified identical findings across both over the whole corpus. tests/test-adr0020-targets.sh gains a dedicated section pinning the two live true positives and all three guards. docs/spec/gates.md and ADR-0020 get a matching amendment. Fixes: #124 ADR: 0020 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EGHFJextYtVQseaHPDDhxB
This commit is contained in:
@@ -414,6 +414,56 @@ and rises to a blocking ERROR the moment a resolving sibling joins it. The reaso
|
||||
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.
|
||||
|
||||
## Amendment (2026-09-22): body-level routing targets are resolved too
|
||||
|
||||
The Decision section's routing-target resolver (`boundary_targets()` / `unresolved_targets()`) reads
|
||||
the **description** only. A target named in the **body** — a dispatch table row, a "run X" step, both
|
||||
routine in a 900-word procedure — was checked by nothing. Two real instances shipped before either
|
||||
was caught: `bin/write-docs` routed twice to a deleted `to-prd` skill, and `bin/triage` told an agent
|
||||
to run a nonexistent `/setup-matt-pocock-skills`. Both were found by reading, not by a gate, during
|
||||
the #99 retrofit and its follow-up audit; both were fixed in `03abcff`. **The fix this amendment
|
||||
records is the gate, not those two edits** (issue #124).
|
||||
|
||||
The body gate is a **separate, narrower** extractor (`body_targets()` /
|
||||
`unresolved_body_targets()`), not the description resolver reused at wider scope. The description
|
||||
resolver's sentence-level heuristics — `BOUNDARY_MARKER`, the follower test, in-sentence
|
||||
corroboration — are tuned for a one-to-three-sentence routing clause and misfire on dispatch-table
|
||||
and procedure prose in both directions: under-firing on a table row that carries no "do not" /
|
||||
"instead", over-firing on a procedure step naming a file, a CLI verb or a config key exactly the way
|
||||
a route names a skill. Retuning those heuristics for the body genre was considered and rejected as
|
||||
the harder half of the problem, with a materially worse cost of getting it wrong (a body is loaded
|
||||
on every invocation, so a false-positive-prone body gate is felt far more often than a
|
||||
false-positive-prone description gate).
|
||||
|
||||
So the body gate reads **only** explicit route notation — `/name` and backticked-or-slash-prefixed
|
||||
`-> name` / `→ name` — already the description gate's own unconditionally-blocking tier, and nothing
|
||||
softer: no SUGGESTION tier, no bare-word forms, no corroboration. Two further restrictions, both
|
||||
earned by a real corpus false positive rather than assumed up front:
|
||||
|
||||
- **the target must be hyphenated**, even in notation. `` `/fork` `` (`forge/SKILL.md`, citing
|
||||
Claude Code's own `/fork` subagent command) and `` `/name` `` (`skill-author/SKILL.md`, a
|
||||
placeholder for the skill's own name) are real corpus citations of a tool or a placeholder, not
|
||||
routes, and both hard-FAILed with no escape hatch before this restriction. This is the same
|
||||
"single-word targets are ordinary English" trade the Decision section already makes for the bare
|
||||
form, extended to notation because the body genre has no boundary-sentence signal to fall back on;
|
||||
- **a bare hyphenated word after any arrow is not notation.** The description gate's own bare-arrow
|
||||
sweep (`NOTATION_ARROW`) reads ordinary process-chain prose as a route: `caveman`'s "Inline obj
|
||||
prop -> new ref -> re-render." dangled to `re-render` under it. The body gate uses `ARROW_MARKED`
|
||||
instead, which requires the target to be backticked or slash-prefixed — true of the one real
|
||||
historical target (`` -> `to-prd` ``, confirmed against `03abcff`'s diff), so this costs no real
|
||||
coverage;
|
||||
- a target immediately preceded by `<` is a closing tag (`</what-to-do>`, `<supporting-info>` — this
|
||||
repo's own `grill-with-docs/SKILL.md` uses these as prompt section delimiters), not `/name`
|
||||
notation, and is discarded on that basis alone.
|
||||
|
||||
Both consumers — `scripts/skill-size-check.sh` and `factory-audit/scripts/lib-checks-skill.sh` —
|
||||
call the shared functions independently over the same `known_targets()` universe the description
|
||||
check already computed, so a body target folds into the existing "DID NOT RUN" INFO tier rather than
|
||||
adding a second one. `tests/test-adr0020-targets.sh` pins the two live true positives, all three
|
||||
guards above, and the fenced-code-block mask; the corpus-wide dangling assertion now covers body
|
||||
targets the same way it already covered description ones. `docs/spec/gates.md`'s "Body-level routing
|
||||
targets" section states the enforced shape in full.
|
||||
|
||||
## Consequences
|
||||
|
||||
**Editing any non-compliant skill now requires retrofitting it first.** At decision time, 30 of 39
|
||||
|
||||
Reference in New Issue
Block a user