refactor(skills): retrofit the corpus to the ADR-0020 context contract (#129)

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>
This commit was merged in pull request #129.
This commit is contained in:
Claude Code AI - Gitea MCP
2026-09-01 13:47:46 +00:00
committed by Defame1297
parent 0e91a3ae66
commit 598a7c326a
420 changed files with 15303 additions and 4740 deletions

View File

@@ -127,8 +127,15 @@ clause**, and a **boundary clause**. Capability enumeration, output-format detai
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, now produces findings identical to the working tree —
26 description FAILs, 9 body FAILs, 2 dangling targets, 0 missing references, 58 SUGGESTIONs.
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
@@ -185,8 +192,11 @@ becomes the system prompt of a fresh context. The rationale for the 900-word FAI
That exemption is expressed in `agent-audit/scripts/validate.sh`, 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` today reports 900-word body FAILs on
`git-orchestrate` (933), `gitea-orchestrate` (1,199) and `apm-orchestrate` (1,080). Agents escape by
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.
@@ -203,8 +213,14 @@ 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 both `.claude/skills/zoom-out/SKILL.md:4` and the flat mirror
at `plugins/bin/skills/zoom-out/SKILL.md:4`, and `zoom-out` is the one installed skill absent from
the model-visible skill listing in a live session. It remains invocable as `/zoom-out`.
at `plugins/bin/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`. `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
@@ -219,10 +235,13 @@ rather than the core job.
skills still exist 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. The merge stays 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 is its own change and not a rider on
this one. Recorded here rather than dropped, so the gap between the rule and the tree is deliberate
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/<name>/references/ | grep -c '\.md$'`. The merge stays 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 is its own change and not a rider
on this one. Recorded here rather than dropped, so the gap between the rule and the tree is deliberate
and dated instead of discovered later.
### Enforcement and rollout
@@ -233,11 +252,13 @@ which tier each rule is in, because the failure this ADR is most exposed to is a
| 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` |
| 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` |
| body-only words (600 SUGGESTION / 900 FAIL) | skills | deterministic | `skill-size-check.sh`, `skill-audit/scripts/validate.sh` |
| description present and non-empty (ERROR) | skills, agents | deterministic | same |
| boundary target resolves to a real skill or agent (ERROR when written as `/name` or `-> name`, or when its own sentence names another target that resolves; SUGGESTION otherwise) | skills, agents | deterministic | same |
| boundary clause absent (SUGGESTION) | 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 |
@@ -254,6 +275,21 @@ that guessed at them would be a worse gate than no gate, because it would be bel
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`,
@@ -264,7 +300,8 @@ raise an error: one followed by an ordinary lowercase noun is a compound **modif
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. The
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
@@ -296,6 +333,46 @@ Three pre-existing contradictions are fixed in the same change, because they are
- `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.
## Consequences
**Editing any non-compliant skill now requires retrofitting it first.** At decision time, 30 of 39
@@ -311,10 +388,17 @@ carries is the ordinary one for hot gates: a gate expensive enough to be inconve
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 `pre-commit run --all-files` is 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 will fix those and still be blocked. The two gates want fixing together.
`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/skill-audit/scripts/vale-wrap.sh plugins/*/.apm/skills/*/SKILL.md`.
**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
@@ -342,8 +426,8 @@ and `git-*` families — where every sibling shares a keyword and boundary claus
— 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 are fixed here and two are live.** Tracked as issue
#100.
**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
@@ -353,14 +437,24 @@ routing degrades, the tier is the first thing to revisit.
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. **Live.**
- `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.
**Live** — the check reports it as a dangling `gitea-labels`.
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 fires on 3 of the 4 against the base commit and on 2 at the tip of this change, and
`tests/test-skill-size-check.sh` probes exactly those three by name rather than asserting a count, so
it degrades to SKIP as #100 lands rather than going stale.
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