Seven ADRs described code that no longer exists or behaviour the gates do
not have. Where the wrong text came from main it carries a dated
correction; where this branch introduced it, it is fixed in place, because
main never published it and there is no record to preserve.
Fixed in place, branch-introduced:
- ADR-0021's 2026-09-14 correction asserted apm audit --ci "was never a
drift gate at all". It is one: it replays the install and diffs. The
claim contradicted this branch's own AGENTS.md and gates.md.
- ADR-0015 said unconditionally that no pre-push hook needs the network.
The guarantee holds only once apm install has populated apm_modules/.
- ADR-0014's 2026-09-16 correction said restoring .pre-commit-hooks.yaml
would ship a hook that fails for every consumer, because their checkout
has no lib-boundary-resolver.sh. pre-commit clones the whole hook repo
and skill-size-check.sh resolves the library from BASH_SOURCE, so the
hook would work.
- ADR-0019's "twelve hooks pass under unshare -rn" matched neither HEAD
(8) nor main (14), and stated the offline guarantee unconditionally.
Corrected, inherited from main:
- ADR-0022 and ADR-0013 named skill-frontmatter's pre-commit hook as the
enforcer of mandatory metadata.version. That hook was deleted on this
branch; the check lives in skill-size-check.sh.
- ADR-0022 enumerated the tip rule's carve-outs as a closed list and
described a single merge-base. The gate also exempts a tree-identical
skill and intersects every base from merge-base --all, and emits a third
failure form. 8cfd54f said the documented behaviour did not change; it
did. The gate is correct and is unchanged -- the record was not.
- ADR-0020's Decision still routed description overflow to README.md, its
ADR-0025 amendment pointed the mirrored constants at validate.sh, which
holds none, and its Enforcement table still named the two deleted
validate.sh paths.
- ADR-0015's Status claimed every plugin's plugin.json is pack output;
none exist.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NwD8Egs5r4ndqeFLmhusX2
45 KiB
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
nulldescription:is a hard FAIL in all three validators. That reads as a trivial precondition and is not: adescription:line with no value followed bymodel: sonnetlet 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 holdingplugins/*/.apm/skillsorplugins/*/.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 ownapm.ymldependencies.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.gitancestor 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 toskill-auditresolved 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 installhas 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 onlyplugins/and the rootapm.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,/clearand/initare 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:104anddescription-quality.md:21are replaced by a conditional: add an indirect trigger only where the user's natural phrasing genuinely omits the domain word — true for thegitea-*family, false forgit-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 isapm-workflow/SKILL.md:33-41promoted 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>.mda 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:58asks whether the description opens with an action verb ("Audits…", "Reviews…"), while:56defers the same question toKyberforge.DescriptionOpenerandskill-author/SKILL.md:101requires an imperative "Use when…" opener. The criterion is unsatisfiable against the house's own skills, both of which open with "Use when".DescriptionOpener.ymlis anchored to^This (skill|agent)\b, which misses a plainThis …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: underscope: text.frontmatter.descriptionthe^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 fourgitea-*skills, 0 false positives.description-quality.md:45-50has no FAIL condition for internal-mechanics content, which is whyskill-author/SKILL.md:102never 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
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-auditrouted to/skill-improvetwice in its description plusREADME.md:10, and no such skill exists — the real target isskill-author. Fixed here, as a side effect of retrofitting kyberforge's own skills.agent-authorsaid "Do not use for read-only review — examine agent files manually", routing away fromagent-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.researchroutes toneuledge-context, which exists only inside that string. Was live; fixed under #99 — the retrofitted description names no such target.gitea-issuescarries the literal stringgitea-labels- milestonesin its folded description, a stray space introduced by YAML wrapping mid-token, breaking the skill name in preloaded text. Was live, reported as a danglinggitea-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:67explicitly 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 inwriting-skills/SKILL.md:154-158cuts 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-404specifies 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-authoras 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 incontract.md(of 205 / 126), 13 inSKILL.md, 14 inimprove.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.