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
617 lines
48 KiB
Markdown
617 lines
48 KiB
Markdown
# Skills and agents are authored against a context budget, not a spec ceiling
|
||
|
||
Every installed skill's `name` and `description` sits in every agent's context from the first token
|
||
of every session, whether or not the skill is ever invoked. Across this repo's 39 skills that is
|
||
23,427 characters — roughly 5,900 tokens — and the authoring rules that produced it optimised for
|
||
triggering reliability with no counter-pressure on size. This ADR sets the budget, the shape, and the
|
||
gates that hold them.
|
||
|
||
**Status: accepted (2026-08-14).**
|
||
|
||
**Amended by ADR-0025 (2026-09-15).** The contract, the tiers and every verdict rule below stand
|
||
unchanged. What moved is the **number and location of the scripts that carry them**. This ADR names
|
||
three: `scripts/skill-size-check.sh`, `skill-audit/scripts/validate.sh` and
|
||
`agent-audit/scripts/validate.sh` — "all three validators" (Decision), "all three scripts"
|
||
(Enforcement table footnote), "`scripts/skill-size-check.sh` and its two mirrored copies"
|
||
(the `_add()` amendment). ADR-0025 merged the two audit skills, so there are now **two**: the root
|
||
`scripts/skill-size-check.sh`, which still embeds the 1,061-line block between `BEGIN`/`END ADR-0020
|
||
SHARED BOUNDARY RESOLVER` markers, and one plugin copy — extracted out of the merged validator into
|
||
`plugins/kyberforge/.apm/skills/factory-audit/scripts/lib-boundary-resolver.sh` and sourced by
|
||
`factory-audit`'s `validate.sh`, once in each of its two mode branches, rather than pasted into
|
||
both. `validate-provenance.sh` is not a third reader: it sources `lib-contributing-files.sh` and one
|
||
of `lib-provenance-skill.sh`/`lib-provenance-agent.sh`, and never touches the resolver at all. The
|
||
Enforcement table's "constants mirrored in `skill-audit/scripts/validate.sh` and
|
||
`agent-audit/scripts/validate.sh`" now means `factory-audit/scripts/lib-checks-skill.sh:313-316`
|
||
(all four constants) and `lib-checks-agent.sh:164-165` (the two description ones). It does **not**
|
||
mean `factory-audit/scripts/validate.sh`, which holds none of them: `validate.sh` auto-detects the
|
||
artifact type and sources the matching check suite (`validate.sh:231-233`, `:244-246`). The
|
||
skills/agents columns are unaffected — only the skill suite carries the body tiers. The two copies
|
||
must still stay byte-identical — a
|
||
plugin script cannot source the root one, which is why a second copy exists at all. Read every
|
||
"three" below as the count at the time of writing.
|
||
|
||
**Amended again (2026-09-16): one copy.** `scripts/skill-size-check.sh` no longer embeds the
|
||
resolver. It sources `factory-audit/scripts/lib-boundary-resolver.sh` by path and fails closed if the
|
||
library is missing or defines no resolver. The embedded copy had been kept only because the hook was
|
||
also exported through `.pre-commit-hooks.yaml`, whose consumers could not reach a file inside the
|
||
plugin; `4de5b6b` retired that export (ADR-0014), so the hook runs only inside this repo. The
|
||
"byte-identical" sentence above is superseded: there is nothing left to keep identical, and
|
||
`tests/test-adr0020-contract.sh` assertion 1 now pins the single copy instead of hashing a pair.
|
||
|
||
## Context
|
||
|
||
Every `file:line` citation in this ADR is against the base commit the decision was taken on,
|
||
`f9b919d7e3bd5e6b51fbdf88b32ace0438b313e0`, not against current `HEAD`. The change that carries this
|
||
ADR rewrites several of the cited files, so a citation resolved against the worktree will land on
|
||
unrelated text. Use `git show f9b919d:<path>` to follow one.
|
||
|
||
Measured before any change, at that commit. Method, so the figures are reproducible: sum
|
||
`len(name) + len(description)` over the frontmatter of every `plugins/*/.apm/skills/*/SKILL.md`,
|
||
folding `>` block scalars to the value the host actually loads (most descriptions here are folded
|
||
scalars, so counting raw lines measures indentation instead); tokens at the standard
|
||
~4-characters-per-token approximation `scripts/skill-size-check.sh` uses. Word counts are
|
||
whitespace-separated tokens, and are stated as **body-only** or **whole-file** every time, never bare.
|
||
|
||
| | |
|
||
|---|---|
|
||
| 39 skill `name` + `description` | 23,427 chars, ~5,900 tokens, **preloaded every session** |
|
||
| 4 agent `name` + `description` | 1,325 chars, ~330 tokens, preloaded every session |
|
||
| skill bodies (body-only words) | median 684, mean 815, p90 1,349 |
|
||
| skill files (whole-file words) | median 816, mean 927, p90 1,526 |
|
||
| `MAX_WORDS` gate (`skill-audit/scripts/validate.sh:147`) | **2,770** whole-file — a density proxy, not a percentile |
|
||
|
||
That last row is worth stating plainly, because it is the first thing this ADR is about. 2,770 is not
|
||
derived from the corpus distribution at all: per the derivation comment in
|
||
`scripts/skill-size-check.sh`, it is 2,770 words at the densest observed 7.22 chars/word ≈ 20,000
|
||
chars ≈ the agentskills.io ~5,000-token ceiling. Neither percentile reaches it — 2× the body-only p90
|
||
is 2,698 and 2× the whole-file p90 is 3,052 — and reading it as "2× p90" would pair a whole-file gate
|
||
against a body-only distribution, which is exactly the conflation this ADR exists to stop.
|
||
|
||
Three findings drove this, none of which is "the descriptions drifted".
|
||
|
||
**The rules mandate the bloat.** `skill-author/SKILL.md:104` requires indirect triggers ("even if the
|
||
user doesn't mention X explicitly") and `skill-audit/references/description-quality.md:21` requires
|
||
authors to "err toward being pushy". Both are enforced. The one rule that would delete the waste —
|
||
`skill-author/SKILL.md:102`, "not the skill's internal mechanics" — is judgment-only and is absent
|
||
from the FAIL conditions at `description-quality.md:45-50`. The enforced rules inflate; the deflating
|
||
rule does not bite. The result is measurable: `gitea-files` spends 147 chars listing six verbs, then
|
||
301 chars re-quoting the same six as user phrasings, in the same order. `apm-workflow` does the same
|
||
with six capability clusters. Across the twelve longest descriptions, 30.7% is capability
|
||
enumeration and 11.6% is composition or implementation detail that cannot affect a routing decision.
|
||
|
||
**Capability enumeration in a description is a correctness hazard, not only a token cost.**
|
||
`plugins/kyberforge/docs/research/examples/skill-write/writing-skills/SKILL.md:154-158` reports a
|
||
measured failure: "when
|
||
a description summarizes the skill's workflow, an agent may follow the description instead of reading
|
||
the full skill content. A description saying 'code review between tasks' caused an agent to do ONE
|
||
review, even though the skill's flowchart clearly showed TWO reviews." `git-commits` is exactly that
|
||
shape — 74% of its description is capability enumeration, including a rules table (`header max 100
|
||
chars, lowercase subject, no trailing periods, 11 standard types`) an agent can act on without ever
|
||
loading the body.
|
||
|
||
**The upstream sources cannot settle this.** The four skill-writing references under
|
||
`plugins/kyberforge/docs/research/examples/skill-write/` disagree on what a description contains —
|
||
when-only (`writing-skills/SKILL.md:99`), what-and-when (`skill-creator/SKILL.md:67`,
|
||
`writing-skills/anthropic-best-practices.md:187`), triggers-only
|
||
(`writing-great-skills/SKILL.md:28`), and what-plus-when-plus-negative
|
||
(`write-skill/SKILL-TEMPLATE.md:5-6`). Those four paths are relative to that directory.
|
||
`writing-skills` and the Anthropic document it bundles contradict each other inside one skill
|
||
directory. They also disagree on whether
|
||
500 lines is binding, on the inline-versus-bundle threshold, and on the TOC threshold (>100 lines vs
|
||
>300 lines). "Grounded in the research" is therefore not available as a tiebreaker; a house choice is
|
||
required and this is it.
|
||
|
||
A fourth observation shaped the body half. The best progressive-disclosure ratio in the repo belongs
|
||
to `apm-workflow` — a 421-word body dispatching to 3,006 words of references — and the worst two
|
||
belong to the skills that define the house standard: `skill-author` (2,623-word body / 1,247 words of
|
||
references) and `agent-author` (2,582 / 1,664). Measured the other way, whole-file, those two are
|
||
2,760 and 2,758 words — ten and twelve words under the 2,770 gate their own plugin enforces. A
|
||
ceiling that nothing approaches is not a constraint; a ceiling that two files have grown into is a
|
||
target. The two numbers for one file are the point: 2,623 and 2,760 describe the same `skill-author`,
|
||
and only one of them is what either gate measures.
|
||
|
||
## Decision
|
||
|
||
### Descriptions
|
||
|
||
A description carries three things and nothing else: a **trigger clause**, at most one **capability
|
||
clause**, and a **boundary clause**. Capability enumeration, output-format detail, composition notes
|
||
("composes X rather than duplicating Y"), and implementation detail move to the body or to
|
||
`README.md`.
|
||
|
||
**Correction (2026-09-20): not `README.md`.** The canonical destination for description overflow is
|
||
"the body or a `references/` file" (`plugins/kyberforge/.apm/skills/skill-author/references/contract.md:35`).
|
||
A skill-root `README.md` is no longer somewhere overflow can go: all 39 of them were deleted, and
|
||
`factory-audit/references/skill-file-structure.md:23` now FAILs a non-spec file at the skill root,
|
||
which a `README.md` is. Read every "or to `README.md`" below as "or to a `references/` file".
|
||
|
||
- **250 characters SUGGESTION, 400 FAIL.** The agentskills.io 1,024-character limit remains as an
|
||
unchanged spec backstop. The SUGGESTION tier is what moves the average; the FAIL tier only stops
|
||
outliers.
|
||
- **A missing, valueless or `null` `description:` is a hard FAIL** in all three validators. That
|
||
reads as a trivial precondition and is not: a `description:` line with no value followed by
|
||
`model: sonnet` let a line regex capture the *next* key, which looked non-empty, so the "missing or
|
||
empty" branch never fired and every gate below it then early-returned on the genuinely empty folded
|
||
value — exit 0, zero output, on a blocking pre-push gate. Presence is decided on the YAML-folded
|
||
value and nowhere else. The field this contract is entirely about is the one field a gate must
|
||
never fail to notice is absent.
|
||
- **Boundary clauses compress** to `Not <thing> → <skill-name>.` and must name a target that
|
||
resolves to a real skill or agent. Resolution walks up **from the file being checked** to an
|
||
*authoring root* — the nearest ancestor holding `plugins/*/.apm/skills` or `plugins/*/.apm/agents`,
|
||
falling back to the nearest ancestor holding `.git`. Two passes rather than one interleaved walk,
|
||
so a nested `.git` (a submodule, a sub-package worktree) cannot beat a real monorepo root further
|
||
up. When an authoring root is found the universe is every skill and agent under
|
||
`<root>/plugins/*/`, plus the target's own apm package and the packages that package declares in
|
||
its own `apm.yml` `dependencies.apm`. Sibling plugins resolve against each other, which is what a
|
||
monorepo means. Deployed `.claude/`/`.agents/` trees are consulted **only** when the walk found no
|
||
plugin monorepo root — whether it landed on a bare `.git` ancestor or on nothing at all. That is
|
||
the consumer case, where there is no monorepo to read. The condition is which of the two passes
|
||
matched, never a name-count delta: a single-plugin monorepo re-collects its own package and adds
|
||
no new name, so a delta test reads zero there and would pull the deployed trees back in. What the
|
||
resolver must never do is
|
||
derive the universe from its own location: a `${BASH_SOURCE}`-relative repo root leaked this repo's
|
||
39-skill universe into every consumer repo running the hook through pre-commit, so a consumer skill
|
||
routing to `skill-audit` resolved against a plugin it had never installed. Checked
|
||
deterministically. A description carrying **no** boundary clause at all is a SUGGESTION, for skills
|
||
and agents alike: most descriptions want one, some genuinely have no near-miss sibling to exclude,
|
||
and that judgment is not a script's to make.
|
||
- **The verdict must not depend on whether `apm install` has been run.** Deployed trees are
|
||
gitignored install output, present only on a machine that has run it. Four cross-plugin targets
|
||
here (`gitea-branches` → `git-branches`, `gitea-branches` → `git-history`, `gitea-issues` →
|
||
`git-branches`, `gitea-workflow` → `git-workflow`) once resolved through `.claude/skills/` alone,
|
||
so the same commit measured 2 dangling targets on a developer machine and 6 on a fresh clone. A
|
||
gate shipping hot with no baseline cannot give two answers. Under the walk-up those four resolve
|
||
because sibling plugins are in the universe — no plugin here declares a cross-plugin apm
|
||
dependency, and none needs to. Verified: a tree holding only `plugins/` and the root `apm.yml`,
|
||
with no `.claude/` or `.agents/` anywhere, produced findings identical to the working tree. The
|
||
figures that reproduction recorded — 26 description FAILs, 9 body FAILs, 2 dangling targets, 0
|
||
missing references, 58 SUGGESTIONs — are the **pre-retrofit** corpus as it stood when the
|
||
experiment ran, kept here as the evidence for the install-independence claim, not as a current
|
||
reading. *Amended 2026-09-01: the #99 retrofit took the first three to zero. Measured at that
|
||
date over the same install-free tree: 0 description FAILs, 0 body FAILs, 0 dangling targets, 0
|
||
missing references, 29 SUGGESTIONs.* What the experiment establishes is that the two trees agree,
|
||
not what either measured; re-derive rather than quote —
|
||
`bash scripts/skill-size-check.sh plugins/*/.apm/skills/*/SKILL.md`.
|
||
- **The universe is the apm marketplace, and nothing else.** A routing target resolves to a skill or
|
||
an agent, or it does not resolve. Host built-ins are deliberately outside it: `/compact`, `/clear`
|
||
and `/init` are Claude Code slash commands with no counterpart in Copilot CLI or Codex, so a
|
||
vendor-neutral `.apm/` description routing to one is a portability defect and the hard FAIL is a
|
||
true positive, not a false one. An allowlist of known built-ins was **rejected**: it answers a
|
||
different question ("does this exist on *some* host?"), it cannot answer that portably from a
|
||
single source file, and it goes stale the next time a host ships a command — reintroducing the
|
||
same-commit-two-verdicts failure the bullet above exists to close. An author who needs to mention
|
||
one writes it un-slashed (``the `compact` built-in``), which is not route notation and makes no
|
||
routing claim.
|
||
- **Blocking is scoped to a sentence, which makes sentence boundaries load-bearing.** A prose-form
|
||
target earns a hard error only when its own sentence names another target that *resolves*; route
|
||
notation (`/name`, `→ name`) is exempt and always blocks. So the splitter is part of the contract,
|
||
not a detail of it. `e.g. "…"` is not a sentence end, and a sentence opening with a code span or a
|
||
lowercase skill name is a start; getting either wrong moves targets between the two tiers in
|
||
opposite directions — a stranded corroborator silently demotes a real finding to SUGGESTION, and a
|
||
missed boundary lets one sentence vouch for a target it never stood beside, producing a hard FAIL
|
||
with no escape hatch.
|
||
- **The blanket pushiness rules are deleted.** `skill-author/SKILL.md:104` and
|
||
`description-quality.md:21` are replaced by a conditional: add an indirect trigger only where the
|
||
user's natural phrasing genuinely omits the domain word — true for the `gitea-*` family, false for
|
||
`git-commits`. Stating the same trigger twice in two registers is a FAIL.
|
||
|
||
### Bodies
|
||
|
||
The body carries the **decision procedure only**: ordered steps, decision branches, gates, and which
|
||
reference to load when. Lookup tables, spec restatements, output schemas, templates, and rationale
|
||
prose move to `references/` behind an explicit "read X when Y" trigger.
|
||
|
||
- **600 words SUGGESTION, 900 FAIL, counted body-only** — everything after the closing `---` of the
|
||
frontmatter. The 2,770-word / 500-line spec backstop is unchanged, keeps its existing meaning
|
||
(conformance, not quality), and keeps counting the **whole file including frontmatter**. These are
|
||
two different gates measuring two different things, and conflating them is what produced the
|
||
current state.
|
||
- **Dispatch is mandatory at two or more mutually exclusive flows.** The body carries the dispatch
|
||
table and the gates that apply to every branch; each flow lives in its own self-contained
|
||
`references/` file. This is `apm-workflow/SKILL.md:33-41` promoted from accident to rule. "Two
|
||
mutually exclusive flows" is not decidable from file text, so this rule is auditor judgment — see
|
||
Enforcement below for what that means and does not mean.
|
||
- **Every `references/<file>.md` a body names must exist.** A dispatch table pointing at a file that
|
||
was never written is a silently dead branch. Checked deterministically.
|
||
- **Gotchas are constrained.** A Gotcha must state a fact that contradicts a reasonable default —
|
||
something the agent gets wrong by acting sensibly. More than five entries is a SUGGESTION, as is a
|
||
Gotchas section exceeding 25% of the body; both are countable and both are checked
|
||
deterministically. A Gotcha that paraphrases a step in the body below it is a FAIL, but a FAIL an
|
||
auditor issues, not a script — semantic equivalence is not pattern-matchable.
|
||
|
||
### Agents
|
||
|
||
Agents take the same description gates — they are preloaded identically — and **no body word gate**.
|
||
A skill body is loaded into the caller's context, competing with the live conversation; an agent body
|
||
becomes the system prompt of a fresh context. The rationale for the 900-word FAIL does not transfer.
|
||
|
||
That exemption is expressed in `agent-audit/scripts/validate.sh` (now `factory-audit`'s
|
||
auto-detecting `validate.sh` on its agent path, see ADR-0025), which has no body constant, and in
|
||
the `files:` pattern of the `skill-size-check` pre-commit hook, which is `SKILL.md`-only. It is *not*
|
||
expressed in `scripts/skill-size-check.sh` itself, which measures whatever path it is handed —
|
||
running it directly over `plugins/*/.apm/agents/*.agent.md` exits 1 with 900-word body FAILs on
|
||
`git-orchestrate` and `gitea-orchestrate`. *Amended 2026-09-01: this sentence named a third agent,
|
||
`apm-orchestrate`, at 1,080 words. It is 876 today — a SUGGESTION, not a FAIL. Counts are
|
||
deliberately no longer pinned here: agent bodies are edited like any other file and a figure in this
|
||
paragraph goes stale the moment one is trimmed. Run the command.* Agents escape by
|
||
file pattern, not by the script knowing the difference. Anyone widening that pattern to cover agents
|
||
would silently enforce a gate this ADR declines to set.
|
||
|
||
A plugin-scope agent is a single file with no sibling `references/` directory, so it cannot disclose
|
||
to itself — it can only delegate to skills. `agent-audit` (now `factory-audit`'s agent flow, see
|
||
ADR-0025) therefore gains a **delegation check**: an
|
||
agent body that restates a procedure owned by a skill it can invoke is a FAIL, with the fix being
|
||
"invoke `<skill>` instead". Length falls out of delegation rather than being gated directly.
|
||
|
||
### Invocation as a design axis
|
||
|
||
`skill-author` asks whether a skill is model-invoked or hand-invoked before writing a description. A
|
||
hand-invoked skill sets `disable-model-invocation: true` and carries one plain human-facing sentence
|
||
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 `.claude/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`. (That check also
|
||
covered the flat mirror at `plugins/bin/skills/zoom-out/SKILL.md:4`, which ADR-0024 deleted on
|
||
2026-09-14; only the `.apm/` source and the deployed copy remain, and the pass-through is unaffected.) `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
|
||
|
||
Two skills that share substantial content, name each other as near-misses, and differ only in the
|
||
type of input they take should be **one skill with a dispatch table**. This catches `skill-audit` +
|
||
`agent-audit` and is scoped to them; the author pair is explicitly excluded, because
|
||
`skill-author` and `agent-author` emit genuinely different artifacts (a skill directory versus a
|
||
one-or-two-file agent pair, per ADR-0005 and ADR-0016) and their overlap is in the improve flow
|
||
rather than the core job.
|
||
|
||
**DEFERRED when this ADR was written — not implemented in the change that carries it. Tracked as
|
||
issue #101. IMPLEMENTED by ADR-0025 (2026-09-15), which merged the pair into `factory-audit` with a
|
||
Step 0 dispatch and closed the deferral.** At the time of writing both
|
||
skills still existed 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. 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/factory-audit/references/ | grep -c '\.md$'`. The merge stayed 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 was its own change and not a rider
|
||
on this one. Recorded here rather than dropped, so the gap between the rule and the tree was deliberate
|
||
and dated instead of discovered later — and ADR-0025 is where it was closed.
|
||
|
||
### Enforcement and rollout
|
||
|
||
Gates land where the existing gates already live — no new layer. The table below is exhaustive about
|
||
which tier each rule is in, because the failure this ADR is most exposed to is a rule filed under
|
||
"Enforcement" that no validator implements:
|
||
|
||
| 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` (now `factory-audit/scripts/lib-checks-skill.sh:313-314` and `lib-checks-agent.sh:164-165`, see the ADR-0025 amendment — **not** `factory-audit/scripts/validate.sh`, which holds no constants) |
|
||
| body-only words (600 SUGGESTION / 900 FAIL) | skills | deterministic | `skill-size-check.sh`, `skill-audit/scripts/validate.sh` (now `factory-audit/scripts/lib-checks-skill.sh:315-316`, see the ADR-0025 amendment) |
|
||
| description present and non-empty (ERROR) | skills, agents | deterministic | same |
|
||
| boundary target resolves to a real skill or agent — **three** verdicts, not two (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. INFO, "DID NOT RUN", exit 0, when no skill universe could be determined for the path at all — no authoring root above it, no apm package root, no declared apm dependencies, no deployed `.claude/` or `.agents/` tree: the targets are named and left unchecked) | 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 |
|
||
| description opener, composition notes in a description | skills, agents | prose pattern | `plugins/kyberforge/.apm/skills/*/assets/vale/styles/Kyberforge/` |
|
||
| a Gotcha paraphrasing a body step | skills | **auditor judgment** | `references/body-discipline.md` (now `references/skill-body-discipline.md`, see ADR-0025) |
|
||
| dispatch at two or more mutually exclusive flows | skills | **auditor judgment** | `references/body-discipline.md` (now `references/skill-body-discipline.md`, see ADR-0025) |
|
||
| delegation: an agent body restating a skill's procedure | agents | **auditor judgment** | `agent-audit` (now `factory-audit`'s agent flow, see ADR-0025) |
|
||
| capability enumeration, restatement, trigger quality | skills, agents | **auditor judgment** | `references/description-quality.md` (now two files, `references/skill-description-quality.md` and `references/agent-description-quality.md`, see ADR-0025 — this row applies to both artifact types, and the merge splits every flow-specific reference by flow) |
|
||
|
||
The rows in bold are stated as FAILs in the Decision above and are FAILs an *auditor* issues. None of
|
||
them is countable: "does this Gotcha paraphrase step 4", "are these two flows mutually exclusive" and
|
||
"does this agent body restate what `git-commits` already owns" are semantic questions, and a script
|
||
that guessed at them would be a worse gate than no gate, because it would be believed. They are not
|
||
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`,
|
||
`prototype` and `tdd` are all real skill names *and* ordinary English, so it must be written
|
||
`` `forge` `` or `/forge` to be seen at all. Grammar then decides whether a recognised target may
|
||
raise an error: one followed by an ordinary lowercase noun is a compound **modifier**, not a route
|
||
("use pre-commit hooks instead of ad-hoc scripts", "invoke the pull-request template"), so it is
|
||
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.
|
||
*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
|
||
merely contain the word, so `## Gotcha handling` and `## Why gotchas matter` are prose sections and
|
||
are skipped; fenced code blocks are masked out of heading detection and entry counting, so a fenced
|
||
example list is not mistaken for the section; and a `references/` pointer named on a line
|
||
that also says the file is gone ("removed", "deprecated", "no longer") is read as a historical
|
||
mention rather than a dead dispatch entry. Note the 25% fraction is deliberately *not* fence-masked
|
||
on either side — fenced lines are real body words, and the fraction is measured against the whole
|
||
body.
|
||
|
||
**The deterministic tier blocks immediately, with no baseline file.**
|
||
|
||
Three pre-existing contradictions are fixed in the same change, because they are the contract:
|
||
|
||
- `skill-audit/SKILL.md:58` asks whether the description opens with an action verb ("Audits…",
|
||
"Reviews…"), while `:56` defers the same question to `Kyberforge.DescriptionOpener` and
|
||
`skill-author/SKILL.md:101` requires an imperative "Use when…" opener. The criterion is
|
||
unsatisfiable against the house's own skills, both of which open with "Use when".
|
||
- `DescriptionOpener.yml` is anchored to `^This (skill|agent)\b`, which misses a plain `This …`
|
||
opener; it is widened here to `^This\b`. The anchor itself stays. Composition prose that sits
|
||
*mid*-description — `gitea-workflow`'s "This is the human-facing entry point…" at character 377,
|
||
`gitea-labels-milestones`'s "This is a cross-cutting shared skill…" at character 300 — was never in
|
||
the opener rule's scope and correctly is not: under `scope: text.frontmatter.description` the `^`
|
||
anchors to the start of the whole folded value, and un-anchoring to reach mid-description text was
|
||
measured at 5 hits and 5 false positives and rejected (`LESSONS.md`, 2026-08-14). The real gap is
|
||
that no rule covered that text at all, which a new token-list rule, `Kyberforge.CompositionNote`,
|
||
closes: 10 alerts across four `gitea-*` skills, 0 false positives.
|
||
- `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.
|
||
|
||
## 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
|
||
descriptions exceeded 400 characters and 13 of 39 bodies exceeded 900 words — the latter counted
|
||
body-only, which is what the new gate measures; the pre-existing 2,770-word gate counts the whole
|
||
file including frontmatter, and the two must not be conflated. The change that carries this ADR also
|
||
retrofits kyberforge's own four author/audit skills, so the figures on landing are **26 and 9**.
|
||
With the gate hot and no baseline, a one-line
|
||
fix to `gitea-prs` cannot be committed until that skill meets the contract. This is deliberate — it
|
||
guarantees convergence and avoids a half-state — but it means the retrofit is lazy and *mandatory*
|
||
rather than deferred. Issue #99 tracks it and should be prioritised accordingly, and the risk it
|
||
carries is the ordinary one for hot gates: a gate expensive enough to be inconvenient gets bypassed
|
||
with `SKIP=` and loses its authority.
|
||
|
||
**A second hot gate ships alongside it, and it is easy to miss.** `Kyberforge.CompositionNote` is
|
||
`level: error` like every other rule in that style, so at decision time `pre-commit run --all-files`
|
||
was red on 10 alerts across `gitea-issues`, `gitea-labels-milestones`, `gitea-prs` and
|
||
`gitea-workflow` independently of anything `skill-size-check` reports. Someone scoping the #99
|
||
retrofit off the size findings alone would have fixed those and still been blocked. The two gates
|
||
wanted fixing together, and were. *Amended 2026-09-01: that figure is historical. The Vale prefilter
|
||
over the same 39 files now reports 0 errors, 0 warnings and 0 suggestions, so
|
||
`Kyberforge.CompositionNote` fires nowhere in the corpus today. The rule is still hot and still
|
||
independent of `skill-size-check`, so a new description can reintroduce it; `skill-size-check` does
|
||
not cover the Vale half, and no `references/` file is linted by anything (`docs/spec/gates.md` has
|
||
both causes, issue #117 tracks them). Re-derive rather than quote —*
|
||
`bash plugins/kyberforge/.apm/skills/factory-audit/scripts/vale-wrap.sh plugins/*/.apm/skills/*/SKILL.md`
|
||
*(path re-pointed by ADR-0025; the `skill-audit` copy this ADR originally named no longer exists).*
|
||
|
||
**A ceiling does not produce an average.** If every author writes to the 400-character FAIL, the
|
||
preload lands at 39 × 400 = 15,600 chars — a 33% cut off 23,427, not the ~50% intended. Writing to
|
||
the 250-character SUGGESTION instead lands at 9,750, a 58% cut. The halving depends entirely on the
|
||
250-character SUGGESTION tier being visible and respected. That tier works here in a way it does not
|
||
elsewhere in this repo: `skill-audit` (now `factory-audit`'s skill flow, see ADR-0025) already
|
||
reports `PASS (N suggestions)` as a first-class
|
||
outcome. This is explicitly **not** the failure ADR-0013 records — Vale warnings are invisible
|
||
because vale's exit code keys on `error` alone, but these gates live in `validate.sh` and the
|
||
audit skill itself, where a SUGGESTION reaches the report. Realistic landing is somewhere in that 33-58%
|
||
band, not a guaranteed 50%.
|
||
|
||
**A word gate cannot detect the defect it is standing in for.** `git-commits` carries twelve Gotchas
|
||
of which four restate steps in its own Workflow (`:32` ≡ step 9, `:33` ≡ step 9, `:36` ≡ step 2,
|
||
`:31` ≡ the description). Its body is 1,102 words and its whole file 1,217, so it does fail the
|
||
900-word body FAIL — but for its length, not for the restatement. The four duplicated Gotchas are 114
|
||
words between them; delete every one and the file still fails, while a skill 250 words shorter with
|
||
the identical defect passes clean. The two properties are uncorrelated, which is why the counts are a
|
||
backstop to the dispatch rule and the Gotchas constraint — both of which are auditor judgment for the
|
||
semantic half, per the Enforcement table — and not a substitute for them. Reading the word gate as
|
||
the mechanism is the specific mistake this paragraph exists to prevent.
|
||
|
||
**Some skills legitimately need more description budget than others.** A tiered limit keyed to
|
||
sibling density was considered and rejected as too clever; the flat 250/400 pair means the `gitea-*`
|
||
and `git-*` families — where every sibling shares a keyword and boundary clauses do real routing work
|
||
— 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 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
|
||
kyberforge's own skills.
|
||
- `agent-author` said "Do not use for read-only review — examine agent files manually", routing away
|
||
from `agent-audit`, the correct sibling. **Fixed here**, same way. Note this one was never
|
||
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. 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. Was
|
||
**live**, reported as a dangling `gitea-labels`; **fixed under #99** — the name now folds intact.
|
||
|
||
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
|
||
root-cause grouping rule in four copies, and the wholesale clone of the "Improving an existing X"
|
||
flow all remain. Cache isolation makes them structurally unavoidable
|
||
(`skill-audit/SKILL.md:95` forbids cross-skill references; `LESSONS.md:107` records why), so the
|
||
options are a sync gate or continued drift. This is an input to issue #101, which carries both halves
|
||
of the kyberforge duplication problem — the deferred audit-pair merge and this — not a solved
|
||
problem. *Amended 2026-09-16: decided — continued drift, no sync gate. The measured overlap (about
|
||
150–180 lines, 36 of them shared Description prose; see the rejected alternative below and ADR-0025
|
||
point 7) does not justify a text-sync gate, so the author-pair duplication stays unguarded by
|
||
decision. With the audit pair merged by ADR-0025, both halves of #101 are settled. The
|
||
`skill-audit/SKILL.md:95` citation above is historical; the cross-skill reference rule now lives in
|
||
`factory-audit`'s `references/skill-file-structure.md`, which allows only the possessive form.*
|
||
|
||
**Provenance frontmatter is explicitly out of scope.** `LESSONS.md:63` asserts that non-routing
|
||
frontmatter (`source_keys`, `category`, `version`) is loaded at agent startup, which would make the
|
||
7,352 characters of it across the corpus a third again on top of the description tax. Measured
|
||
against a live session on this Claude Code version, it is not: the model-visible skill listing
|
||
contains only `name` and `description`. That is host-observed rather than spec-guaranteed and says
|
||
nothing about Copilot CLI, but it is sufficient to establish that cutting `source_keys` would break
|
||
the ADR-0009 provenance machinery for no runtime gain. The metadata was added deliberately and stays.
|
||
|
||
## Alternatives considered
|
||
|
||
Upstream citations below are relative to
|
||
`plugins/kyberforge/docs/research/examples/skill-write/`, as in Context above.
|
||
|
||
- **Keep pushiness, raise the budget to ~500 chars.** Undertriggering is the worse failure mode — a
|
||
skill that never fires is worth nothing regardless of cost — and `skill-creator/SKILL.md:67`
|
||
explicitly recommends being "pushy" against an observed undertriggering tendency. Rejected because
|
||
that claim is an unmeasured assertion about an older model, and because the correctness hazard in
|
||
`writing-skills/SKILL.md:154-158` cuts the other way: a fat description is not merely expensive, it is a
|
||
shortcut agents take instead of reading the body. Would have landed a 35% cut.
|
||
- **A trigger-eval loop to set lengths empirically.** `skill-creator/SKILL.md:337-404` specifies 20
|
||
queries per skill, 8-10 positive and 8-10 near-miss, with a 60/40 train/test split selecting on
|
||
test score. This is the rigorous answer and the repo has deliberately never built it. Rejected
|
||
because it blocks the context cut behind a substantial new subsystem.
|
||
- **A repo-level aggregate preload budget** (≤12,000 chars across all skills, checked at pre-push).
|
||
The only option that measures the actual goal rather than a proxy. Rejected because it makes one
|
||
skill's edit fail on account of another skill's growth, and because it is meaningless for an
|
||
external consumer installing a subset of the plugins.
|
||
- **500-word body FAIL, matching `writing-skills/SKILL.md:217-221`.** Best-grounded in upstream and
|
||
would align this repo with the tightest source. Rejected because it fails 28 of 39 skills body-only
|
||
(35 of 39 measured whole-file), and a blunt gate gets satisfied by deleting content rather than
|
||
relocating it.
|
||
- **A shrinking baseline file** recording each non-compliant skill's current numbers, failing only on
|
||
growth. Would have made the retrofit a visible burn-down instead of a wall. Rejected in favour of
|
||
hot gates.
|
||
- **A sync gate over the duplicated spans** instead of a merge rule — generalising
|
||
`scripts/check-vale-style-sync.sh` (live when this was written; ADR-0025 deleted it along with the
|
||
second copy it diffed) to cover shared prose so duplication persists but drift cannot.
|
||
Rejected for the audit pair in favour of merging, which removes the duplication rather than
|
||
policing it, and removes a mutually-excluding near-miss pair from the router at the same time. It
|
||
remains the only available answer for the author pair.
|
||
- **Merging `skill-author` + `agent-author` as well**, taking kyberforge from seven skills to five.
|
||
Largest cut available. Rejected because it reopens ADR-0005, ADR-0008 and ADR-0016 together, and a
|
||
merged author skill would carry both the skill-directory scaffold and the dual-provider agent
|
||
scaffold behind one dispatch.
|
||
**Measured (2026-09-16):** the pair shares about 150–180 distinct non-blank lines — 36 in
|
||
`contract.md` (of 205 / 126), 13 in `SKILL.md`, 14 in `improve.md`, 48 in the two scaffold
|
||
scripts — against the 2,934 that the audit-pair merge removed. The rejection holds; simplification
|
||
audit finding 15 is refuted on this basis.
|
||
- **Demoting Gotchas** to the end of the body or into `references/gotchas.md`, removing its
|
||
position-based exemption from the dispatch rule. Maximum saving on the largest body construct
|
||
(6,830 words, 21% of all body text). Rejected because a gotcha read after the mistake is worthless.
|