fix(gates): check body-level routing targets, not just descriptions #140

Merged
Defame1297 merged 1 commits from fix/124-body-level-routing-targets into main 2026-09-22 15:47:18 +00:00
Collaborator

What

The ADR-0020 boundary resolver only ever read a SKILL.md's description for routing targets. A target named in the body — a dispatch table row, a "run X" step — 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).

How

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.

Testing

  • tests/test-adr0020-targets.sh gains a dedicated "body-level routing targets (issue #124)" section pinning the two live true positives and all three guards.
  • Full suite (bash tests/run-tests.sh --strict): 21/21 test files, 423/423 assertions passing.
  • docs/spec/gates.md and ADR-0020 get a matching amendment (dated 2026-09-22).
  • factory-audit's SKILL.md version bumped 1.0.4 → 1.0.5 per ADR-0022 (scripts it owns changed).

Fixes #124


🤖 Generated with Claude Code

https://claude.ai/code/session_01EGHFJextYtVQseaHPDDhxB

## What The ADR-0020 boundary resolver only ever read a SKILL.md's **description** for routing targets. A target named in the **body** — a dispatch table row, a "run X" step — 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). ## How 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. ## Testing - `tests/test-adr0020-targets.sh` gains a dedicated "body-level routing targets (issue #124)" section pinning the two live true positives and all three guards. - Full suite (`bash tests/run-tests.sh --strict`): 21/21 test files, 423/423 assertions passing. - `docs/spec/gates.md` and ADR-0020 get a matching amendment (dated 2026-09-22). - `factory-audit`'s `SKILL.md` version bumped 1.0.4 → 1.0.5 per ADR-0022 (scripts it owns changed). Fixes #124 --- 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01EGHFJextYtVQseaHPDDhxB
Claude added 1 commit 2026-09-22 15:22:00 +00:00
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
Defame1297 approved these changes 2026-09-22 15:47:16 +00:00
Defame1297 merged commit d654dca056 into main 2026-09-22 15:47:18 +00:00
Defame1297 deleted branch fix/124-body-level-routing-targets 2026-09-22 15:47:19 +00:00
Sign in to join this conversation.