Retrofits all 39 skills to ADR-0020's description/body context contract, then fixes what six rounds of independent review found in that retrofit — including four ways the hot gate itself failed open. Closes #99, #107, #108, #110, #111, #114, #115, #120. ## The retrofit (waves 1-5) | | Start | Now | |---|---|---| | Description FAILs (>400 chars) | 26 | **0** | | Body FAILs (>900 words, body-only) | 9 | **0** | | Dangling routing targets | 2 | **0** | | `Kyberforge.CompositionNote` | 10 | **0** | | Preload tax | 21,005 chars | **~10,500** | Under the 12,000-char success criterion. Per-wave detail is on #99. ## The review fixes **The gate failed open four ways, three of them found after the retrofit shipped.** An unrecognised follower token made a dangling target vanish. A skill directory with no `SKILL.md` resolved as a valid target, so a commit could be green locally and red in a fresh clone — three existing fixtures were relying on that, one of which made the install-leak A/B pass vacuously. Then the free-standing `/name` sweep turned out to be gated on the sentence carrying a boundary marker, so route notation in any other sentence was invisible — not an ERROR, not a SUGGESTION, not an INFO — which left the documented "`/name` always blocks" promise false from a second direction. All four fixed and pinned. **Two checks were silently not running.** `validate-provenance.sh` checks 7-8 were dead across nine skills. Waking them exposed a deeper problem: they assume `Research doc:` names a source index, but 30 of 121 entries point at topic content documents, so every new check-7 INFO was a false positive and check 8 was saved from a false-FAIL flood only by an *unannounced* skip. Checks 7/8 are now scoped to source indexes and every skip announces itself (#121). **The retrofit's own anti-goal, four times.** ADR-0020 warns that a blunt gate gets satisfied by deleting content rather than relocating it. `diagnose` and `skill-audit` relocated prose and then read it unconditionally; `prototype` and `vale-config` deleted rules outright that survived nowhere. All four addressed. ## Verification - `bash tests/run-tests.sh --strict` — 24 suites, 0 skipped, 0 failed - `bash tests/run-bats.sh` — 325 tests, 0 failures - `pre-commit run --all-files` — 17/17 - `pre-commit run --hook-stage pre-push --all-files` — 16/16, with `apm marketplace check` and `apm pack --check-clean` run against the remote, not skipped - `scripts/skill-size-check.sh` over all 39 skills — rc 0, 0 ERROR/FAIL, SUGGESTION-only - Preload tax measured at **10,498 chars**, max description 390 — both inside budget - Every new test proven non-vacuous by a deliberate mutation of the behaviour it covers **Per-commit sync, stated accurately:** the ten commits from the latest review round each pass `check-plugin-content-sync` in isolation, verified by checking each out in a detached worktree with a clean between. The earlier gitea window (`dfacf05..bedbd1d`, nine commits) does **not** — its mirror was regenerated in one batch at `bbc7300`. An earlier revision of this description claimed the property held for every commit; it does not, and a bisect through that window lands on a red commit. **Squash-merge** to collapse it, or accept that this range is not bisectable. ## Version bump Six plugins and the catalog take a **patch**, not a minor. The branch is **89 commits — 40 `fix` / 30 `refactor` / 12 `docs` / 5 `chore` / 2 `test` — zero `feat`, zero `!`, zero `BREAKING CHANGE`** — and adds no skill, agent, command or hook. (Two earlier revisions of this section cited a stale histogram, most recently 78 commits; the figures above are measured at HEAD.) Both rules this repo ships (`forge/references/version-bump.md`, landing in this PR, and `git-commits/references/conventional-commits-spec.md`) make that a patch, and the catalog set is unchanged at 7 entries. Not settled by that: four published files were removed from the installed tree, three moved, and `caveman` gained `disable-model-invocation`, retiring its old triggers. Under a strict reading those are major-class and currently ship under `refactor:` with no marker. Whether the deployed skill surface is a public contract is written down nowhere — worth deciding, but it outlives this PR. ## Deliberately not in scope #112 (cherry-pick ownership, now resolved in favour of `git-commits`), #113 (`rtk git` normalisation), #116 (research fan-out), #101 (audit-skill merge), #122 (non-spec skill-root files), #123 (no PRD producer) stay open. #117 is the one worth reading: the contract's remedy is to move prose into `references/`, which is exactly where neither the size gate nor Vale looks — and the blind spot is wider than #117 currently records, since there is no root `.vale.ini` at all, so every ADR, `CONTEXT.md` and `README.md` is unlinted too. That blind spot let this branch carry two `level: error` `Kyberforge.SentenceOpenerThereIs` violations into `references/` files it created — `provider-adapter-author/references/provider-matrix.md:31` and `agent-audit/references/finding-criteria.md:95`. Both are reworded in `afadaae`, confirmed by routing each file through the audit's own `vale-wrap.sh` (1 error each before, 0 after). Five further occurrences sit in `references/` files already on `main`; those are the pre-existing corpus and stay with #117, which is the real fix. Also unfixed and not this PR's: `apm install` appends a duplicate `SessionStart` entry to `.claude/settings.json`, so a fresh clone cannot get pre-push green without an edit AGENTS.md warns against. Reproduces identically on `main`. Co-authored-by: Defame1297 <gitea@rkdr.net> Reviewed-on: https://git.dev.rkdr.net/Defame1297/holocron/pulls/129 Co-authored-by: Claude Code AI - Gitea MCP <claude@noreply.git.dev.rkdr.net> Co-committed-by: Claude Code AI - Gitea MCP <claude@noreply.git.dev.rkdr.net>
11 KiB
source_keys
| source_keys | ||
|---|---|---|
|
Retrofitting a skill to the ADR-0020 contract
Read this when references/improve.md Step 4 sends you here: the skill you are editing is over
the description or body budget and has to come into contract before any other change can be
committed. The gates are hot and carry no baseline file, so a one-line fix to a non-compliant
skill is blocked until this is done.
Measure first. Do not guess which gate fired: run /skill-audit on the directory and read its
### Structure dimension, which reports the description characters and the body-only word
count separately from the whole-file spec backstop. Retrofit against the number that actually
fired — a skill can sit a thousand words inside the whole-file backstop while failing the body
budget.
Validate in place. Audit the skill's real directory inside its package. Never audit a copy in a scratch directory, and never move a skill out to work on it: the boundary-target universe is built by walking up from the file being checked, so a copy with no authoring root above it resolves against nothing and the check declines rather than running —
INFO boundary-target resolution DID NOT RUN — no skill universe could be determined for
this path ... Unchecked target(s): totally-fake-target
The run still exits 0, so that line reads as a pass and is not one. Treat DID NOT RUN as not
checked, always. A retrofit signed off on a scratch copy carries an unverified boundary target
into the corpus, which is precisely the failure this gate exists to catch.
Cut in this order
Work the list top down and stop as soon as the gate clears. The order is by ratio of tokens removed to behaviour lost — inverting it is how a retrofit ends up deleting the one instruction the skill existed to carry.
- Gotchas that paraphrase a step in the body below. Zero information, and already a FAIL on its own. Delete the Gotcha, keep the step.
- Spec restatements — text that repeats a published specification, a tool's
--help, or a ceiling the validator already enforces. The agent gets this right without it. Delete, or move the table toreferences/if a flow genuinely needs to look it up. - Capability enumeration — in a description, the feature list after the trigger clause; in a
body, the paragraph that recites what the skill can do. One capability clause survives in the
description; the rest belongs in
README.md. - Per-flow prose — anything only one branch of the procedure ever reaches. This is the
largest single win in most bodies, and it is a move, not a delete: each flow gets its own
self-contained
references/file, wired from a dispatch table.
If the body is still over after all four, the skill is doing two jobs. Split it, and say so rather than compressing prose until it stops being readable.
What "mutually exclusive flows" means
Two or more flows that a single invocation cannot both take. The three-way test, copied verbatim
from the body-discipline rubric /skill-audit judges against — nothing to load, it is quoted in
full here:
separate subcommands, separate input types, separate lifecycle stages
Any one of the three is enough. Two flows that differ only in a parameter value are one flow. At two or more mutually exclusive flows a dispatch table is mandatory regardless of word count, because every invocation otherwise pays for every branch it did not take.
Reference-file conventions
The create flow owns these rules, and this flow is forbidden from reading references/create.md,
so what a retrofit needs is restated here:
- One topic per file. A file mixing two concerns gets loaded for one of them and spends the caller's context on the other.
- Kebab-case filenames, named after the topic rather than the flow that reads it —
body-discipline.md, notstep-3.md. - Wire every file with the literal conditional form
If <condition>, read `references/<file>.md`. A generic pointer ("seereferences/for details") is a Vale error. - Two hops from
SKILL.md, never three. A flow file may route on to a shared contract file; a file reachable only through two intermediates is rarely loaded when it is needed. source_keysfrontmatter. If the content you are moving drew on a research source, the new file needs top-levelsource_keys:frontmatter listing those slugs, and every slug must already exist as an## <slug>heading inreferences/sources.md. Moving sourced content out ofSKILL.mdwithout carrying its slugs across breaks the provenance chain, and/skill-auditreports the new file as an INFO with nosource_keys.
Collateral is mandatory, not optional
Moving content out of a SKILL.md leaves three files describing a structure that no longer
exists. /skill-audit's provenance check exits clean on all three of these, so nothing catches
them for you. After every retrofit that adds, removes or renames a file:
README.mdfile table — a row for every newreferences/file, and no row left for a file that is gone. Say what triggers the load, not just what the file contains.references/README.md, where the skill has one — same update, same reason.references/sources.md→Contributing files— add the new file to every slug whose content moved into it, and remove any file the retrofit deleted. This is the one that gets missed:sources.mdkeeps citing sections ofSKILL.mdthat no longer exist, the provenance check still exits 0, and the stale claim survives review.- Reachability of every relocated gate. For each Gotcha or gate the retrofit moved out of
the body, list the flows that need it and confirm each one reaches the surviving copy. A gate
that lands in a single flow file is invisible to every other branch, and no gate detects
that:
/skill-auditreads whichever file it was handed, and the word counts improve either way. Where more than one flow needs it, the copy belongs in the body's common-gates section, not in a flow file. Grep the skill for the gate's key term and check every branch that hits zero. - Re-run
/skill-auditand confirm its### Provenancedimension does not report the new file as missingsource_keys.
Worked example — a description retrofit
gitea-issues before, 827 characters, the single most common shape in the corpus:
Use when reading or writing Gitea issues: listing repo issues, getting a single issue's details/
comments/labels, creating an issue, updating its state, adding or editing comments, applying
labels via issue_write, or searching issues/PRs across repositories. Triggers on "create an
issue", "what issues are open", "get issue #N", "close issue #N", "comment on issue #N", "search
issues for X" — even when the user doesn't say "Gitea" explicitly. Composes gitea-labels-
milestones for all label inference/resolution and milestone lookup — do not use this skill to
manage label or milestone definitions themselves (create/edit/delete a label, create/close a
milestone), that's gitea-labels-milestones directly. Do not use for pull requests (use gitea-prs)
or for local git branch/commit work (use gitea-branches or git-branches).
After, the 290 characters that shipped:
Use when reading or writing Gitea issues — "create an issue", "what issues are open", "close
issue #N", "comment on issue #N", "search issues for X" — even when the user does not say
"Gitea". Not pull requests -> `gitea-prs`. Not label or milestone definitions ->
`gitea-labels-milestones`.
The retrofit kept the quoted-phrasing register and dropped the verb list, not the other way round. Either register is admissible — what is banned is carrying both. Choose whichever routes better for the skill in hand; here the quoted user phrasings do, because they are how people actually ask.
What came out, and why:
| Removed | Why |
|---|---|
The second trigger register — Triggers on "create an issue", "what issues are open", … |
The same triggers restated as quoted user phrasings. Two registers of one trigger list is a FAIL, not a suggestion. |
applying labels via issue_write |
Implementation detail. The router does not choose a skill by which MCP call it makes. |
Composes gitea-labels-milestones for all label inference/resolution and milestone lookup |
A composition note. It changes no routing decision and belongs in README.md. |
The parenthetical (create/edit/delete a label, create/close a milestone) |
Capability enumeration inside a boundary clause. The boundary needs the target, not its feature list. |
The gitea-branches / git-branches boundary |
Dropped entirely. Neither was ever going to win an issue request, so the clause defended against nothing — an invented boundary costs characters and buys no routing accuracy. |
Do not use for pull requests (use gitea-prs) prose form |
Kept, but rewritten as Not pull requests -> \gitea-prs`.` The rewrite buys characters, one uniform shape for the router, and a stricter check: an unresolved arrow target is a blocking ERROR, while an unresolved prose target is only a SUGGESTION unless another target in the same sentence resolves. The prose form does not dangle as loudly. |
What stayed: one trigger clause, one capability clause, the indirect trigger (genuinely warranted here — people say "create an issue", not "create a Gitea issue"), and the boundary clauses.
Two rules the gates enforce but the prose does not spell out
Boundary clauses may be plural. Write one per genuine near-miss — the example above carries two, because two different skills could each steal activations. "A boundary clause" in the contract means at least one, not exactly one. What is banned is a boundary clause invented for a skill that was never going to compete, not a second real one.
Never let a hyphenated routing target wrap across lines in a folded > scalar. YAML folding
replaces the newline with a space, so gitea-labels- at the end of one line and milestones at
the start of the next fold into gitea-labels- milestones. The gate then reads the target as
gitea-labels, finds no such skill, and reports a dangling boundary target. This is not
hypothetical — it is how gitea-labels-milestones broke (issue #100). It is fixed: the corpus
carries no dangling target today, and the repo's test suite pins that set as empty, so a
reintroduction fails the suite rather than joining a backlog. Reflow the line so the whole name
sits on one of them. The same applies to any backticked skill or agent name in a description.