Why
A six-agent review of the two preceding commits found their code sound -- the
differential claim holds, the published hook contract is byte-unchanged -- but
their prose drifted from it in three ways: statements of fact the code
contradicts, markers in a convention this repo does not use, and figures that
went stale when the merge changed what they counted.
Implementation Notes
ADR-0025's edge-path table is rewritten around one stated doctrine: exit 0 is
audited and clean, exit 1 is audited with findings or a target present but
unreadable, exit 2 is that nothing was audited. Its old row 1 promised "one
generic matches-neither message" for three different inputs; there are three
distinct messages, and the missing-path case exited 1 until the preceding commit
fixed it. Rows are added for the preflight and CDPATH changes, because a table
claiming to enumerate every entry-point behaviour change reproduces its own
"an earlier revision of this ADR said they were behaviour-neutral" failure if it
omits any.
ADR-0025 also gains a Consequences supersession record in ADR-0016's form:
partially-superseded entries for 0008, 0014, 0020 and 0021, and explicit
"is not superseded" entries with reasoning for the rest. Twelve ADRs are amended
and it previously listed none.
ADR-0008 moves from an amendment note to partially superseded. Its contract
genuinely narrowed -- an agent .md outside an agents/ directory was audited
before the merge and is refused now -- and ADR-0020 already recorded that the
merge "reopens ADR-0008". Its detector description said "a path under
.apm/agents/", the phrasing ADR-0025 rejects as wider than the script and
circular; the shipped rule is a .md whose immediate parent is named agents/, at
any scope.
ADR-0020's amendment claimed the boundary resolver is sourced by
validate-provenance.sh. It is not, and never was; only validate.sh sources it,
once per mode branch. Three Home-column entries pointed at reference filenames
the merge renamed, one of which now resolves to two files because its row covers
skills and agents.
Five ADRs opened with "Skill renamed per ADR-0025", a form this repo does not
use, in the same commit that used the conventional "Amended by ADR-0025" twice.
They are normalized. "Renamed" was also wrong: the BREAKING-CHANGE trailer says
the skills were removed and their flows merged.
SIMPLIFICATION-AUDIT.md had 2026-09-15 notes attached to headlines that were
never updated, against its own convention of correcting in place with
strikethrough. Every figure here was re-derived at HEAD by command, and several
differed from the review's own numbers, so the notes record the basis rather
than the result alone.
LESSONS.md asserted the two review-time suite failures were the SIGPIPE race.
The commit that fixed that race explicitly declined to claim it -- the suite was
running while agents edited live config files -- so the hedge is restored.
Impact
No code, test or configuration change; documentation only. Suites stay 20/20
strict with 0 skipped and 374/374 bats. No gate parses ADR or gates.md content,
so nothing here is load-bearing for a hook.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YR2CjVumUbEGWcMikcoXBD
@@ -136,4 +136,8 @@ A retrofit replaced "keep reference chains one level deep" with "two hops, never
## 2026-09-15 — A rare flake in a pipefail suite is a race until proven otherwise
The pre-push `run-tests` failed 2 of 3 full runs, on a different suite each time, and neither failure reproduced alone, so it was treated as noise. The cause was`echo "$OUT" | grep -q P` under `set -o pipefail`, at 116 sites. `grep -q` exits on its first match, `echo` takes SIGPIPE on its next write, and pipefail reports correct output as "no match". Unforced it failed about once in 670 runs; with a pause forced before the last line, 50 of 50. Fix: use `grep -q P <<< "$OUT"` (a here-string has no writer process to race), and add a static guard (`tests/test-no-pipefail-early-exit-grep.sh`) instead of relying on convention.
The pre-push `run-tests` failed 2 of 3 full runs, on a different suite each time, and neither failure reproduced alone, so it was treated as noise. Investigating it found a real race at 116 sites:`echo "$OUT" | grep -q P` under `set -o pipefail`. `grep -q` exits on its first match, `echo` takes SIGPIPE on its next write, and pipefail reports correct output as "no match". Unforced it failed about once in 670 runs; with a pause forced before the last line, 50 of 50.**The two failures that started this were never proven to be that race** — they are consistent with it, but the suite was running while agents edited live config files in place, and a brief change to `.vale.ini` or `.pre-commit-hooks.yaml` produces exactly those two failures. The race is real and is fixed either way; what is not established is that it caused the symptom that led to finding it. Fix: use `grep -q P <<< "$OUT"` (a here-string has no writer process to race), and add a static guard (`tests/test-no-pipefail-early-exit-grep.sh`) instead of relying on convention.
## 2026-09-16 — A static guard covers exactly the spellings its regex models
The guard added above (`tests/test-no-pipefail-early-exit-grep.sh`) reported the tree clean the day it landed, and the tree was not clean. Its pattern could not see a multi-stage pipeline (`echo x | filter | grep -q y`), a pipeline wrapped after a trailing `|`, or `set -o errexit -o pipefail` — the last a file-level miss that silently skipped every site in that file. Five live vulnerable sites sat in `tests/test-apm-current-hook.sh` while the suite reported green, and the fixtures carried 4 of the 12 vulnerable spellings that turned out to exist. A clean verdict from a static scanner is a claim about its regex, not about the tree. Fix: before trusting one, enumerate the spellings of the defect and measure the miss surface against them; and write the scanner's remit and its known false-positive shapes into the gate's own documentation, so the next reader knows what the green covers.
@@ -21,25 +21,27 @@ Counting convention: line counts are hand-edited `.apm/` source unless marked "i
>
> > **Re-measured (2026-09-14, at `a6434e0`):** the right-hand column originally read 31,473 / 6,050 / 3,471 / 2,360 / 923 / 2,083 = 46,360 and was labelled "Today" against "the current working tree". It did not reconcile to its own commit's tree — at `061bb3d`, where it was written, the six plugins measured 31,435 / 6,048 / 3,474 / 2,358 / 926 / 2,087 = 46,328 — and "the current working tree" is a basis that goes stale silently. Re-counted at `a6434e0` and the column now names its SHA. The baseline column is confirmed exact against `9eb8bc7`. Commits after `061bb3d` (`c96ca9c`, which deleted the six plugin-root `.mcp.json` files) account for most of the remaining drift.
> **Re-derived (2026-09-16, at HEAD on `docs/simplification-audit`):** the 2026-09-15 notes recording finding 14's merge (`467bbd7`, ADR-0025) and the pipefail fix (`4059cb4`) were written without correcting the headlines they annotate, so this pass re-counted every figure those two commits could have moved and corrected each in place above and below. Everything re-measured here came from a command run at HEAD — `git ls-files`, `wc -l`, `grep -c`, and `bash tests/run-tests.sh --strict` — never from an earlier note. What moved: finding 2 (two surviving sync gates → one), the `.pre-commit-config.yaml` hook counts (27/9 → 26/8), the skill census (39 → 38 and everything derived from it), finding 11's validator and `sources.md` figures, finding 16's whole numeric basis, and the stale `skill-audit/`, `agent-audit/` and `formatting-and-scripts.md` paths in findings 18, 19 and 33. §1's three rows re-measured at HEAD: **469** tracked files (465 regular plus the 4 submodule gitlinks) / **74,594** lines; `plugins/` **46,106** (62%); the 38 `SKILL.md` bodies **2,409** (5.2% of plugin lines); enforcement **20 `tests/test-*.sh` totalling 10,189 lines**, the two runners **502** (`run-tests.sh` 283 + `run-bats.sh` 219), and `scripts/` **2,901**; kyberforge's validator scripts and their bats tests **5,861 + 6,015** (the merge deduplicated scripts and left the test corpus larger, not smaller — `git ls-files 'plugins/kyberforge/.apm/skills/*/scripts/*.sh'` and `.../tests/*.bats`). `run-tests.sh --strict` reports **20 passed, 0 skipped, 0 failed**. Re-checked and unchanged, so left alone: `docs/research/` inside plugins (19,030) and repo-level `docs/research/` + `docs/notes/` (4,488). Not re-measured, and still carrying their last stated basis: the preload-tax and commit-share rows, §2's timings, and the per-plugin table in the note above.
> **Reviewed (2026-09-14):** commits `718c79a` and `d2480b8` were put through a five-agent review. Result: **zero skill, agent or hook regressions** — 39 skills before and after, all gates passing, and both hook removals (`validate-plugins`, `check-plugin-content-sync`) genuinely moot rather than merely unenforced. One real functional regression was found — MCP propagation to consumers, broken by the same commit's manifest deletion; see finding 37 — along with the numeric and bookkeeping drift in this document's own 2026-09-14 notes, corrected in place above and below.
| Lines in `plugins/` | ~~70,600 (69% of repo)~~ → ~~46,301 (63% of repo)~~ → 46,106 (62% of repo) |
| Of which the ~~39~~ → 38`SKILL.md` files a model actually loads | ~~about 2,600 lines (under 4% of plugin lines)~~ → ~~2,509 lines (5.4% of plugin lines)~~ → 2,409 lines (5.2% of plugin lines) |
| Commits since 2026-05-10 / share touching hook, test, gate, vale, or sync | 447 / ~25% |
> **Corrected then done (2026-09-14):** the mirror row's figure was wrong. The true mirror was **213 files / 20,061 lines**, not 263 / ~22,000 — the original count swept in files that were never mirror output. All 213 were deleted in commit `718c79a` on `docs/simplification-audit` (245 files changed, 298 insertions, 22,602 deletions across the whole change), so the row is now zero. The enforcement row is stale on **both** halves — it was correct at the 2026-09-10 baseline (`9eb8bc7`: 33 `- id:` entries, 14 repo-authored pre-push hooks), but `.pre-commit-config.yaml` today has **27 entries and 9 `stages: [pre-push]`**. Like for like that is 14 → 9 repo-authored pre-push hooks. The stage *reports* 11, because the 2 pre-commit `meta` hooks also run there — a different counting basis; see the corrected §3 target, which states it the same way.
> **Corrected then done (2026-09-14):** the mirror row's figure was wrong. The true mirror was **213 files / 20,061 lines**, not 263 / ~22,000 — the original count swept in files that were never mirror output. All 213 were deleted in commit `718c79a` on `docs/simplification-audit` (245 files changed, 298 insertions, 22,602 deletions across the whole change), so the row is now zero. The enforcement row is stale on **both** halves — it was correct at the 2026-09-10 baseline (`9eb8bc7`: 33 `- id:` entries, 14 repo-authored pre-push hooks), but `.pre-commit-config.yaml` today has ~~**27 entries and 9 `stages: [pre-push]`**~~ → **26 entries and 8 `stages: [pre-push]`** (re-measured 2026-09-16 at HEAD; `467bbd7` removed `check-vale-style-sync` with finding 14's merge). Like for like that is 14 → ~~9~~ → 8 repo-authored pre-push hooks. The stage *reports* ~~11~~ → 10, because the 2 pre-commit `meta` hooks also run there — a different counting basis; see the corrected §3 target, which states it the same way.
> **Re-measured (2026-09-14, at `a6434e0`):** this table is a **dated snapshot corrected in place**, not a live figure — every arrow above reads "baseline (2026-09-10, `9eb8bc7`) → value at the stated commit". Three further rows were still carrying baseline values after `d2480b8`/`061bb3d` corrected their neighbours, and are now corrected at `a6434e0`:
>
@@ -96,17 +98,18 @@ This is the area you named as hardest to understand and slowest. Root cause: mos
> **Grilled and closed (2026-09-14):** `apm-audit-ci` — already resolved before this audit was written: `.pre-commit-config.yaml`'s own comment block (added in commit `a155af6`, months before this audit) already rebuts the "overclaimed description" complaint and gives a dated, verified justification for what the hook still checks. Keep, no action. `apm-marketplace-check` — its stated purpose ("the only hook that checks remote package references rather than local-source paths") is void: finding 35 (commit `568ca74`) already removed the only remote package entry, so every `marketplace.packages[]` entry is now a local `./plugins/<name>` path and the hook is pure overlap with `apm-pack-check-clean`. Removed the hook entry, and corrected the now-stale "does NOT join apm-marketplace-check ... on the offline SKIP= list" comment on `apm-audit-ci` (there is no offline skip list any more — every pre-push hook already passes offline per `README.md`). Updated `README.md` (tool table, "Offline?" section) and `docs/spec/gates.md` (hook table, hook counts 13→11 self-authored / 15→13 total, the "Three of these shell out to apm" paragraph, and the "Pushing without a network" section) accordingly. Verified: `apm audit --ci` still passes per-plugin, and the pre-push hook count now matches `.pre-commit-config.yaml`.
> **Corrected and closed (2026-09-14, at `a6434e0`):** two things above went stale within hours of being written, and the finding was never given a marker.
>
> - **"Keep the two `claude plugin validate` hooks"** is void. `718c79a` (ADR-0024) deleted `validate-plugins` — the ADR's own reasoning is that `claude plugin validate` reads manifests only and could never detect the empty-content defect it was credited with guarding, and with the per-plugin manifests gone it has nothing left to read. Only **`validate-marketplace`** survives, over the one manifest this repo still ships (`.claude-plugin/marketplace.json`). Of the six hooks this finding named, three now exist: `validate-marketplace`, `apm-pack-check-clean`, `apm-audit-ci`. Verified against `.pre-commit-config.yaml`: 27 `- id:` entries, 9 with `stages: [pre-push]`, no `validate-plugins` entry.
> - **"Keep the two `claude plugin validate` hooks"** is void. `718c79a` (ADR-0024) deleted `validate-plugins` — the ADR's own reasoning is that `claude plugin validate` reads manifests only and could never detect the empty-content defect it was credited with guarding, and with the per-plugin manifests gone it has nothing left to read. Only **`validate-marketplace`** survives, over the one manifest this repo still ships (`.claude-plugin/marketplace.json`). Of the six hooks this finding named, three now exist: `validate-marketplace`, `apm-pack-check-clean`, `apm-audit-ci`. Verified against `.pre-commit-config.yaml`: ~~27 `- id:` entries, 9 with `stages: [pre-push]`~~ → **26 `- id:` entries, 8 with `stages: [pre-push]`** (re-measured 2026-09-16 at HEAD), no `validate-plugins` entry.
> - **The gates.md figures above ("13→11 self-authored / 15→13 total") were correct for `0dffff3` and are no longer current.** `718c79a` removed two more pre-push hooks after that commit, and `docs/spec/gates.md:24` read **11 reported / 9 self-authored** when this note was written; finding 14's merge has since removed `check-vale-style-sync`, and it now reads **10 reported / 8 self-authored**. Read the count from that file, not from this note.
>
> Marked `[x]`: all three of this finding's decisions are resolved — `check-manifests` deleted (`e647f14`), `apm-audit-ci` kept on the grill above, `apm-marketplace-check` removed (`0dffff3`).
2.**~~Four~~ two surviving "keep two copies in sync" gates: ~~1,100 script lines + 1,600 test lines~~ 778 script lines + 1,079 test lines.** Each one is a symptom of duplication that could be removed instead of guarded:
2.**~~Four~~ ~~two~~ → one surviving "keep two copies in sync" gate: ~~1,100 script lines + 1,600 test lines~~~~778 script lines + 1,079 test lines~~ → 381 script lines + 297 test lines.** Each one is a symptom of duplication that could be removed instead of guarded:
> **Re-measured (2026-09-16, at HEAD):** `467bbd7` deleted `check-vale-style-sync` with finding 14's merge, so the "two" above is now **one** — `check-scope-walkup-sync`, at **381** script lines (`wc -l scripts/check-scope-walkup-sync.sh`) and **297** test lines (`wc -l tests/test-check-scope-walkup-sync.sh`). Both grew since `a6434e0`, where they measured 365 + 282. Reading `check-executables-allow-sync` into the group as the note below does makes it two gates, **603 + 540** (222 + 243 for that one, unchanged).
> **Re-measured (2026-09-14, at `a6434e0`):** two of the four are gone — `check-marketplace-mirror-sync` deleted in `0dffff3` (2c below) and, though it was never in this finding's own count, `check-plugin-content-sync` in `718c79a`. The two that survive are `check-vale-style-sync` (413 script + 797 test) and `check-scope-walkup-sync` (365 + 282); `check-executables-allow-sync` also survives, shrunk to 222 + 243 (2d below), and counts as the third if that gate is read as part of this group rather than as its own item. Two-gate total 778 + 1,079; three-gate total 1,000 + 1,322. The per-bullet script and test figures below are all still exact at this commit except `check-executables-allow-sync`'s "474 lines", which 2d already corrects.
- `check-vale-style-sync`: 413 lines + 798 test lines guarding a byte-identical 526-line `vale-wrap.sh` and style directory copied between skill-audit and agent-audit. About 350 of its lines run Vale glob probes against the hook file patterns. Disappears if the two audit skills merge (finding 14); the probes belong in `test-vale-wrap.sh`.
-`check-scope-walkup-sync`: 365 lines cross-checking four independent ports of the same package-root walk-up. Disappears if the ports share one script or the skills merge.
-[x]~~`check-vale-style-sync`: 413 lines + 798 test lines guarding a byte-identical 526-line `vale-wrap.sh` and style directory copied between skill-audit and agent-audit. About 350 of its lines run Vale glob probes against the hook file patterns. Disappears if the two audit skills merge (finding 14); the probes belong in `test-vale-wrap.sh`.~~**Done (2026-09-15, `467bbd7`)** — hook, script and test all deleted; see the settled note below for the corrected probe arithmetic.
-`check-scope-walkup-sync`: ~~365~~ → **381** lines (plus **297** test lines; re-measured 2026-09-16 at HEAD) cross-checking four independent ports of the same package-root walk-up. Disappears if the ports share one script ~~or the skills merge~~ — the second half is refuted below, and the first is unreachable.
> **Grilled, held (2026-09-14):** both of the above are gated on findings 14/15 (merging skill-audit+agent-audit and skill-author+agent-author), deliberately held for a separate session rather than decided here. Correction for that session: the audit's §8 grouping is wrong — these merges don't need ADR-0012 revisited (that ADR governs the unrelated `core` plugin's three `agentsmd-*` skills). The actual constraint is ADR-0014 (no-cross-skill file sharing on plugin cache-install), and merging sidesteps it rather than requiring it be reversed. The open question for that session is a design one — a shared skill's `description` carrying both skill- and agent-audit trigger phrases — not an ADR supersession. ADR-0012 revisit is needed only for finding 24.
> **Settled (2026-09-15) — split verdict, and the first bullet held in full.** Finding 14 landed as `factory-audit` (ADR-0025). **`check-vale-style-sync` is deleted**, hook, script and test, exactly as the first bullet predicted — and its probes **were** rehomed into `test-vale-wrap.sh`, as cases 28-30 (case 31 carries the override allowlist), so both halves of that bullet are closed. `docs/spec/gates.md` records the rehoming, not an open gap. *(Updated later on 2026-09-15.)* The one assertion this note used to call still uncovered — cross-manifest *agreement* between `.pre-commit-hooks.yaml`'s and `.pre-commit-config.yaml`'s `files:` regexes — is now ported as case 33, which pairs the hooks by `id:`. Case 32 covers the separate zero-match question. It was a real gap while it lasted: narrowing the local skill hook to `^plugins/kyberforge/` left 6 of 38 skills prefiltered and the suite green. `bash tests/test-vale-wrap.sh` now reports `61 passed, 0 failed` (it was 56 before cases 0 and 33 and the Part B mutation self-tests). The bullet's "about 350 of its lines run Vale glob probes" overstates the probe half: at `a5962ba` the script is **413 lines**, of which the `.vale.ini` coverage section is **332** (`67..398`) and the machinery that actually invokes vale against a probe path is **204** (`195..398`). The balance of that section is `StylesPath`, `BasedOnStyles` and per-rule-override greps — text assertions, not probes. (Its test file is **797** lines, as the note above says, not the 798 the bullet carries.) **`check-scope-walkup-sync` stays**, and the second bullet's "or the skills merge" is wrong: two of its four walk-up ports are in the *author* skills (`new-agent.sh`, `new-skill.sh`), which this merge does not touch, and the audit-side pair is Python against the author-side pair's Bash, so the gate can never degrade into a text diff. Full reasoning in §10's 2026-09-15 note. Finding 15 would not remove it either.
> **Settled (2026-09-15) — split verdict, and the first bullet held in full.** Finding 14 landed as `factory-audit` (ADR-0025). **`check-vale-style-sync` is deleted**, hook, script and test, exactly as the first bullet predicted — and its probes **were** rehomed into `test-vale-wrap.sh`, as cases 28-30 (case 31 carries the override allowlist), so both halves of that bullet are closed. `docs/spec/gates.md` records the rehoming, not an open gap. *(Updated later on 2026-09-15.)* The one assertion this note used to call still uncovered — cross-manifest *agreement* between `.pre-commit-hooks.yaml`'s and `.pre-commit-config.yaml`'s `files:` regexes — is now ported as case 33, which pairs the hooks by `id:`. Case 32 covers the separate zero-match question. It was a real gap while it lasted: narrowing the local skill hook to `^plugins/kyberforge/` left 6 of 38 skills prefiltered and the suite green. `bash tests/test-vale-wrap.sh` now reports ~~`61 passed, 0 failed`~~ → `63 passed, 0 failed` (it was 56 before cases 0 and 33 and the Part B mutation self-tests; 61 on 2026-09-15, and 63 once case 34 — the static `.vale.ini` style-load check — landed on 2026-09-16. Without vale on PATH it reports 19 and exits 77, up from 17). The bullet's "about 350 of its lines run Vale glob probes" overstates the probe half: at `a5962ba` the script is **413 lines**, of which the `.vale.ini` coverage section is **332** (`67..398`) and the machinery that actually invokes vale against a probe path is **204** (`195..398`). The balance of that section is `StylesPath`, `BasedOnStyles` and per-rule-override greps — text assertions, not probes. (Its test file is **797** lines, as the note above says, not the 798 the bullet carries.) **`check-scope-walkup-sync` stays**, and the second bullet's "or the skills merge" is wrong: two of its four walk-up ports are in the *author* skills (`new-agent.sh`, `new-skill.sh`), which this merge does not touch, and the audit-side pair is Python against the author-side pair's Bash, so the gate can never degrade into a text diff. Full reasoning in §10's 2026-09-15 note. Finding 15 would not remove it either.
- [x]~~`check-marketplace-mirror-sync`: guards `.github/plugin/marketplace.json`. The script header calls it Copilot's legacy convention path and says Copilot also accepts the Claude path; the vendored Copilot docs list it as primary. Verify against current Copilot CLI before deleting hook, script, test, and mirror file.~~
> **Grilled and done (2026-09-14):** verified against GitHub's current Copilot CLI plugin docs (not the vendored copy, which risked drift). Copilot CLI's marketplace discovery checks paths in order — `marketplace.json`, `.plugin/marketplace.json`, `.github/plugin/marketplace.json`, `.claude-plugin/marketplace.json` — falling through to whichever exists first. `.claude-plugin/marketplace.json` (apm's own `claude` output) already satisfies that chain's last step, so the dedicated `.github/plugin/marketplace.json` mirror bought Copilot users its *preferred* discovery path rather than a required one. Decided against reopening ADR-0018 (native install for both Claude Code and Copilot CLI stays supported) to justify this — the deletion holds either way, since Copilot's own fallback covers it. Deleted `.github/plugin/marketplace.json`, `scripts/sync-marketplace-mirror.sh` (81 lines), `tests/test-sync-marketplace-mirror.sh` (304 lines), and the `check-marketplace-mirror-sync` pre-push hook; removed the dangling references to the deleted script in `scripts/sync-plugin-content.sh` and `tests/test-sync-plugin-content.sh` (both had comments citing its reasoning by name), and updated `docs/spec/architecture.md`'s description of the marketplace-manifest compile step. `tests/test-sync-plugin-content.sh` (92 cases) still passes in full.
>
@@ -125,7 +128,7 @@ This is the area you named as hardest to understand and slowest. Root cause: mos
5.**`skill-size-check.sh` has six test files totalling 3,589 lines for one 1,497-line script**, split by ADR section rather than behaviour. `test-adr0020-differential.sh` is 452 lines for 12 assertions. Merge to two files. Effort M.
> **Not proceeding (2026-09-14):** premise doesn't hold, in the same way finding 3's did not. The six suites are **not** split by ADR section — they are split by failure class, and five of the six headers name the incident they guard. (The exception is `tests/test-skill-size-check.sh`, whose header names no incident: it describes the two gate families the script must not conflate and flags the constant-agreement block as the load-bearing part.) `test-adr0020-contract.sh` defends *structural* claims that "each one fails silently": that the resolver block copied verbatim into three scripts has not drifted, that both interpreter preflights still exist, that `verbose: true` is still set on the hook (the entire delivery mechanism for the SUGGESTION tier). It records that the `validate-provenance.sh` pair "had already drifted" once. `test-adr0020-differential.sh` compares *verdicts* between `skill-size-check.sh` and `validate.sh` on real files, and its header states that constant-agreement is "necessary but demonstrably not sufficient — a previous review found the two scripts disagreeing on real files while every constant matched perfectly", with two ceilings excluded "until a real divergence shipped behind the exclusion". The suites also do not cover the same scripts: `contract` reaches `validate-provenance.sh` (`tests/test-adr0020-contract.sh:115-116` byte-compares both copies of it). Merging by subject would delete exactly the guards against silent drift between hand-duplicated validators. Re-measured at HEAD: **3,619 lines** across six suites against a **1,517-line** script, not 3,589/1,497. That ratio is the cost of the duplication, not an independent defect — it is deleted by **finding 16**, which removes the thing being differentially compared. **#5 is downstream of #16 and should be reconsidered only after it.** The one salvageable part is a performance change, not a coverage change: `test-adr0020-differential.sh` spends 29 s of every push re-running two validators over the live corpus, and could be sped up with no coverage loss. That is a different finding than the one written here.
6. [x] ~~**Prose-grep tests.** `test-governance-layer.sh` and `test-instructions-and-docs.sh` (583 lines) grep markdown for phrases, including a one-shot "issue 0015 refactor incomplete" assertion made permanent and an assertion that `docs/notes/` exists. Delete both.~~`check-apm-agents-valid.sh` (161 + 264 test lines) is a loop plus fail-closed guards around `validate.sh`; it folds into the merged audit skill's own tests (finding 14). Effort S.
6. [x] ~~**Prose-grep tests.** `test-governance-layer.sh` and `test-instructions-and-docs.sh` (583 lines) grep markdown for phrases, including a one-shot "issue 0015 refactor incomplete" assertion made permanent and an assertion that `docs/notes/` exists. Delete both.~~`check-apm-agents-valid.sh` (~~161 + 264 test lines~~ → **167 + 282**, re-measured 2026-09-16 at HEAD) is a loop plus fail-closed guards around `validate.sh`; it folds into the merged audit skill's own tests (finding 14). Effort S.
> **Done (2026-09-12):** see commit `5f9f2b3` on `docs/simplification-audit`. Deleted `tests/test-governance-layer.sh` (270 lines) and `tests/test-instructions-and-docs.sh` (313 lines); no other file referenced either. `check-apm-agents-valid.sh` was left untouched — its fate is tied to the separate, out-of-scope skill-merge finding 14.
7. [x] ~~**`check-plugin-content-sync.sh` is 813 lines wrapping `apm pack`, with a 1,291-line test.** The mirror itself must stay (Claude Code marketplace installs need flat directories), and the script does real work a bare `git diff` would lose: it strips `tests/` from the mirror, regenerates both `plugin.json` files with `mcpServers` reinjected, and packs into a scratch copy so `--check` never mutates. Even so, 2,100 lines for that is disproportionate; target a third. Effort M.~~
@@ -156,18 +159,19 @@ The shared pattern: per-skill `README.md` files no model reads, a `docs/research
10. [x] ~~**Delete per-skill `README.md` and `references/README.md` (48 files, 1,574 lines).** They restate the SKILL.md in narrative form. The pre-commit config itself notes a skill README "is consumer-facing prose that no agent ever loads". Keep one plugin-level README with one line per skill. Requires dropping the README criterion in `skill-audit/references/file-structure.md` and the README step in `new-skill.sh`. Effort S.~~
> **Done (2026-09-12):** see commit `edcc57c` on `docs/simplification-audit`. Deleted the 48 per-skill/reference READMEs plus 2 scaffold templates; dropped the README criterion from `skill-audit`'s `file-structure.md` and `finding-criteria.md` and the README-generation step from `new-skill.sh`; updated `new-skill.bats` to match. Plugin-root READMEs were kept, not part of this finding.
11.**Drop the provenance chain: `sources.md`, `source_keys` frontmatter, `validate-provenance.sh`.** 32 plugin and skill `sources.md` files (about 1,300 lines) plus 9 research indexes, 216 source files with `source_keys`, two copies of the validator (1,198 and 632 lines) with ten checks, and 125 bats tests exist to track which upstream informed which file. Git blame and a URL in the README do the same job. This is more code than the content it tracks. Effort M (touches skill-audit, both validator copies, two repo tests, and every skill's frontmatter).
11.**Drop the provenance chain: `sources.md`, `source_keys` frontmatter, `validate-provenance.sh`.** 32 plugin and skill `sources.md` files (about 1,300 lines) plus 9 research indexes, 216 source files with `source_keys`, ~~two copies of the validator (1,198 and 632 lines)~~ → **one validator, 2,171 lines across four files**, with ten checks, and ~~125 bats tests~~ → **138 bats tests** exist to track which upstream informed which file. Git blame and a URL in the README do the same job. This is more code than the content it tracks. Effort M (touches ~~skill-audit, both validator copies~~ → **`factory-audit`, its one provenance validator**, two repo tests, and every skill's frontmatter).
> **Re-measured (2026-09-16, at HEAD):** ADR-0025 merged the two copies, so the "two copies" arithmetic throughout this finding and its note below no longer resolves. The provenance validator is now `factory-audit/scripts/` `validate-provenance.sh` (320) + `lib-provenance-skill.sh` (1,145) + `lib-provenance-agent.sh` (572) + `lib-contributing-files.sh` (134) = **2,171** lines (`wc -l` on the four), against **3,209** bats lines (`validate-provenance-skill.bats` 2,062 + `validate-provenance-agent.bats` 1,147) carrying **138** cases (`grep -c '^@test'`). Note this is *more* than the 1,198 + 632 = 1,830 the finding counted, not less: the merge deduplicated the resolver and the Contributing-files parser, not the per-mode provenance checks, and the shared entry script added the exit-tier and library guards described in `docs/spec/gates.md`. The `sources.md` census also moved: **45 files / 1,756 lines** — 27 skill `references/sources.md` (1,207), 13 research indexes (435), 4 plugin-root (100), 1 scaffold template (14). The note below's 46 / 1,752 swept in `docs/adr/0013-vale-harness-scope-and-rule-sources.md`, which matches `sources\.md$` and is not one. Imbalance at HEAD: **5,380 validator+bats lines against 1,756 of metadata, 3.1:1** — worse than the 2.6:1 below, on the same direction of argument.
> **Verified (2026-09-14, at HEAD `062ca47`):** direction defensible, two scope figures wrong, and **blocked on a decision the finding never poses**. The `sources.md` census below is exact, and so are the finding's own validator and bats figures (1,198 / 632 lines, 125 bats tests); the scope errors are narrower than an earlier revision of this note claimed.
>
> Corrected figures: **46 `sources.md` files / 1,752 lines** in three distinct classes — 29 skill `references/sources.md` (1,217 lines), 13 research indexes (435), 4 plugin-root files (100, ADR-0010). The finding does **not** double-count: it states two disjoint classes additively ("32 plugin and skill `sources.md` files (about 1,300 lines) **plus** 9 research indexes"), and that plugin-and-skill subtotal is really **33 files / 1,317 lines**, matching its "about 1,300" exactly — had the 32 swept in the research indexes the figure would have been ~1,750. Its real errors there are an off-by-one (32 should be 33) and an omission: it missed the 4 vendored example indexes under `kyberforge/docs/research/examples/skill-write/`, so 9 should be 13. Carriers of `source_keys` in YAML frontmatter: **196** — 168 at column 0 and 28 nested two spaces under `metadata:` — so the finding's 216 is closer to the truth than it looks. (219 files merely *mention* the string. A naive `^[[:space:]]*source_keys:` grep returns 200, but 4 of those are heredoc or fixture text rather than frontmatter: both `validate-provenance.bats` copies, `scripts/check-scope-walkup-sync.sh`, and a fenced example in `plugins/bin/.apm/skills/research/references/file-format.md`.) Checks: **16 across the two copies** (skill-audit 0–9, agent-audit 0–5), not ten. Validator line counts (1,198 / 632) and 125 bats tests are exact.
>
> **"Touches every skill's frontmatter" is roughly right.** **28 of the 39 real skills carry `source_keys` in frontmatter**, nested under `metadata:` — see `plugins/git/.apm/skills/git-commits/SKILL.md:10-17`, where `metadata:` → `source_keys:` carries four slugs. (44 tracked files match `*SKILL.md`; subtract `skill-author/assets/templates/SKILL.md` and the 4 vendored under `kyberforge/docs/research/examples/skill-write/`, leaving 39 real skills.) The 11 without it are exactly the `plugins/bin/` skills. Check 2 in the skill-side validator (SKILL.md `source_keys` → slug in `sources.md`) is correspondingly **live**, not dead code: `parse_source_keys()` at `plugins/kyberforge/.apm/skills/factory-audit/scripts/lib-provenance-skill.sh:277-305` handles both spellings explicitly — the metadata-nested branch at `:292`, the top-level branch at `:295`, and a docstring that says "handles metadata.source_keys and top-level" — check 2 at `:712` runs against all 28 carrier skills, every one of which has a `references/sources.md`, and bats pins it at `plugins/kyberforge/.apm/skills/factory-audit/tests/validate-provenance-skill.bats:222` ("FAIL: source_keys slug in SKILL.md not present as H2 in sources.md") and `:1337` (a BOM must not silently disable check 2). (Paths and line numbers re-derived at HEAD: ADR-0025's merge moved this code out of `skill-audit/scripts/validate-provenance.sh` into the shared skill-side library, so the figures this note carried at `062ca47` — `:242-270`, `:257`, `:260`, `:766`, `:1313` — no longer resolve.) The imbalance the finding names is real and **worse** than claimed: 4,641 validator+bats lines against 1,752 of metadata, a 2.6:1 ratio.
> **"Touches every skill's frontmatter" is roughly right.** ~~**28 of the 39 real skills carry `source_keys` in frontmatter**~~ → **27 of the 38** (re-measured 2026-09-16 at HEAD; the audit-pair merge took one carrier skill with it), nested under `metadata:` — see `plugins/git/.apm/skills/git-commits/SKILL.md:10-17`, where `metadata:` → `source_keys:` carries four slugs. (~~44~~ → **43** tracked files match `*SKILL.md`; subtract `skill-author/assets/templates/SKILL.md` and the 4 vendored under `kyberforge/docs/research/examples/skill-write/`, leaving ~~39~~ → **38** real skills.) The 11 without it are exactly the `plugins/bin/` skills. Check 2 in the skill-side validator (SKILL.md `source_keys` → slug in `sources.md`) is correspondingly **live**, not dead code: `parse_source_keys()` at `plugins/kyberforge/.apm/skills/factory-audit/scripts/lib-provenance-skill.sh:277-305` handles both spellings explicitly — the metadata-nested branch at `:292`, the top-level branch at `:295`, and a docstring that says "handles metadata.source_keys and top-level" — check 2 at `:712` runs against all ~~28~~ → **27** carrier skills, every one of which has a `references/sources.md`, and bats pins it at `plugins/kyberforge/.apm/skills/factory-audit/tests/validate-provenance-skill.bats:222` ("FAIL: source_keys slug in SKILL.md not present as H2 in sources.md") and ~~`:1337`~~ → `:1338` (a BOM must not silently disable check 2). (Paths and line numbers re-derived at HEAD: ADR-0025's merge moved this code out of `skill-audit/scripts/validate-provenance.sh` into the shared skill-side library, so the figures this note carried at `062ca47` — `:242-270`, `:257`, `:260`, `:766`, `:1313` — no longer resolve.) The imbalance the finding names is real and **worse** than claimed: ~~4,641 validator+bats lines against 1,752 of metadata, a 2.6:1 ratio~~ → **5,380 against 1,756, a 3.1:1 ratio** (re-measured 2026-09-16 at HEAD; see the note under the headline).
>
> **Omitted entirely: the chain has a producer.** `plugins/bin/.apm/skills/research/` *specifies* the `sources.md` + `source_keys:` output format, and `plugins/bin/evals/research/research/eval.yaml` carries three criteria asserting it. **This is the blocking decision: does `research` keep emitting `sources.md`?** If yes, the chain is not dropped — only unenforced, and the finding collapses to "delete the validators." If no, the research skill's output contract and its evals need redesigning.
>
> Also breaks: `check-scope-walkup-sync` loses one of four walk-up ports (the hook exists because three scripts drifted); `tests/test-adr0020-contract.sh` loses its parser byte-identity assertion; `tests/test-check-scope-walkup-sync.sh` must re-base its fixture; ADR-0010 is superseded outright and ADR-0009/0016 need amending (`field-inventory.md`'s allowlist data line carries `source_keys`). `LESSONS.md:73` records this validator as the **only** thing that catches a skill authored outside `skill-author` — a failure that "recurred twice in one session" — so "git blame + a README URL do the same job" is false for the one thing the chain demonstrably catches. Side effect: 55 reference files have frontmatter containing *only* `source_keys:`, leaving empty `---\n---` blocks to delete.
>
> **Effort L, not M** (~6,393 lines deleted across 242 files: the 4,641 validator and bats lines plus the 1,752 of `sources.md` measured above, across 196 `source_keys` carriers and 46 `sources.md` files. An earlier revision of this note said ~4,600 lines across ~230 files, which was internally inconsistent — 4,600 is validator-plus-bats only and silently drops the `sources.md` this same note measures, and ~230 inherited a carrier count of 172 that missed every `metadata:`-nested file.) Smaller alternative worth considering: scope the drop to the skill half only (1,217 lines, 1,198-line validator, 82 tests) and leave the ADR-0010 plugin-root half alone — no ADR supersession needed.
> **Effort L, not M** (about ~~6,393~~ → **7,136** lines deleted across 242 files: the ~~4,641~~ → **5,380** validator and bats lines plus the ~~1,752~~ → **1,756** of `sources.md` measured above, across 196 `source_keys` carriers and ~~46~~ → **45** `sources.md` files. An earlier revision of this note said ~4,600 lines across ~230 files, which was internally inconsistent — 4,600 is validator-plus-bats only and silently drops the `sources.md` this same note measures, and ~230 inherited a carrier count of 172 that missed every `metadata:`-nested file.) Smaller alternative worth considering: scope the drop to the skill half only (~~1,217 lines, 1,198-line validator, 82 tests~~ → **1,207 lines of skill `sources.md`, the 1,145-line `lib-provenance-skill.sh`, 87 tests**, re-measured 2026-09-16 at HEAD) and leave the ADR-0010 plugin-root half alone — no ADR supersession needed.
12. [x] ~~**Strip ADR and changelog narration from model-facing files.** `ADR-0020` is cited in 3 of 7 kyberforge SKILL.md files and 16 references; ADR-0023 is cited inline 21 times in the git plugin. Examples: "was the old house rule and ADR-0020 deleted it", "were removed per ADR-0015 once issue #90 landed", "this file previously recorded `list_issues` as having neither a `type` nor a `milestones` parameter". `skill-author/references/retrofit.md` (197 lines) is a one-time migration guide; it is loaded from `improve.md` and listed in `sources.md`, so remove those in the same change. These belong in git history or the ADR, not in context. Effort S.~~
> **Done (2026-09-12):** see commit `edcc57c` on `docs/simplification-audit`. Historical narration stripped from kyberforge (ADR-0020) and git (ADR-0023) skill content; `retrofit.md` deleted along with its load-step and `sources.md` entries. Caught in review: some `ADR-0023` tags were not narration but the `check-rtk-prefix` hook's required opt-out marker for intentionally-bare git commands — those 12 were restored, not left stripped.
@@ -191,6 +195,8 @@ The shared pattern: per-skill `README.md` files no model reads, a `docs/research
16.**Cut the validators by an order of magnitude.**`validate.sh` is 1,677 lines of bash with embedded Python, ported twice; `skill-size-check.sh` is 1,497. Target about 200 lines total: frontmatter present, size ceilings, boundary targets resolve. The 526-line `vale-wrap.sh` exists to work around folded `>` scalars in descriptions; writing descriptions as `|` literal blocks removes the folding problem, but the wrapper is also the exported hook entry in `.pre-commit-hooks.yaml` and carries the NOT RUN guard the audits depend on, so it shrinks rather than disappears. This is where the real complexity lives and is the item most worth discussing. Effort L.
> **Refuted (2026-09-14, at HEAD `062ca47`). Finding 16 has no independent content — its only safe saving belongs to finding 14.**
>
> **Re-measured (2026-09-16, at HEAD) — the basis of every figure below changed when ADR-0025 landed; the refutation is unaffected.** There are no longer three validators or two `vale-wrap.sh` copies. The headline's "ported twice" is void, and its `1,677` and `526` no longer name anything. At HEAD: `scripts/skill-size-check.sh` is **1,522** (the note below's 1,517 was correct at `a6434e0`); `factory-audit`'s validator is **2,663** lines across four files (`validate.sh` 255 + `lib-checks-skill.sh` 621 + `lib-checks-agent.sh` 683 + `lib-boundary-resolver.sh` 1,104); `vale-wrap.sh` is **535**, one copy. Validator total **4,185**, of which the resolver is **2,165** (the 1,061-line block still embedded in `skill-size-check.sh`, plus `lib-boundary-resolver.sh`'s 1,104 — the same 1,061 block wrapped in 43 lines of library preamble, which is why the byte-identity test compares the block and not the files). So the resolver is now **52%** of validator lines, not 65%, and **2,020** lines remain once it is excised, not 1,749. Tests: the six repo suites over `skill-size-check.sh` are **3,907** (was 3,619) and the two in-skill validator bats files **2,248** (`validate-skill.bats` 1,029 + `validate-agent.bats` 1,219), for **6,155**, not 5,506. The 200-line target is off by the same order of magnitude it was. (All figures `wc -l`; the resolver block by `awk '/BEGIN ADR-0020 SHARED BOUNDARY RESOLVER/,/END .../'`.)
>
> The three validators are **not three implementations**. They contain **one block, 1,061 lines, byte-identical in all three**, delimited by `# ===== BEGIN/END ADR-0020 SHARED BOUNDARY RESOLVER =====` and hashed by `tests/test-adr0020-contract.sh`. So 3,183 of 4,932 validator lines (65%) are that block × 3, and **what is left once the resolver is excised is 1,749 lines across all three** — 1,580 non-blank, 992 with comments and blanks both stripped. The duplication is forced by the self-containment constraint, which is why *merging* is the lever and *shrinking* is not.
>
> Corrected figures: `skill-size-check.sh` is **1,517**. The finding's 1,497 was correct when written — `git show 9eb8bc7:scripts/skill-size-check.sh` is 1,497 lines, and `9eb8bc7` (2026-09-10) is this audit's own first commit. It went stale two days *after*, at `c8a7c9e` (2026-09-12), the commit that folded `skill-frontmatter` in — which is why the finding's "frontmatter present" target is now work already done, not why its number was wrong. agent-audit's `validate.sh` is **1,738**, a superset, not a 1,677-line port. `vale-wrap.sh` 526 × 2 is exact.
@@ -201,7 +207,7 @@ The shared pattern: per-skill `README.md` files no model reads, a `docs/research
>
> **The `vale-wrap` half is wrong on its conclusion.** `|` literal blocks do fix the folding case — the script says so and deliberately no-ops on them — but the wrapper handles **four** affected scalar forms (folded `>`, bare plain, double- and single-quoted continuation lines), and **277 of its 526 lines are argv handling unrelated to folding** (cwd-relative absolutization, the `is_builtin_output` guard, scratch-tree mirroring, path relativization), each with its own incident record. Decisively, `.pre-commit-hooks.yaml` exports these hooks to external consumer repos whose scalar style this repo cannot dictate. Converting the 40 in-repo descriptions to `|` is a fine independent change; **it does not shrink the wrapper.**
>
> Where the savings actually are: **merge skill-audit + agent-audit (finding 14) → −1,587 lines, zero coverage loss.** A second option — sourcing the resolver into `scripts/skill-size-check.sh` rather than embedding it (−1,061) — is technically possible but couples the root hook to plugin layout and dismantles the byte-identity contract test's design; needs a decision, not an assumption.
> Where the savings actually are: **merge skill-audit + agent-audit (finding 14) → ~~−1,587 lines~~ → landed 2026-09-15 at −2,934 lines, zero coverage loss.** A second option — sourcing the resolver into `scripts/skill-size-check.sh` rather than embedding it (−1,061) — is technically possible but couples the root hook to plugin layout and dismantles the byte-identity contract test's design; needs a decision, not an assumption.
17.**Fold `forge` and `apm-install`.**`forge` is a four-row routing table plus 207 lines of references explaining fork vs inline; it should be 25 lines with no references. `apm-install` (53 lines + 17-line sources) becomes a sixth dispatch row in `apm-workflow`. Effort S.
@@ -210,14 +216,15 @@ The shared pattern: per-skill `README.md` files no model reads, a `docs/research
>
> | Example | Location | What it actually is |
> |---|---|---|
> | "Code blocks carry a language tag" | `skill-audit/references/formatting-and-scripts.md:19` | An **audit criterion** |
> | "Code blocks carry a language tag" | ~~`skill-audit/references/formatting-and-scripts.md:19`~~ → `factory-audit/references/skill-formatting-and-scripts.md:19` | An **audit criterion** |
> | "Data to stdout, diagnostics to stderr" | same file, line 39 | An **audit criterion** |
> | "Valid characters: lowercase letters…" | `skill-author/assets/templates/SKILL.md:7` | A **scaffold comment** emitted into new skills by `new-skill.sh`; never loaded as model context |
> | pipx / PEP 668 | `apm-install/SKILL.md:35-36`, `skill-author/references/scripts.md` | Generic tool prose — the only true positive |
>
> Three exemptions agreed, which is what re-scopes the finding:
>
> - **Audit criteria are exempt.** `body-discipline.md:14` frames the rule as "Would the agent get this wrong without this instruction?" — an auditor *would*, because the criterion is what it reports against. Cutting criteria is a redesign of what `skill-audit` checks, which belongs with finding 14.
> - **Audit criteria are exempt.** ~~`body-discipline.md:14`~~ → `factory-audit/references/skill-body-discipline.md:14` frames the rule as "Would the agent get this wrong without this instruction?" — an auditor *would*, because the criterion is what it reports against. Cutting criteria is a redesign of what ~~`skill-audit`~~ → `factory-audit` checks, which belongs with finding 14.
> > **Repointed (2026-09-16, at HEAD):** ADR-0025's merge both moved the directory and renamed the file (`references/formatting-and-scripts.md` → `references/skill-formatting-and-scripts.md`), so the two citations above were doubly stale. Line 19 and line 39 still land on the two criteria named, and `skill-body-discipline.md:14` still carries the core test — verified with `sed -n`. Per finding 14's note, finding 18 **stays open against `factory-audit`'s files**, so these are repointed, not struck.
> - **`assets/templates/` is exempt.** Scaffold output, not context.
> - **Sourced restatement of a spec this repo's own artifacts are built to is exempt.** `skill-author/references/scripts.md` carries `source_keys: agentskills-using-scripts` and deliberately restates the agentskills.io spec — the contract every skill here is written against, so the restatement governs this repo's artifacts and has to be in front of the author. **`source_keys:` alone is not the test**, and cannot be: `conventional-commits-spec.md` and `bisect.md` both carry it too, and finding 20 recommends reducing both to a pointer plus the house delta. The decidable line is what the content governs — a spec this repo's artifacts must satisfy (agentskills.io) is exempt; documentation of an external tool the model already has (Conventional Commits, `git bisect`) is not. Grounding, stated honestly: findings 9 and 26 closed as "Keep — vendored upstream content is intentional", but both closed over the `docs/research/` and `docs/notes/` *directories*, not over skill `references/*.md`; extending them to `scripts.md` is this note's inference, not a recorded decision. (An earlier revision added "finding 11 re-decides this content's status anyway" — withdrawn: finding 11 proposes dropping the provenance *metadata and validators*, not the sourced prose.)
>
@@ -225,7 +232,7 @@ The shared pattern: per-skill `README.md` files no model reads, a `docs/research
19.**Delete the two router skills and two orchestrate agents (309 lines + 195 reference lines).** No skill invokes them as a step; they appear only in boundary clauses (`AGENTS.md`, `git-worktrees`, `gitea-issues`, `gitea-prs`) and as worked examples in agent-audit references, all of which must change in the same commit or `skill-size-check` fails on the dangling target. Claude Code already routes on descriptions. The chain today is `git-workflow` step 5 invokes `git-orchestrate`, whose step 5 invokes `git-commits`, which runs `rtk git commit`: three hops. Both agents exceed 900 words; ADR-0020 deliberately sets no agent body gate. Effort S.
19.**Delete the two router skills and two orchestrate agents (309 lines + 195 reference lines).** No skill invokes them as a step; they appear only in boundary clauses (`AGENTS.md`, `git-worktrees`, `gitea-issues`, `gitea-prs`) and as worked examples in ~~agent-audit references~~ → **`factory-audit`'s `references/agent-body-and-delegation.md` and `references/agent-description-quality.md`** (repointed 2026-09-16 at HEAD; ADR-0025 moved them), all of which must change in the same commit or `skill-size-check` fails on the dangling target. Claude Code already routes on descriptions. The chain today is `git-workflow` step 5 invokes `git-orchestrate`, whose step 5 invokes `git-commits`, which runs `rtk git commit`: three hops. Both agents exceed 900 words; ADR-0020 deliberately sets no agent body gate. Effort S.
> **Not proceeding (2026-09-13):** premise doesn't hold. There are no separate "router skills" — only two `.agent.md` files. `git-orchestrate` is not a dangling boundary-clause reference; it's `git-workflow` step 5's actual execution backend (documented both directions), so deleting it breaks `git-workflow`'s only execution path rather than tidying an orphan. `gitea-orchestrate` is intentional per ADR-0011 (agent-facing counterpart for agent callers) even though `gitea-workflow` doesn't call it. A third, undocumented instance of the same pattern (`apm-orchestrate`) exists and isn't addressed by this finding. The four boundary-clause locations named above don't actually reference either agent. No changes made. This needs the "short discussion" §7 bucket 2 implies, not a mechanical delete.
20.**Collapse git 7 skills to 1; gitea 7 to 2.** Git references are man-page restatement: `git-log-format.md` (242 lines listing `%H`, `%ar`), `conventional-commits-spec.md` (170 lines), `worktrees.md` (178), `merging.md` explaining fast-forward. Roughly 60% of the plugin is generic. The genuinely house-specific content fits in about 150 lines: the `rtk` rule and ADR-0023 exceptions, main/master refusal, `--no-verify`, the `-i --autosquash` 2.39.5 trap, `--force-with-lease --force-if-includes`, bisect exit codes, submodule push ordering, the detached-HEAD worktree trap. Gitea is more legitimately specific (MCP schema quirks: `tree_sha`, `withLines`, silent drops on PR create, `per_page` 20 vs 30, 404 means 403) and splits naturally into `gitea-tracker` (issues, PRs, labels, milestones) and `gitea-repo` (branches, files, releases). Risk: one description must carry all trigger phrases; keep a dispatch table at the top of the body. Keep `pc-author` and `pc-run` (finding 38). Effort M.
@@ -349,15 +356,18 @@ The shared pattern: per-skill `README.md` files no model reads, a `docs/research
Not covered by the area audits above; found on a final sweep of the root config and install pipeline. The install pipeline itself (`scripts/install.sh` 55 lines, `deploy-manifest.sh` 24, statusline 109) is fine and needs nothing.
33.**Every plugin version lives in four places (five for kyberforge), plus one per skill.**`plugins/<name>/apm.yml`, two generated `plugin.json` files, the root `apm.yml` packages list, the `executables.allow` key (`kyberforge#1.6.2`), and a `metadata.version` in all 39 SKILL.md files (ADR-0022) that nothing consumes and that drifts freely (gitea skills sit at five different values). Repo tags (`v2.0.1`) follow a third scheme that the declared `tagPattern: v{version}` can never match under `per_package` versioning. ADR-0006, ADR-0022, `check-executables-allow-sync`, `skill-frontmatter`, and `apm pack --check-versions` all exist to police this. Proposal: one version per plugin in its `apm.yml`; drop `metadata.version` and ADR-0022; let `apm pack` derive the rest. Effort M.
> **Partially advanced (2026-09-14):** see commit `718c79a` on `docs/simplification-audit`. Two of the four locations per plugin are gone: the twelve generated `plugin.json` manifests (`plugins/*/.claude-plugin/` and `plugins/*/.github/plugin/`) were deleted with the mirror. ADR-0006 needed no action — it was already moot and governed only those two now-deleted manifests, so no version bumps were required by the change. **Not closed.** Still outstanding: `plugins/<name>/apm.yml`, the root `apm.yml` packages list, the `executables.allow` pin, and `metadata.version` in all 39 SKILL.md files (still unconsumed, still drifting), plus ADR-0022 and the `v{version}` `tagPattern` mismatch.
33.**Every plugin version lives in four places (five for kyberforge), plus one per skill.**`plugins/<name>/apm.yml`, two generated `plugin.json` files, the root `apm.yml` packages list, the `executables.allow` key (`kyberforge#1.6.2`), and a `metadata.version` in all ~~39~~ → **38** SKILL.md files (ADR-0022) that nothing consumes and that drifts freely (gitea skills sit at five different values). Repo tags (`v2.0.1`) follow a third scheme that the declared `tagPattern: v{version}` can never match under `per_package` versioning. ADR-0006, ADR-0022, `check-executables-allow-sync`, `skill-frontmatter`, and `apm pack --check-versions` all exist to police this. Proposal: one version per plugin in its `apm.yml`; drop `metadata.version` and ADR-0022; let `apm pack` derive the rest. Effort M.
> **Partially advanced (2026-09-14):** see commit `718c79a` on `docs/simplification-audit`. Two of the four locations per plugin are gone: the twelve generated `plugin.json` manifests (`plugins/*/.claude-plugin/` and `plugins/*/.github/plugin/`) were deleted with the mirror. ADR-0006 needed no action — it was already moot and governed only those two now-deleted manifests, so no version bumps were required by the change. **Not closed.** Still outstanding: `plugins/<name>/apm.yml`, the root `apm.yml` packages list, the `executables.allow` pin, and `metadata.version` in all ~~39~~ → **38** SKILL.md files (still unconsumed, still drifting), plus ADR-0022 and the `v{version}` `tagPattern` mismatch.
> **Verified (2026-09-14, at HEAD `062ca47`): headline wrong, central claim inverted — and it contains the one zero-risk, empirically-verified win in this audit.**
>
> **Do this regardless of anything else: delete the six root `apm.yml` `packages[].version` lines.** Tested in an isolated scratch copy (repo untouched): setting `plugins/lint/apm.yml` to `9.9.9` while root says `1.1.7` **passes `apm pack --check-versions --check-clean` with exit 0**, reports `[matches]`, and emits `1.1.7` — the curator entry wins (`output_mappers.py:163-171`). Deleting the root `version:` line entirely leaves `marketplace.json` **byte-unchanged** (`builder._fetch_local_metadata` reads the plugin's own `apm.yml`). All six are removable with zero output diff. This is unpoliced duplication that silently ships the wrong number on drift. Effort S, no decision needed.
>
> Corrected headline: **two** hand-maintained per-plugin locations (**three** for kyberforge), not four — the audit's own "already done" note records the `plugin.json` deletion but never fixed the headline. Gitea skills drift across **six** values (`0.1.2, 0.1.3, 0.1.4, 0.1.5, 0.1.6, 1.0.1`), not five. 39 `SKILL.md` files ✓. The `0.4.6` duplication between root `version:` and `marketplace.version:` is **forced by apm, not a repo choice** — deleting `marketplace.version` makes `--check-clean` go dirty.
> Corrected headline: **two** hand-maintained per-plugin locations (**three** for kyberforge), not four — the audit's own "already done" note records the `plugin.json` deletion but never fixed the headline. Gitea skills drift across **six** values (`0.1.2, 0.1.3, 0.1.4, 0.1.5, 0.1.6, 1.0.1`), not five — still six at HEAD on 2026-09-16, re-derived by parsing `metadata.version` out of each `plugins/gitea/.apm/skills/*/SKILL.md`. ~~39 `SKILL.md` files ✓~~ → **38** carry it, and all 38 do (re-measured 2026-09-16; ADR-0025's merge took one). The `0.4.6` duplication between root `version:` and `marketplace.version:` is **forced by apm, not a repo choice** — deleting `marketplace.version` makes `--check-clean` go dirty.
>
> **"Nothing consumes `metadata.version`" is false twice over.** Machine enforcers: `scripts/skill-size-check.sh:1365-1374` and `skill-audit/scripts/validate.sh:1292-1332`, both FAIL tier, the latter citing ADR-0022 by name, with four dedicated bats cases and ~10 fixture generators baking the field in. Instruction-level consumers: `skill-author/SKILL.md:60` (bump minor on create, patch on improve), `create.md:89,101`, `improve.md:82`, and `forge/SKILL.md:54` + `references/version-bump.md`. apm parses it for Chatmode/Instruction/Context primitives but not for Skills, and never emits it. Precise statement: the value is written, shape-validated, and never read *downstream* — it is an agent-visible revision counter, and the drift table shows the counter is not being maintained.
> **"Nothing consumes `metadata.version`" is false twice over.** Machine enforcers: ~~`scripts/skill-size-check.sh:1365-1374`~~ → `scripts/skill-size-check.sh:1370-1379` and ~~`skill-audit/scripts/validate.sh:1292-1332`~~ → `plugins/kyberforge/.apm/skills/factory-audit/scripts/lib-checks-skill.sh:235-277`, both FAIL tier, the latter citing ADR-0022 by name, with four dedicated bats cases and ~10 fixture generators baking the field in.
> Instruction-level consumers: `skill-author/SKILL.md:60` (bump minor on create, patch on improve), `create.md:89,101`, `improve.md:82`, and `forge/SKILL.md:54` + `references/version-bump.md`. apm parses it for Chatmode/Instruction/Context primitives but not for Skills, and never emits it. Precise statement: the value is written, shape-validated, and never read *downstream* — it is an agent-visible revision counter, and the drift table shows the counter is not being maintained.
>
> > **Repointed (2026-09-16, at HEAD):** `skill-audit/scripts/validate.sh` no longer exists — ADR-0025's merge moved the ADR-0022 check into `factory-audit`'s skill-side check library, where it is the `SEMVER_RE` block (comment header at `:235`, `fail()` calls at `:261` and `:275`). `skill-size-check.sh` grew by 5 lines above the block since `062ca47`, hence the shifted range there. All five instruction-level citations still resolve at HEAD, verified with `sed -n`.
>
> **ADR-0022 already considered and rejected dropping the field**, on the grounds that `skill-author` depends on it to decide whether a pass owes a bump — a rationale still live today. Superseding costs: rewrite skill-author's bump rule, delete `forge`'s version-bump route premise, strip two scripts, delete four bats cases, fix ~10 fixture generators, edit the scaffold template, update `gates.md:97` — and re-open the "is this field present here?" question issue #127 closed, just from the other side. **Recommendation: keep it and fix the actual defect, which is that nobody bumps it.** Either enforce the bump in the skill-author workflow or declare the values advisory in the ADR.
>
@@ -416,8 +426,10 @@ Not covered by the area audits above; found on a final sweep of the root config
## 7. Suggested order
1. Quick wins, all S, no design decisions needed: findings 9, 10, 26, 30, 31, 29, 12, 13, 1, 6, 4, 35, 37, 38, and the mirror-sync and executables-allow halves of 2. Removes roughly 25,000 to 30,000 lines and 6 hooks.
2. Structural changes that need a short discussion: 14, 15, 19, 20, 23, 25, 17, 3, 5, 7, 33, 34, 36.
3. The real complexity: 16 (validators), 11 (provenance), 24 (core), 8 and 28 (gates.md and ADRs).
2. Structural changes that need a short discussion: ~~14~~, 15, ~~19~~, 20, 23, 25, 17, ~~3~~, 5, ~~7~~, 33, ~~34~~, 36.
3. The real complexity: ~~16 (validators)~~, 11 (provenance), ~~24 (core)~~, 8 and 28 (gates.md and ADRs).
> **Re-derived (2026-09-16, at HEAD):** this ordering was written before the findings were worked, and seven of its entries are now closed. Struck above: **14** landed (`467bbd7`, ADR-0025); **7** was superseded then done (`718c79a`); **3** and **19** are not proceeding on refuted premises; **34**, **16** and **24** are refuted outright. **5** is left standing but is downstream of 16 by its own note, so it cannot be taken in this bucket's order. Read each finding's own marker, not this list — it is a plan of record, not a status board. Bucket 1 is left as written: every entry in it is marked `[x]` or carries a decision note at its own finding.
Findings 9, 10, 11, and 12 are coupled through the provenance validator and the audit criteria; land them together or the audit gates start reporting the removals.
@@ -436,6 +448,7 @@ Findings 9, 10, 11, and 12 are coupled through the provenance validator and the
- **Provenance chain.** Is "which upstream informed this file" a requirement you still want, or was it a governance experiment? Finding 11 hinges on this.
> **Sharpened (2026-09-14):** still open, but ask it of the **producer** first. `plugins/bin/.apm/skills/research/` specifies the `sources.md` + `source_keys:` format and three evals in `plugins/bin/evals/research/research/eval.yaml` assert it. If `research` keeps emitting the chain, finding 11 collapses to "delete the validators" and the metadata stays. See finding 11's verification note.
- **ADR-0012 (three core skills) and the one-script-per-skill install constraint.** ~~The merges in 14, 15, and 24 need the first revisited and are the only way around the second.~~**Corrected (2026-09-14):** this grouping was wrong, and finding 2b's note has said so since `0dffff3` while this bullet said the opposite. ADR-0012 governs only the `core` plugin's three `agentsmd-*` skills (`agentsmd-author`, `agentsmd-audit`, `provider-adapter-author`) — read it: it names those three and nothing else. **Only finding 24 touches them, so only finding 24 needs ADR-0012 revisited.** Findings 14 and 15 merge kyberforge's `skill-audit`/`agent-audit` and `skill-author`/`agent-author`, which ADR-0012 does not govern; what constrains them is the self-containment rule, and merging is the way *around* it rather than a reason to reverse anything. That rule survives ADR-0024 — see §9's negative result and ADR-0024 consequence 6, which also correct its source: it is the agentskills.io spec for APM package mode, not a property of Claude Code's plugin cache-install as finding 2b's note assumed. The open question for 14/15 is a design one — one `description` carrying both skills' trigger phrases — not an ADR supersession. Are you open to superseding ADR-0012, for finding 24?
> **Closed on the 14/15 half (2026-09-16, at HEAD):** finding 14 landed as `factory-audit` on 2026-09-15 (`467bbd7`, ADR-0025), and the design question this bullet holds open was answered by doing it — the merged description ships at 241 characters, inside the 250 SUGGESTION target, and the binding ceiling turned out to be the 900-word **body**, solved with a dispatch body over `skill-*`/`agent-*` reference files. See finding 14's own note. What remains open here is finding 15 (`skill-author` + `agent-author`) and the ADR-0012 question below, which the next note already answers.
> **Moot (2026-09-14):** finding 24 is refuted on arithmetic before this question is reached — the three `core` bodies total 1,360 words against `BODY_MAX_WORDS=900`, and their descriptions 806 chars against a 400 cap. Nothing needs superseding because the merge it would unblock cannot be committed. Question closed unless finding 24 is rewritten.
- **Granularity of git/gitea skills.** One `git` skill vs seven trades routing precision for size. Is one broad description acceptable?
> **Answered by measurement (2026-09-14): no, and it is not a preference question.** A merged git description measures **1,950 chars against a 400-char FAIL ceiling (4.9×)** and a 3,381-word body against 900 (3.8×). Both proposed gitea halves also FAIL at 2.5×, and the gitea split additionally puts a hard boundary through the edit-a-file-then-open-a-PR workflow. (An earlier revision also called the gitea split "blocked by ADR-0011, which already rejected a *smaller* bundling" — withdrawn; ADR-0011's objection is to a boundary being crossed, not to bundle size. See finding 20's verification note.)
@@ -499,7 +512,7 @@ Three findings (16, 24, 34) are refuted outright; two (22, 28) contain a real fi
The recurring failure mode is worth naming, because it has now produced six wrong findings (3, 5, 16, 22, 24, and by implication 28): **dense validator and test code with heavy comments reads as over-engineering when skimmed, and turns out to be regression coverage whose comments name the incident.** Findings 16 and 24 propose reverting validators to sizes whose defects are on the commit record. Before proposing to cut any script or suite in this repo, read its header.
**Where the real remaining opportunity is:** finding 14 (merge `skill-audit` + `agent-audit`) at **−1,587 lines with zero coverage loss**, which is also where finding 16's savings actually live. Its blocker is the design question in §8 — one `description` carrying both skills' trigger phrases — not an ADR supersession.
**~~Where the real remaining opportunity is:~~ Taken.** Finding 14 (merge `skill-audit` + `agent-audit`) at ~~**−1,587 lines with zero coverage loss**~~ → **−2,934 lines**, which is also where finding 16's savings actually live. ~~Its blocker is the design question in §8 — one `description` carrying both skills' trigger phrases — not an ADR supersession.~~ That blocker was a non-issue; see the note below.
> **Executed, and one knock-on claim corrected (2026-09-15).** Finding 14 landed as `factory-audit` (ADR-0025); yield **2,934 lines and one pre-push hook**, and the §8 blocker turned out to be a non-issue. The body was the binding ceiling, not the description, which ships at 241 characters, under the 250 target, once a duplicated trigger register was removed. See finding 14's own note for the corrections.
# agent-audit takes a single file path and derives the counterpart by scope detection
**Skill renamed per ADR-0025 (2026-09-15):**`agent-audit` merged with `skill-audit` into
**Partially superseded by ADR-0025 (2026-09-15).**`agent-audit` merged with `skill-audit` into
`factory-audit`. Read `agent-audit` below as `factory-audit`'s agent flow. The single-file
invocation contract this ADR sets survives the merge intact — `factory-audit` dispatches at Step 0
on the target path, and an `*.agent.md` or a path under `.apm/agents/` takes the agent flow, so the
caller still names one file and the script still derives the rest.
invocation contract this ADR sets survives — `factory-audit` dispatches at Step 0 on the target
path, so the caller still names one file and the script still derives the rest — but the set of
paths that contract accepts is **narrower**. The agent flow is taken by an `*.agent.md` file, or by
a `.md` file whose *immediate* parent directory is named `agents/`, at any scope:
`.claude/agents/x.md` is accepted, `.apm/agents/sub/x.md` is refused. "A path under `.apm/agents/`"
is not the rule — ADR-0025 rejects that phrasing as both wider than the script and circular. An
agent `.md` outside an `agents/` directory was audited before the merge and now stops at exit 2,
the never-ran tier, because a single auto-detecting entry point classifies on the path alone and
detection never guesses.
ADR-0020 recorded the merge as reopening this ADR; that is the clause it reopened.
`agent-audit` validates agent definition file pairs (Claude Code `.md` + Copilot `.agent.md`). The skill accepts a path to either file and derives the counterpart using scope detection rather than requiring the caller to name both files or supply a root directory.
Three skills in the `core` plugin (`core`'s first active skills):
- **`agentsmd-author`** — creates/updates a target repo's `AGENTS.md`, including nested monorepo placement (nearest-file-wins). Closes out by invoking `agentsmd-audit` inline, mirroring the `skill-author`/`skill-audit` pattern (**skill renamed per ADR-0025, 2026-09-15:** `skill-audit` is now `factory-audit`'s skill flow; the author-then-audit pattern is unchanged). When it detects an existing provider-specific file (`CLAUDE.md`, etc.) with content that duplicates what AGENTS.md should own, it calls `provider-adapter-author` via skill composition.
- **`agentsmd-author`** — creates/updates a target repo's `AGENTS.md`, including nested monorepo placement (nearest-file-wins). Closes out by invoking `agentsmd-audit` inline, mirroring the `skill-author`/`skill-audit` pattern (**Amended by ADR-0025, 2026-09-15:** `skill-audit` was removed and its flow is now `factory-audit`'s skill flow; the author-then-audit pattern is unchanged). When it detects an existing provider-specific file (`CLAUDE.md`, etc.) with content that duplicates what AGENTS.md should own, it calls `provider-adapter-author` via skill composition.
- **`agentsmd-audit`** — a single combined pass checking three mandatory baselines against `AGENTS.md` only: secrets/credentials (governance.md hard prohibition), structural completeness (common-sections checklist from the agents.md spec), and accuracy/drift (do referenced commands/paths resolve against the repo). Never inspects provider adapter files.
- **`provider-adapter-author`** — detects and converts a provider-specific instruction file into a thin adapter that imports `AGENTS.md` (mirroring this repo's own two-tier `CLAUDE.md` pattern). Self-validates via its own bundled deterministic script (`scripts/validate-adapter.sh`) rather than a separate paired audit skill, since the check (import present, no duplicated headings, size threshold) is mechanical.
| body-only words (600 SUGGESTION / 900 FAIL) | skills | deterministic | `skill-size-check.sh`, `skill-audit/scripts/validate.sh` (now `factory-audit/scripts/validate.sh`, see ADR-0025) |
| 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 |
@@ -285,10 +287,10 @@ which tier each rule is in, because the failure this ADR is most exposed to is a
| 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` |
| dispatch at two or more mutually exclusive flows | skills | **auditor judgment** | `references/body-discipline.md` |
| 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
@@ -524,7 +526,8 @@ Upstream citations below are relative to
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` to cover shared prose so duplication persists but drift cannot.
`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.
@@ -142,7 +142,10 @@ exactly the shapes `scripts/validate.sh` detects:
the skill flow.
- A `*.agent.md` file, or a `.md` file whose *immediate* parent directory is `agents/`, takes the
agent flow.
- Anything else stops, runs no validator, and names the two accepted shapes.
- Anything else stops, runs no validator, and names the two accepted shapes.`validate.sh`
distinguishes **three** cases here and says which one applies — the path does not exist, it is a
directory with no `SKILL.md`, or it matches neither shape — so a typo is never reported as a
spec violation.
Putting the dispatch after any deterministic check would mean running the wrong validator first and
reading its output as a finding. An earlier revision of the body carried a two-row table with no
@@ -158,9 +161,12 @@ script and circular.
one of `scripts/lib-provenance-skill.sh` / `scripts/lib-provenance-agent.sh`.
Two things justify this shape. First, **self-containment binds between skills, not within one.** The
resolver had to be embedded verbatim in three copies because three skill directories cannot read
each other's files; two files inside one skill directory have no such problem. Sourcing is available
the moment the directory boundary between them disappears. Second, **a single auto-detecting entry
resolver had to be embedded verbatim in three copies because its three homes could not read each
other's files: two of them were the separate `skill-audit` and `agent-audit` directories, and the
third is `scripts/skill-size-check.sh` at the repo root, which is consumed through
`.pre-commit-hooks.yaml` and so can reach into no plugin path at all (point 6 below). Two files
inside one skill directory have no such problem. Sourcing is available the moment the directory
boundary between them disappears. Second, **a single auto-detecting entry
point makes a Step 0 misdispatch detectable.** The script re-detects the flow from the target, so
even after a misdispatch it runs the right checks and its finding tiers are correct. That alone does
not make the misdispatch self-correcting, and an earlier revision of this ADR wrongly said it did.
@@ -218,17 +224,31 @@ directory and agent file matched stdout, stderr and exit code.
**The entry points are not behaviour-neutral, and an earlier revision of this ADR said they were.**
Those differential runs used valid targets only, so they could not see that the new detection layer
changed what happens to *invalid* ones. Every change below is deliberate:
changed what happens to *invalid* ones.
**The exit tiers are what every row below is measured against**, so state them once: **0** is
audited and clean, **1** is audited and has findings — a target that is *present but unreadable*
counts here — and **2** is that **nothing was audited**. Exit 2 is the never-ran tier, so the flow
files report the section as unverified and quote the reason instead of reading the run as a verdict.
The pre-merge scripts did not hold that line: several never-ran conditions exited 1, which puts a
target that was never opened into the findings tier. The doctrine is now applied uniformly across
both entry points, and that is what most of the table is. Every change below is deliberate:
| Input | Pre-merge | Now |
|---|---|---|
| a missing path, a directory with no `SKILL.md`, a non-agent `.md` (e.g. `README.md`) | exit 1, or a mode-specific exit-2 message | **exit 2** with one generic "matches neither" Error/Why/Fix. Exit 2 is the never-ran tier, so the flow files report the section as unverified and quote the reason. |
| a missing path, of any shape | exit 1 — the `*.agent.md`, `agents/`-parent and `SKILL.md` branches classify on the *name*, so a typo'd path went to the validator and its absence came back as a FAIL | **exit 2**, with its own dedicated "does not exist" Error/Why/Fix. The test runs on the typed path *before* the `SKILL.md` → parent-directory rewrite, so a missing `docs/SKILL.md` is not silently tested as `docs`. |
| a dangling symlink, or a symlink loop | exit 1 | **unchanged: exit 1**, FAIL "could not be read". The guard in the row above deliberately stops short of these — `-L` rescues what `-e` rejects, because something *is* at that path and merely cannot be opened. Present-but-broken is a real finding, and reclassifying it as "does not exist" would send the reader after a typo instead of after the link. |
| a directory with no `SKILL.md` | a mode-specific exit-2 message | **exit 2**, with its own distinct message — a skill directory is identified by its `SKILL.md`, and an agent target is a file, never a directory. |
| anything else that matches neither shape (e.g. `README.md`) | exit 1, or a mode-specific exit-2 message | **exit 2**, the generic "matches neither a skill directory nor an agent file" Error/Why/Fix. |
| a `SKILL.md` file path | exit 1 or 2 (`…/SKILL.md/SKILL.md not found`, "not a directory") | **accepted**; its parent directory is audited |
| an agent `.md`*not* under an`agents/` directory (e.g. `~/drafts/my-agent.md`) | audited | **refused, exit 2**. Detection never guesses. No tracked file in this repo is affected. |
| an agent `.md`whose *immediate* parent directory is not`agents/` (e.g. `~/drafts/my-agent.md`, or `.apm/agents/sub/x.md`) | audited | **refused, exit 2**. Detection never guesses. No tracked file in this repo is affected. |
| a bare or `./`-relative agent filename, run from inside its `agents/` directory | audited | audited. The parent directory's name is read from the real path, not the typed string. |
| a `lib-*.sh` missing or unreadable, or the script directory unresolvable | did not apply (single file) | **exit 2** with Error/Why/Fix, never a raw bash error at exit 1, which is the real-findings tier |
| `CDPATH` exported | did not apply (no `cd`) | no effect. `SCRIPT_DIR` resolves with `CDPATH=''` and `cd -- … >/dev/null`. |
| no argument | `Error: skill-dir is required.` / `agent-file is required.` | one combined message and usage block; exit code unchanged (1 from `validate.sh`, 2 from `validate-provenance.sh`) |
| `python3` missing, or PyYAML not importable | exit 1 | **exit 2** with Error/Why/Fix, and the interpreter is checked separately from the library so the message names the right one. No interpreter means no check ran, which is the never-ran tier and not a finding about the target. `validate.sh`'s preflight is mode-specific and runs after detection, so it names the gates the *selected* flow would skip; `validate-provenance.sh` needs `python3` but not PyYAML, and already exited 2 for it. |
| `CDPATH` exported | `validate.sh`/`validate-provenance.sh`: did not apply (no `cd`). `vale-wrap.sh`: **broken** — a `CDPATH` entry with a `scripts/` child made `cd` print the directory it resolved, so `--config` was handed a two-line argument and vale died | no effect on any of the three entry scripts. Every `cd` now runs as `CDPATH='' cd -- … >/dev/null`. This is a fix in the wrapper, not a no-op: an earlier revision of this ADR implied all three were already safe. |
| no argument | `Error: skill-dir is required.` / `agent-file is required.`, at exit 1 from `validate.sh` and exit 2 from `validate-provenance.sh` | one combined message and usage block, and **exit 2 from both**. Nothing was audited, so the never-ran tier is the correct one; `validate.sh`'s exit 1 was the outlier and is corrected rather than carried across. |
| `--help` | exit 0 | exit 0, and from `validate-provenance.sh` it now works **even with a library missing**: the help text needs no library, so failing the preflight first made `--help` unusable exactly when a reader most needed the usage block. |
| an empty-string positional (`validate-provenance.sh ""`) | reported as "only flags were given" | reported accurately — an empty argument is an argument, and misnaming it sent the reader to the wrong fix. |
A single `validate.sh` copied or symlinked out of its `scripts/` directory still does not work,
because its libraries are not beside it. It now fails at exit 2 and says so.
@@ -237,10 +257,12 @@ because its libraries are not beside it. It now fails at exit 2 and says so.
**Keep two skills and rely on the byte-identity contract test alone (rejected).** This is the status
quo: `tests/test-adr0020-contract.sh` already hashes the resolver across copies, and
`check-vale-style-sync.sh` already diffs the Vale halves at pre-push. Only 6 of its 17 assertion
sites exist because there are two copies. The other 11 do other work, and are rehomed or ported above rather than being an
argument for the status quo. On the duplication itself it polices drift rather than removing the thing that drifts,
and it pays 2,934 lines plus a pre-push hook to do so. It also leaves
`check-vale-style-sync.sh` already diffs the Vale halves at pre-push. Only **2** of its 17
assertion sites actually diff the two copies, and four more exist solely so the script can locate
them — a real `REPO_ROOT`, non-stale `.apm/` paths, both copies present. The other 11 do other
work, and are rehomed or ported above rather than being an argument for the status quo. On the
duplication itself it polices drift rather than removing the thing that drifts, and it pays 2,934
lines plus a pre-push hook to do so. It also leaves
the router carrying a mutually-excluding near-miss pair whose two descriptions each spend a boundary
clause pointing at the other — a routing cost the merge removes for free. ADR-0020 already weighed
this option for this pair and chose merging; nothing measured since changes the balance.
@@ -259,8 +281,10 @@ today, and folding each into one file with a skill section and an agent section
outcome. It defeats the dispatch. The entire point of moving Steps 1-3 into `references/` is that an
invocation loads one flow's content and not the other's; a two-section reference file re-inflates
per-invocation context to the full 1,532-word span the body ceiling forced out. ADR-0020 measured
these same files at 100 of ~120 differing lines after normalising `skill`/`agent`, so the merged file
would also be mostly disjoint text under one heading.
exactly one of the three pairs — the two same-named `references/description-quality.md` files — at
100 of ~120 differing lines after normalising `skill`/`agent`. The other two pairs are unmeasured,
and the figure should not be read onto them; the one that was measured is enough to make the point,
because a merged file built out of it would be mostly disjoint text under one heading.
**Naming it `audit` (rejected).** Shortest available name and an accurate one. It collides with
`agentsmd-audit`, which audits a repo's `AGENTS.md` and has nothing to do with the factory. A bare
@@ -374,3 +398,37 @@ resetting to `0.1.0` would signal an immaturity that the merged content does not
above to Step 0, the gotchas and the description are an improve pass, so under `skill-author`'s
patch-bump rule it is now **`1.0.1`**. The plugin itself goes from `1.6.2` to **`2.0.0`**, because
removing two invocable skills breaks anyone calling them by name.
**Twelve earlier ADRs are amended, and the split between the ones that are narrowed and the ones
that only change a name is the load-bearing part.** A reader who takes "merged" to mean every
decision about either skill is reopened will re-litigate settled ground; a reader who takes it to
mean nothing changed will miss the one contract that did.
- **ADR-0008 is partially superseded** — its single-file invocation contract stands, but the set of
accepted target shapes is narrower. An agent `.md` whose *immediate* parent directory is not
`agents/` was audited pre-merge and is now refused at exit 2, because one auto-detecting entry
point has to classify on the path alone and detection never guesses. Scope detection, counterpart
derivation and the "name one file, the script derives the rest" signature are unaffected. ADR-0020
anticipated exactly this when it recorded that the merge "reopens ADR-0008".
- **ADR-0014 is partially superseded** — its two skill-scoped Vale copies, its split of `.vale.ini`
into a skill config and an agent config, and the `check-vale-style-sync.sh` pre-push gate that
diffed them, no longer apply. The reasoning is not reversed; the condition it operated on is gone.
Its "one hook per file-scope" finding is untouched, and is why both exported hook IDs survive.
- **ADR-0020 is partially superseded**, and only in the *count and location of the scripts* that
carry it. Every tier, threshold and verdict rule stands. "All three validators" is two, and the
mirrored constants live at one plugin path. Its deferred merge rule for this pair is discharged
here rather than superseded — that is what this ADR implements.
- **ADR-0021 is partially superseded** on that same count: "the three ADR-0020 validators" is two.
The rule it decides — a published description states a domain boundary and never enumerates the
skills behind it — is untouched.
- **ADR-0004 is not superseded.** FAIL/SUGGESTION/INFO, and the rule that INFO never moves the
pass/fail verdict, are carried into the skill flow unchanged. No audit criterion changes here.
- **ADR-0009 is not superseded.** Its mechanism — field lists read from a reference file at runtime
rather than hardcoded into script arrays, with a `source_keys` provenance chain back to the
research corpus — survives and is reused as-is. Only the filename moves, `field-inventory.md` to
`agent-field-inventory.md`, and that rename is byte-identical.
- **ADR-0010, ADR-0012, ADR-0013, ADR-0015, ADR-0016 and ADR-0018 are not superseded.** Each names
`skill-audit` or `agent-audit`, or a path beneath one, while deciding something else: a provenance
file's location, a plugin boundary, Vale rule scope, an allowlist's home, a namespacing example.
Every one of those references resolves through `factory-audit` now, and no claim any of them makes
moves. They carry pointer annotations rather than amendments for that reason.
`--silent`, `--max-count` or `--files-with`. (The last two match the regex and were missing from
this list.)
- **Intermediate stages are seen.** `echo x | filter | grep -q y` is a site; only the two-stage form
used to be.
- **Both line continuations are joined** before matching: a trailing backslash, and a trailing `|`,
which is equally legal in a pipeline. The hit is reported at the line the command starts on.
- **`pipefail` may sit anywhere in a `set` line**, so `set -o errexit -o pipefail` arms the file. It
previously had to follow the *first*`-o`, and a file-level miss skips every site in that file
rather than one — which is why that test is deliberately loose.
- **A bare `&` ends a segment**, so `echo ok && other | grep -q x` — whose writer is `other`, not the
`echo` — is not a site. `&` followed by a digit is kept, so `2>&1` does not end one.
The widening turned up **five live vulnerable sites in `tests/test-apm-current-hook.sh`** that the
narrower scanner never saw while the suite reported green; all five are converted, and the scanner
now reports zero over the tree. Its fixtures went from 4 vulnerable spellings to **12**, plus
near-miss negatives it must leave alone, and the suite from 5 cases to **7**.
**Remit: early-exiting GREP readers only.**`head`, `sed -n 1p` and a bare `read` exit early too, and
an `echo` or `printf` feeding any of them is the same race. Those are guarded by **convention** —
absorb the writer's status with `|| true`, or take the verdict from a here-string — and deliberately
not by this test: most legitimate uses of them in this tree are already absorbed, and the scanner
cannot see absorption from the pipeline text alone, so flagging them would be noise. Two live
`grep … | head -1` sites (`tests/test-vale-wrap.sh:620` and `:1046`) were fixed by hand with that
idiom. Pipes from a non-builtin writer (`run_wrap … | grep -q`) are out of scope for the same reason:
in practice they either absorb the writer's exit status with `|| true` or write only once, at exit.
**Known limitation: heredoc bodies are scanned as code.** A `cat <<'EOF'` body containing a
vulnerable-looking line would be reported as a real site. There are none in the tree today.
`tests/run-bats.sh` derives the set of `.bats` files it expects from `git ls-files`, so a `.bats`
file deleted from the worktree but still tracked in the index fails the run rather than silently
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.