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:
@@ -98,11 +98,57 @@ loudly (`Error: jq is required but not installed`).
|
||||
## Skill and agent context gates (ADR-0020)
|
||||
|
||||
The `skill-size-check` pre-commit hook, scoped to `^plugins/[^/]+/\.apm/skills/[^/]+/SKILL\.md$`,
|
||||
runs `scripts/skill-size-check.sh`. That scope means it never lints the
|
||||
`plugins/kyberforge/docs/research/examples/` reference skills. It is also shipped to external repos
|
||||
as `kyberforge-skill-size-check` (see
|
||||
runs `scripts/skill-size-check.sh`. It is also shipped to external repos as
|
||||
`kyberforge-skill-size-check` (see
|
||||
[External consumers](#external-consumers-the-root-pre-commit-hooksyaml)).
|
||||
|
||||
**Two things fall outside that scope, both deliberately.** The `[^/]+/SKILL\.md$` tail admits only a
|
||||
`SKILL.md` sitting directly in a skill directory under `.apm/skills/`:
|
||||
|
||||
- the `plugins/kyberforge/docs/research/examples/` reference skills, which are vendored upstream
|
||||
corpus and not this repo's to gate;
|
||||
- `plugins/kyberforge/.apm/skills/skill-author/assets/templates/SKILL.md` — inside `.apm/skills/`,
|
||||
but two directories deeper. It is the `FILL IN:` scaffold `skill-author` copies, so its
|
||||
`description: >` is a comment block rather than a description and every ADR-0020 measurement over
|
||||
it would be meaningless. A reader adjusting the pattern needs to know it is there.
|
||||
|
||||
Everything else it matches exactly, with nothing over- or under-caught. Re-derive both halves:
|
||||
|
||||
```
|
||||
git ls-files | grep -cE '^plugins/[^/]+/\.apm/skills/[^/]+/SKILL\.md$' # the real skills
|
||||
git ls-files | grep -E '^plugins/[^/]+/\.apm/skills/.*SKILL\.md$' \
|
||||
| grep -vE '^plugins/[^/]+/\.apm/skills/[^/]+/SKILL\.md$' # the scaffold only
|
||||
```
|
||||
|
||||
The first count equals the number of skill directories (`ls -d plugins/*/.apm/skills/*/ | wc -l`);
|
||||
the second returns exactly the template. The remaining unmatched `SKILL.md` files in the tree are the
|
||||
generated flat mirror, which is excluded by the `.apm/` segment on purpose — a mirror edit is drift,
|
||||
not an authoring change.
|
||||
|
||||
### `skill-frontmatter`, the other hook on that scope
|
||||
|
||||
A second `repo: local` pre-commit hook, `skill-frontmatter`, runs on the **same** `files:` pattern at
|
||||
the same stage. It is a short shell loop: for each file, `grep -q "^name:"` and
|
||||
`grep -q "^description:"`, failing with "missing required frontmatter fields" if either is absent.
|
||||
|
||||
**It overlaps ADR-0020's "description present and non-empty" FAIL, and the overlap is not clean.**
|
||||
The ADR (`:95-101`) requires that question be decided on the **YAML-folded value** and nowhere else,
|
||||
precisely because a line regex gets it wrong in both directions. Measured on fixtures:
|
||||
|
||||
| Frontmatter | `skill-frontmatter` | `skill-size-check` |
|
||||
|---|---|---|
|
||||
| `description:` with no value, then `model: sonnet` | passes — the key is on a line | ERROR, "missing or empty" |
|
||||
| `"description": …` (quoted key, valid YAML) | **fails** — `^description:` does not match | passes, description read normally |
|
||||
|
||||
So the grep is not a second opinion on presence. It is blind to the shape ADR-0020 was written
|
||||
against, and it is the only one of the two that objects to a quoted key. Neither disagreement is
|
||||
currently live in the corpus, and the honest reading is that presence is `skill-size-check`'s
|
||||
question — the grep's contribution to it is noise on one shape and silence on the other.
|
||||
|
||||
What the grep does add is the `name:` key, which **no** ADR-0020 check reads: a `SKILL.md` with no
|
||||
`name:` passes `skill-size-check` at exit 0. That is its real and only unique coverage, and the
|
||||
reason not to fold it into the size gate on the grounds of redundancy.
|
||||
|
||||
### Two independent gate families, neither replaced the other
|
||||
|
||||
**Family 1 — agentskills.io spec backstop** (unchanged, conformance not quality):
|
||||
@@ -141,7 +187,7 @@ A boundary-clause target that resolves to no skill or agent has **three** possib
|
||||
| Verdict | When |
|
||||
|---|---|
|
||||
| **SUGGESTION** — the default | the target does not resolve and neither promotion condition below holds |
|
||||
| **blocking ERROR** | the target is **terminal** (not a compound modifier) **and** either written in route notation (`/name` for any name; `-> name` only when the name is hyphenated — see the gap below) **or** corroborated by another target in the same sentence that *does* resolve |
|
||||
| **blocking ERROR** | the target is written in **route notation** — `/name` for any name, or any arrow form (a bare `-> name` only when the name is hyphenated, a backticked `` -> `name` `` for any — see the gap below); **or** it is a bare **terminal** name (not a compound modifier) **corroborated** by another target in the same sentence that *does* resolve |
|
||||
| **INFO, "DID NOT RUN"** | no skill universe could be determined for the path at all — the targets are named and left unchecked, exit 0 |
|
||||
|
||||
The default is deliberately soft because a hyphenated word in a boundary clause is as likely to be a
|
||||
@@ -149,14 +195,36 @@ tool, a file format or an English compound as a route: "pre-commit hooks" is pro
|
||||
never reaches the check at all, being a compound modifier rather than a terminal name. The
|
||||
SUGGESTION text says how to opt in — write it as `/name` or `-> name` and it gets checked properly.
|
||||
|
||||
**Known gap: the arrow form only works for hyphenated names.** Target extraction is built on
|
||||
`NAME_HYPH` (`scripts/skill-size-check.sh:543`), which requires at least one hyphen, and
|
||||
`ARROW_BOUNDARY` (`:561`) inherits that. So `-> gitea-prs` is extracted and checked, while
|
||||
`-> triage` is not extracted at all — no ERROR, no SUGGESTION, exit 0. The unicode arrow `→` is not
|
||||
recognised in either case. This makes the SUGGESTION's own advice unsafe for a single-word skill:
|
||||
taking it silences the finding rather than checking it. `/name` has no such restriction and is the
|
||||
form to prefer. Tracked as a defect; `tests/test-adr0020-targets.sh` has one arrow case and its
|
||||
target happens to be hyphenated, so nothing currently covers this.
|
||||
**The two promotion conditions are not symmetric, and the order matters.** `_add()` decides
|
||||
**notation first**: when the name is written `/name`, or reached through any arrow form, the target
|
||||
is marked error-eligible there and the terminal test is never run. Terminality gates only the *bare*
|
||||
path — a name in prose earns its error from corroboration, and a compound modifier can never dangle.
|
||||
Reading the row as "terminal AND (notation OR corroborated)" gets the notation half backwards: it
|
||||
predicts that `` … Do not use for Y — use /no-such-skill afterwards. `` is a SUGGESTION, because
|
||||
`afterwards` is a follower outside `FOLLOWER_OK`. It exits 1. That was the defect — `-> name` reached
|
||||
`_add()` with `strict=True` from both its call sites and `/name` did not, so the one spelling
|
||||
ADR-0020 offers an author who wants a route checked unconditionally was the one spelling a stray
|
||||
follower could silence.
|
||||
|
||||
**Known gap: a BARE arrow target must be hyphenated.** Target extraction is built on `NAME_HYPH` in
|
||||
`scripts/skill-size-check.sh`, which requires at least one hyphen, and `ARROW_BOUNDARY` inherits
|
||||
that. So `Not X -> gitea-prs` is extracted and checked, while `Not X -> triage` yields no target.
|
||||
The exclusion is deliberate, not an oversight: `research`, `triage`, `forge`, `prototype` and `tdd`
|
||||
are all real skill names *and* ordinary English, so a bare single-word rule would flag most of the
|
||||
corpus. The marked spellings carry no such restriction — `` `triage` `` and `/triage` are both
|
||||
extracted — and are the forms to prefer. **Both arrow spellings are recognised:** `ARROW_MARKED`,
|
||||
`ARROW_BOUNDARY` and `BOUNDARY_ARROW` are each built from `(?:->|→)`, so the unicode arrow `→`
|
||||
behaves exactly like `->` in every case below. Cite these constants by symbol name, never by line
|
||||
number: the script moves often enough that a pinned line lands a reader in an unrelated comment
|
||||
block and reads as plausible.
|
||||
|
||||
**The gap is no longer silent.** It used to be exactly that — no ERROR, no SUGGESTION, exit 0 — which
|
||||
made the dangling-target SUGGESTION's own advice unsafe for a single-word skill: taking it silenced
|
||||
the finding instead of checking it. `boundary_clause_status()` now separates the case out and
|
||||
reports it as `unparsed` (see below), naming the parse failure and the two spellings that fix it.
|
||||
The target is still not *resolved*; the author is now told so rather than left with a green gate.
|
||||
`tests/test-adr0020-targets.sh` covers both directions (`arrow-single-word-target` and the silent
|
||||
control `arrow-single-word-marked`).
|
||||
|
||||
Corroboration is what makes the soft default safe: a sentence whose *other* target resolves is
|
||||
demonstrably a routing sentence, so a sibling that does not resolve is a typo rather than a noun, and
|
||||
@@ -200,17 +268,109 @@ through `.claude/skills/` alone, so **the same commit measured 2 dangling target
|
||||
machine and 6 on a fresh clone**. A gate shipping hot with no baseline cannot give two answers.
|
||||
|
||||
Verified fixed: running the hook over a tree holding only `plugins/` and the root `apm.yml`, with no
|
||||
`.claude/` or `.agents/` anywhere, produces findings identical to the working tree — **26 description
|
||||
FAILs, 9 body FAILs, 2 dangling targets, 0 missing references, 58 SUGGESTIONs**.
|
||||
`.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
|
||||
— are the pre-retrofit corpus as it stood when the experiment was run, kept here as the evidence for
|
||||
the install-independence claim. They are not current: the retrofit under #99 took the first three to
|
||||
zero. What the experiment establishes is that the two trees agree, not what either measured.
|
||||
|
||||
### Boundary-clause detection: three outcomes, not two
|
||||
|
||||
`boundary_clause_status()` returns one of three values, and the two findings get separate messages:
|
||||
|
||||
| Status | When | Reported as |
|
||||
|---|---|---|
|
||||
| `present` | a prose marker (`do not`, `instead`, `rather than`, `not for`) or an arrow clause was found | nothing |
|
||||
| `absent` | neither was found | SUGGESTION: add a boundary clause, in either form |
|
||||
| `unparsed` | an arrow clause was found and **no target could be read out of it** | SUGGESTION: the clause is present — this is a *parse* failure, not a missing clause |
|
||||
|
||||
The third had to be split out. Collapsing it into `absent` is a **wrong** finding, not a strict one:
|
||||
it sends the author to add a clause that is already there. Three of them instead reworded a correct
|
||||
clause until the regex accepted it, one stripping the very filename that discriminates the skill
|
||||
from its neighbour (**#110**).
|
||||
|
||||
`unparsed` is narrow and certain on purpose. It fires only on the arrow form, which *always* names a
|
||||
target, so zero targets means the name is written in a shape the extractor cannot see — in practice
|
||||
a bare single-word target, per the known gap above, and the message says to write it `` `name` `` or
|
||||
`/name`. A **prose** clause yielding no target is not reported at all: "Do not use for anything else"
|
||||
is a complete and legitimate boundary clause that names nowhere to go.
|
||||
|
||||
**One arrow, one target.** An arrow clause naming two or more targets draws its own SUGGESTION,
|
||||
quoting both names and asking for a split, because only the first is ever resolved: the conjunction
|
||||
continuation (`CONT_MARKED` / `CONT_ANY`) is wired to the prose route verbs and never to arrows. So
|
||||
`Not X -> a or b` resolved `a`, left `b` resolved by nothing and reported by nothing, and then let
|
||||
the audit print "1 of 1 boundary target(s) resolve" on a clause naming two — a gate under-reporting
|
||||
its own coverage, which is the one failure mode ADR-0020 says a gate must not have (**#107**). The
|
||||
clause is **rejected rather than the arrow scan extended**: extending it would widen the resolver's
|
||||
deliberately conservative false-positive tuning across every arrow in the corpus, where splitting
|
||||
costs the author one full stop. The convention is one arrow per target — `Not X -> a. Not Y -> b.` —
|
||||
already what every retrofitted `gitea-*` skill does in practice, now stated in
|
||||
`skill-author`'s `references/contract.md` instead of being folklore.
|
||||
|
||||
**Dotted filenames in a boundary clause now parse.** `CLAUSE_BODY` — what may sit between `Not` and
|
||||
the arrow — used to be `[^.;]`, a class that cannot cross a `.`, so every clause naming a dotted
|
||||
filename between the two (`AGENTS.md`, `.vale.ini`, `.pre-commit-config.yaml`) was invisible to both
|
||||
`BOUNDARY_ARROW` and `ARROW_BOUNDARY`. The two resulting failures were different sizes (**#110**):
|
||||
|
||||
- with a **backticked** target the clause was *misdiagnosed*. The backtick sweep still extracted the
|
||||
target, so the route was checked, but the gate reported "no boundary clause" on a clause that was
|
||||
present and working. That is the misdiagnosis the three rewordings above came from.
|
||||
- with a **bare** target the clause was *unchecked*. `ARROW_BOUNDARY` is the only extractor for a
|
||||
bare arrow target, so `Not AGENTS.md -> no-such-skill` produced no target, no dangling report and
|
||||
no missing-clause SUGGESTION. Silence, not noise — the worse of the two.
|
||||
|
||||
`CLAUSE_BODY` is now `(?:[^.;]|\.(?=\S))`: a dot inside a filename is followed by a non-space, a
|
||||
sentence-ending dot by whitespace or end of string, so the class crosses `AGENTS.md` and still stops
|
||||
at a real sentence end. **Read the second bullet forward as well as back:** a bare target sitting
|
||||
after a dotted filename is now extracted, resolved, and a blocking ERROR when it dangles, where the
|
||||
same clause used to pass unchecked in silence.
|
||||
|
||||
### SUGGESTION-only checks
|
||||
|
||||
Three more, deterministic to measure but judgment to act on:
|
||||
Deterministic to measure, judgment to act on:
|
||||
|
||||
- a description with **no boundary clause at all**;
|
||||
- a description with **no boundary clause at all** (`absent`);
|
||||
- an **arrow clause whose target could not be read** (`unparsed`);
|
||||
- an **arrow clause naming more than one target**;
|
||||
- a `## Gotchas` section with **more than five entries**;
|
||||
- a `## Gotchas` section over **25% of the body**.
|
||||
|
||||
### Hand-invoked skills are exempt from the routing rules, and only those
|
||||
|
||||
A skill or agent whose frontmatter carries `disable-model-invocation: true` skips three checks:
|
||||
|
||||
- the boundary-clause check, `absent` and `unparsed` alike;
|
||||
- the multi-target arrow check;
|
||||
- the 250-character description **target** (`hand_invoked()` in `scripts/skill-size-check.sh`).
|
||||
|
||||
It keeps the 400-character description FAIL and **both** body word tiers, and if its description
|
||||
does happen to name a target, that target is still resolved and can still dangle.
|
||||
|
||||
Why the exemption is right: `disable-model-invocation: true` removes the skill from the
|
||||
model-visible listing entirely — it is not preloaded, and the Skill tool refuses to call it — so its
|
||||
description is never matched against user intent. ADR-0020 and `skill-author`'s contract therefore
|
||||
give such a skill **one plain human-facing sentence**: no trigger list, no boundary clause. No
|
||||
validator knew the field existed (**#108**), so the boundary-clause SUGGESTION fired on exactly the
|
||||
shape the contract mandates, and its 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 followed the advice made
|
||||
the file worse. There is no router to inform.
|
||||
|
||||
The half that does **not** lift is the point. The body is still loaded on invocation and still
|
||||
competes with the caller's live conversation, so neither body tier moves. The 400-character ceiling
|
||||
stands too: a hand-invoked description is not preloaded, but it is still the one line the user reads
|
||||
when choosing from the `/` menu, and that ceiling is an outlier stop rather than a routing-quality
|
||||
budget — which is precisely why the 250-character target is the tier that lifts.
|
||||
|
||||
The field is read as a **boolean**, not as a mention of the key. PyYAML already resolves the
|
||||
unquoted YAML 1.1 booleans, so the extra handling catches a quoted `"true"`, which a host reads as
|
||||
truthy; `disable-model-invocation: false` is the model-invoked case written out longhand and buys
|
||||
nothing. A frontmatter parse failure returns false rather than raising — the flag is a *modifier* on
|
||||
other checks, and `description_value()` on the same text already reports the broken frontmatter, so
|
||||
raising here would diagnose one file twice two different ways.
|
||||
|
||||
`caveman` and `zoom-out` are the two carriers here. `tests/test-skill-size-check.sh` pins both
|
||||
halves — what the carve-out lifts, each with a flag-removed control, and what it must not.
|
||||
|
||||
### `verbose: true` is load-bearing
|
||||
|
||||
The hook is declared `verbose: true` so the SUGGESTION tier is audible. pre-commit prints nothing at
|
||||
@@ -281,8 +441,10 @@ script.** `scripts/skill-size-check.sh` applies its body gate to whatever path i
|
||||
bash scripts/skill-size-check.sh plugins/*/.apm/agents/*.agent.md
|
||||
```
|
||||
|
||||
exits 1 today with 900-word body FAILs on `git-orchestrate` (933), `gitea-orchestrate` (1,199) and
|
||||
`apm-orchestrate` (1,080). Agent files escape only because the hook definitions filter on `SKILL.md`
|
||||
exits 1 today with 900-word body FAILs on `git-orchestrate` and `gitea-orchestrate`. (Counts are
|
||||
deliberately not 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.) Agent files escape only because
|
||||
the hook definitions filter on `SKILL.md`
|
||||
— a file-pattern accident that happens to implement the design, not the design itself. **Do not
|
||||
"extend" that hook's `files:` pattern to cover agents** on the assumption that the script already
|
||||
knows the difference; doing so silently enforces a gate ADR-0020 declines to set.
|
||||
@@ -292,20 +454,39 @@ knows the difference; doing so silently enforces a gate ADR-0020 declines to set
|
||||
**The ADR-0020 gates ship hot, with no baseline file.** A shrinking baseline recording each
|
||||
non-compliant skill's current numbers was considered and rejected in favour of hot gates.
|
||||
|
||||
Two independent hot gates are currently red, and the first will not warn you about the second.
|
||||
**The corpus is now clean on both gates.** Issue **#99** retrofitted all 39 skills plugin by plugin;
|
||||
`kyberforge` was the last wave, after which the corpus was swept as a whole rather than per plugin.
|
||||
Each sweep is followed by an **independent review round**: a fresh agent with no memory of the
|
||||
retrofit re-measures the corpus and files what it finds, and the round repeats until one lands no
|
||||
findings. The rounds are recorded as comments on **#99** — read the current state off that thread,
|
||||
which is why no round count is pinned here.
|
||||
|
||||
| Gate | Current findings |
|
||||
|---|---|
|
||||
| `skill-size-check` | **26 of 39** descriptions and **9 of 39** bodies exceed their FAIL tier; 2 dangling targets; 58 SUGGESTIONs |
|
||||
| `Kyberforge.CompositionNote` (Vale) | **10 errors across four skills**: `gitea-issues`, `gitea-labels-milestones`, `gitea-prs`, `gitea-workflow` |
|
||||
| `skill-size-check` | **0 of 39** descriptions and **0 of 39** bodies exceed their FAIL tier; 0 dangling targets; SUGGESTIONs outstanding (count not pinned — see below) |
|
||||
| `Kyberforge.CompositionNote` (Vale) | **0 errors** — the four `gitea-*` carriers were all retrofitted |
|
||||
|
||||
**The SUGGESTION count is deliberately not recorded here.** It moves with every skill edit *and*
|
||||
with every change to the gate's own tiering, so any figure written down is stale by the next commit.
|
||||
Measure it instead:
|
||||
|
||||
```
|
||||
bash scripts/skill-size-check.sh plugins/*/.apm/skills/*/SKILL.md | grep -c '^SUGGESTION'
|
||||
pre-commit run skill-size-check --all-files # same findings, via the hook
|
||||
```
|
||||
|
||||
A non-zero count is the expected steady state, not a regression. SUGGESTIONs exit 0 and block
|
||||
nothing; only the two FAIL tiers, the dangling-target ERROR and the missing-`references/` ERROR do.
|
||||
Read the count as a work queue, and the FAIL columns above as the gate.
|
||||
|
||||
`Kyberforge.CompositionNote` is the ADR-0020 Vale rule banning composition and architecture prose
|
||||
from a description. Every Vale rule here is `level: error` with no ignorable tier, so touching any of
|
||||
those four skills means fixing its prose findings as well as its size findings.
|
||||
from a description. Every Vale rule here is `level: error` with no ignorable tier, so a description
|
||||
that reintroduces one blocks the commit even though no skill carries one today.
|
||||
|
||||
Consequence: editing a non-compliant skill *for any reason* means retrofitting it to the contract
|
||||
first — a one-line fix to `gitea-prs` cannot be committed until that skill complies. This is
|
||||
deliberate; it guarantees convergence and avoids a half-state. Tracked as Gitea issue **#99**.
|
||||
Because nothing is grandfathered, the gates now bite on **first commit**: a new skill, or an edit
|
||||
that pushes a description past 400 characters or a body past 900 words, is blocked until it
|
||||
complies. That is the steady state the retrofit was for — it is no longer true that an unrelated
|
||||
one-line fix to a skill requires retrofitting that skill first.
|
||||
|
||||
Check where a skill stands before starting, and check **both** gates:
|
||||
|
||||
@@ -434,6 +615,35 @@ passing one explicit file per invocation. The two manifests scope **differently
|
||||
Narrowing a `.vale.ini` glob to a `plugins/`-shaped path to "tighten" it breaks the consumer case,
|
||||
and `check-vale-style-sync`'s probe set is built to catch exactly that.
|
||||
|
||||
### The blind spot: `references/` is unlinted, for two independent reasons
|
||||
|
||||
Every `references/*.md` file in the corpus is outside the prose gate. Count them with
|
||||
`git ls-files | grep -cE '^plugins/[^/]+/\.apm/skills/[^/]+/references/.*\.md$'` rather than reading
|
||||
a figure here; it moves with every retrofit. This is the gap that matters most, because the context
|
||||
contract's own remedy for an over-long body is to move prose **into** `references/` — the gate pushes
|
||||
text across its own boundary and then stops watching it.
|
||||
|
||||
**Closing either cause alone changes nothing.** There are two, and they are independent:
|
||||
|
||||
| Cause | Where | Effect on a `references/` file |
|
||||
|---|---|---|
|
||||
| the `Kyberforge` style is scoped `[**/SKILL.md]` | `skill-audit/assets/vale/.vale.ini` | matches no section, so Vale lints 0 files and exits 0 |
|
||||
| the hook's `files:` regex is `^plugins/[^/]+/\.apm/skills/[^/]+/SKILL\.md$` | `vale-audit-prefilter-skill` in `.pre-commit-config.yaml` | the file is never handed to Vale at all |
|
||||
|
||||
Verified both ways. Handing skill-audit's `vale-wrap.sh` a reference file directly — bypassing
|
||||
pre-commit entirely, so only the style scope is in play — prints `0 errors … in 0 files` and exits 0,
|
||||
where the same wrapper on a `SKILL.md` reports `in 1 file`. And the hook's `files:` regex, applied to
|
||||
`git ls-files`, selects only the skill-directory `SKILL.md` files scoped at the top of this page, so
|
||||
pre-commit never hands Vale a reference file to begin with. Widening the glob to `[**/*.md]` would
|
||||
still lint nothing through the hook; widening the hook's `files:` alone would hand Vale files its own
|
||||
config declines to match, which is the [0-file NOT RUN](#a-0-file-vale-run-is-not-run) shape — a
|
||||
green run that measured nothing. **Issue #117** records the style-scope half; the hook half has to
|
||||
land in the same change or the fix is cosmetic.
|
||||
|
||||
The consumer manifest is a third axis and does not rescue this either: `.pre-commit-hooks.yaml`'s
|
||||
`(^|/)SKILL\.md$` is layout-agnostic but still filename-shaped, so an external repo running
|
||||
`kyberforge-vale-audit-skill` has the same gap.
|
||||
|
||||
### `vale-wrap.sh`, never bare `vale`
|
||||
|
||||
Both audit skills' Step 1 and both pre-commit hooks call **each copy's own**
|
||||
|
||||
Reference in New Issue
Block a user