diff --git a/CONTEXT.md b/CONTEXT.md index 58075d2..5bbbd08 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -30,10 +30,10 @@ _Avoid_: router body, thin body **Hand-invoked skill**: A skill reached only by typing its slash command, declared `disable-model-invocation: true`. The host -withholds it from the model-visible listing entirely, so it pays no preload tax and its description -becomes human-facing text. The flag also hard-blocks the Skill tool, so **no other skill can route to -a hand-invoked skill** — a `` Call `x` `` step in another skill's body stops working the moment `x` -takes the flag. Check inbound routes before declaring one. Exemplar: `zoom-out`. +withholds it from the model-visible listing entirely, so it costs nothing in always-on context and +its description becomes human-facing text. The flag also hard-blocks the Skill tool, so **no other +skill can route to a hand-invoked skill** — a `` Call `x` `` step in another skill's body stops +working the moment `x` takes the flag. Check inbound routes before declaring one. Exemplar: `zoom-out`. _Avoid_: manual skill, disabled skill **Delegation discipline**: @@ -155,9 +155,9 @@ _Avoid_: namespace, category > **Dev:** "This one only fires when someone types the slash command. Does its description still need > trigger words?" > **Maintainer:** "No — that's a **hand-invoked skill**. The host withholds it from the model-visible -> listing, so it pays no **preload tax** at all and the description is human-facing text." +> listing, so it costs nothing in always-on context and the description is human-facing text." > **Dev:** "Then the body can be as long as it needs to be?" -> **Maintainer:** "Different budget. The **skill context contract** gates the body whether or not the +> **Maintainer:** "Different budget. ADR-0020's authoring rules gate the body whether or not the > skill is model-invoked — the description competes with every other skill's description, the body > competes with the caller's live conversation. Four mutually exclusive flows means a **dispatch > body**: table in `SKILL.md`, one `references/` file per flow." diff --git a/docs/adr/0016-apm-agent-primitive-drops-provider-specific-fields.md b/docs/adr/0016-apm-agent-primitive-drops-provider-specific-fields.md index 86af1a9..8716154 100644 --- a/docs/adr/0016-apm-agent-primitive-drops-provider-specific-fields.md +++ b/docs/adr/0016-apm-agent-primitive-drops-provider-specific-fields.md @@ -1,5 +1,13 @@ # Plugin-scope agent-author omits `tools:` and all Claude-only fields from `.apm/agents/*.agent.md` +**Amended by ADR-0025 (2026-09-15).** `agent-audit` was removed and its flow merged with +`skill-audit`'s into `factory-audit`, which dispatches to a skill flow and an agent flow at Step 0. +Read `agent-audit` below as `factory-audit`'s agent flow, and `validate.sh` as that flow's +validator. The decision is unchanged — plugin-scope `.apm/agents/*.agent.md` still carries only the +allowlisted fields, and the allowlist is still read as data from a reference file, now +`factory-audit/references/agent-field-inventory.md`. The present-tense skill names below are +updated accordingly. + This ADR is a narrower, downstream consequence discovered while designing issue #89's implementation under ADR-0015's broader direction (Microsoft APM replaces hand-authored plugin/marketplace authoring). It does not restate ADR-0015's rationale — see that ADR for @@ -47,8 +55,8 @@ Absent `tools:` means inherit-all-tools on both harnesses — the one value that on either target, unlike a present, harness-specific value that is guaranteed wrong on at least one of them. -`agent-audit`, at plugin scope, is intended to flag — as a **SUGGESTION**, not a FAIL, since -this is an upstream schema limitation rather than an authoring mistake — any agent whose +`factory-audit`'s agent flow, at plugin scope, is intended to flag — as a **SUGGESTION**, not a +FAIL, since this is an upstream schema limitation rather than an authoring mistake — any agent whose description or body implies a need for tool restriction or a Claude-only behavior the frontmatter can no longer express. This would give visibility into the gap without pretending the schema can do something it can't. **Not yet implemented**: `check_apm_agent_file()` in @@ -71,7 +79,7 @@ write Claude's space-separated `tools:` string. Rejected because it ships a valu silently wrong (or possibly a hard error) on Copilot, and which harness "wins" would be an arbitrary, undocumented asymmetry. -**Same as above, but `agent-audit` flags the cross-harness breakage as a tracked finding +**Same as above, but `factory-audit` flags the cross-harness breakage as a tracked finding (rejected).** Rejected for the same core reason — it still ships a wrong value to a real harness. Tracking the breakage doesn't prevent it, and the chosen decision already gets equivalent visibility (a SUGGESTION finding) without ever shipping the wrong value in the first @@ -141,7 +149,7 @@ admitted as the portable-by-construction half of what was lost. It restores a re confirmed write fence against the tool-call path, not a complete write sandbox. The consequence below is narrowed accordingly. -Enforcement follows the decision: `agent-audit`'s plugin-scope validator reads its allowlist as +Enforcement follows the decision: `factory-audit`'s plugin-scope validator reads its allowlist as data from the `apm-agent-allowlist` section of `plugins/kyberforge/.apm/skills/agent-audit/references/field-inventory.md` (now `factory-audit/references/agent-field-inventory.md`, see ADR-0025), and that line now reads @@ -161,9 +169,9 @@ whether a field is safe under verbatim copy in a single vendor-neutral file. Plugin scope is now "directory containing `apm.yml` → single vendor-neutral file lands in `/.apm/agents/`." Project and user scope, and the rest of ADR-0005, are unaffected. - **ADR-0008 is partially superseded** — its counterpart-derivation/pair-validation mechanism - no longer applies at plugin scope; `agent-audit` takes the single file directly there. Project + no longer applies at plugin scope; `factory-audit` takes the single file directly there. Project and user scope, where a real pair still exists, are unaffected. -- **ADR-0009 is not superseded.** The mechanism it established — `agent-audit` reading field +- **ADR-0009 is not superseded.** The mechanism it established — `factory-audit` reading field lists from `references/field-inventory.md` (now `factory-audit/references/agent-field-inventory.md`, see ADR-0025) rather than hardcoding them, with a `source_keys` diff --git a/docs/adr/0019-session-start-hook-keeps-the-apm-install-current.md b/docs/adr/0019-session-start-hook-keeps-the-apm-install-current.md index 9a0a576..09fc8fc 100644 --- a/docs/adr/0019-session-start-hook-keeps-the-apm-install-current.md +++ b/docs/adr/0019-session-start-hook-keeps-the-apm-install-current.md @@ -85,12 +85,13 @@ question is only what catches a missed edit. > does not silently stop the hook deploying. Whether apm behaved this way when this ADR was written > was not established. **The decision stands** — `scripts/check-executables-allow-sync.sh` is now > justified by this repo's own requirement that the key track `plugins/kyberforge/apm.yml`'s -> `version:`, not by an apm-level failure mode. `docs/spec/gates.md` carries the same correction. A comment in the `executables:` block is not enough: -this repo gates generated-content drift, marketplace mirror drift and vale style drift -deterministically, and a silent-staleness failure is strictly worse than any of them. So -`scripts/check-executables-allow-sync.sh` runs at pre-push, parsing `version:` out of -`plugins/kyberforge/apm.yml` and asserting root `apm.yml` carries the matching -`kyberforge#` key. The comment stays as the human-facing pointer; the hook is what +> `version:`, not by an apm-level failure mode. `docs/spec/gates.md` carries the same correction. + +A comment in the `executables:` block is not enough: this repo gates generated-content drift, +marketplace mirror drift and vale style drift deterministically, and a silent-staleness failure is +strictly worse than any of them. So `scripts/check-executables-allow-sync.sh` runs at pre-push, +parsing `version:` out of `plugins/kyberforge/apm.yml` and asserting root `apm.yml` carries the +matching `kyberforge#` key. The comment stays as the human-facing pointer; the hook is what actually holds. It parses with PyYAML where importable and falls back to a two-shape scan otherwise, so a missing pip package cannot become the thing that blocks every push. diff --git a/docs/adr/0024-apm-is-the-only-supported-install-path.md b/docs/adr/0024-apm-is-the-only-supported-install-path.md index 996d16f..8e4fdf7 100644 --- a/docs/adr/0024-apm-is-the-only-supported-install-path.md +++ b/docs/adr/0024-apm-is-the-only-supported-install-path.md @@ -13,6 +13,14 @@ ADR-0015. The root `marketplace:` block in `apm.yml` and the compiled `.claude-plugin/marketplace.json` it produces are **kept** — see "Also delete the marketplace catalogue" under considered options. +**Amended by ADR-0025 (2026-09-15).** The decision stands unchanged — apm is the only supported +install path, and `.apm/` still ships the per-skill `tests/` directories this ADR accepted as +dev-fixture leakage. What moved is **consequence 2's skill count**. `skill-audit` and `agent-audit` +merged into `factory-audit`, collapsing two `.bats`-carrying skill directories into one, so the same +10 `.bats` files now deploy across **5** skills, not the six counted here on 2026-09-14. The figure +below is corrected in place; "all six `apm.yml` files" in the same paragraph counts plugins, not +skills, and is unaffected. + ## Context ADR-0018 moved this repo's own consumption of its own plugins onto `apm install`. From that point @@ -167,7 +175,7 @@ README note is the only available mitigation, and a note is not a gate. **2. Consumers now receive dev-fixture files.** apm installs from `.apm/`, and `.apm/` contains the per-skill `tests/` directories the mirror explicitly stripped (ADR-0017's depth-scoped -`//tests` exclusion). 10 `.bats` files across 6 skills therefore now deploy into +`//tests` exclusion). 10 `.bats` files across 5 skills therefore now deploy into every consumer's skill directories. Suppressing them would mean switching all six `apm.yml` files from `includes: auto` to explicit include lists — and an explicit list that is wrong silently drops content, which is the same failure class ADR-0017 was written to fix. Trading a cosmetic problem for diff --git a/docs/notes/simplification-audit-2026-09.md b/docs/notes/simplification-audit-2026-09.md index a5aca28..32728ed 100644 --- a/docs/notes/simplification-audit-2026-09.md +++ b/docs/notes/simplification-audit-2026-09.md @@ -117,7 +117,8 @@ This is the area you named as hardest to understand and slowest. Root cause: mos - [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`~~ → `620f20b`)** — 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:`~~ → was ported as case 33, and case 33 was deleted with `.pre-commit-hooks.yaml` in `4de5b6b` (finding 36), so there is no second manifest left to agree with. 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. + > **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:`~~ → was ported as case 33, and case 33 was deleted with `.pre-commit-hooks.yaml` in `4de5b6b` (finding 36), so there is no second manifest left to agree with. 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` reports ~~`61 passed, 0 failed`~~ → ~~`63 passed, 0 failed`~~ → **`65 passed, 0 failed` (pinned at `1614bce`)** (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~~ → **14** 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. + > > **Re-measured and pinned (2026-09-20, at `1614bce`).** The two `test-vale-wrap.sh` counts in the note above were written as current readings rather than pinned to a commit, and both went stale when `ea119d8` added cases to that suite after this note. Measured here, not copied forward: `bash tests/test-vale-wrap.sh` → `Results: 65 passed, 0 failed`, exit 0; `env PATH=/usr/bin:/bin bash tests/test-vale-wrap.sh` → `Results: 14 passed, 0 failed`, exit 77. The struck 63 and 19 were correct for the commits they were taken at; no attempt is made here to attribute the 19 → 14 move, only to record the reading at `1614bce`. Take the counts from a run against a named commit, never from this note — that is the same reason §1 carries its "Pinned (2026-09-16, review round)" note. - [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. > @@ -164,6 +165,7 @@ This is the area you named as hardest to understand and slowest. Root cause: mos Pre-commit stays roughly as is minus `skill-frontmatter`, and minus `check-ast` once finding 9 removes the only `.py` files. ~~Tests 26 files to about 10 (12,400 to about 5,000 lines).~~ Keep bats and its three submodules; the 351 bats tests ship inside plugins and are the right tool there. ~~Do not port the bash suites to bats; delete them instead.~~ **Struck (2026-09-16, grill):** see finding 8's closing note — the suites are regression coverage (findings 3 and 5; finding 16 found the same of the validators they test). > **Re-measured (2026-09-14, at `a6434e0`):** the tests target was stated against the 2026-09-10 baseline and both its numbers are stale. `tests/` now holds **20 `test-*.sh` suites totalling 9,123 lines** (plus the two runners, 490). Six suites have gone since the baseline: `test-check-manifests.sh` (`e647f14`), `test-skill-frontmatter.sh` (`c8a7c9e`), `test-governance-layer.sh` and `test-instructions-and-docs.sh` (`5f9f2b3`), `test-sync-marketplace-mirror.sh` (`0dffff3`), `test-sync-plugin-content.sh` (`718c79a`). ~~Restated on the same basis the target is **20 files to about 10, 9,123 to about 5,000 lines**~~ — **struck (2026-09-16):** the target itself is withdrawn (see the struck sentence above); for the record, `tests/` holds **19** suites totalling **10,000** lines at `4b17703`, after `4de5b6b` deleted `test-check-release-needed.sh` and `test-vale-hooks-consumer.sh`. Finding 9's `check-ast` clause is moot anyway, since finding 9 is not proceeding. +> > **Corrected (2026-09-20, at `1614bce`) — the deletion tally is nine, not ~~six~~ → ~~eight~~.** The six named above plus the two the 2026-09-16 strike adds come to eight, and a ninth was never folded into the running tally: **`test-check-vale-style-sync.sh`**, removed by `620f20b` with the `factory-audit` merge (finding 14) — the same commit finding 2's bullet already credits for deleting that gate's hook and script. The full `main...HEAD` set is nine: `test-check-manifests.sh` (`e647f14`), `test-check-release-needed.sh` (`4de5b6b`), `test-check-vale-style-sync.sh` (`620f20b`), `test-governance-layer.sh` and `test-instructions-and-docs.sh` (`5f9f2b3`), `test-skill-frontmatter.sh` (`c8a7c9e`), `test-sync-marketplace-mirror.sh` (`0dffff3`), `test-sync-plugin-content.sh` (`718c79a`), `test-vale-hooks-consumer.sh` (`4de5b6b`). Method: `git diff --name-status main...HEAD -- tests/ | grep '^D'`. The pinned "19 suites at `4b17703`" is unaffected — `620f20b` precedes that commit, so the file count already reflected the deletion even though the tally did not. At `1614bce` `tests/` holds **19** `test-*.sh` suites totalling **10,897** lines. ## 4. Plugins @@ -328,6 +330,8 @@ The shared pattern: per-skill `README.md` files no model reads, a `docs/research > **The self-containment constraint does not support this finding the way it supports 14/15** — there is no cross-skill duplication here to merge away. `agentsmd-audit`'s three scripts share essentially nothing with `validate-adapter.sh` (no `read_text`, no BOM handling, no NUL check; they exit 1 on usage errors). Merging would *expose* that they are unhardened — costing lines, not saving them. > > Two further blockers if it were ever revisited: the merge dissolves `agentsmd-author`'s standing prohibition *"Never write to a provider file yourself, in any circumstance"* (SKILL.md:21), a hazard `c59e4bf` closed after the validator's own size-FAIL remediation text "actively invited the prohibited edit"; and `skill-size-check.sh:121` + `tests/test-skill-size-check.sh:729` both cite `a8cd5e8`'s exit-2 split as precedent for their own, so deleting it orphans two live cross-references. + > + > > **Corrected (2026-09-20, at `1614bce`) — neither cross-reference points at `a8cd5e8` any more, and one line number was wrong when written.** `e4ed343` ("docs(gates): cite the reachable squash commit for the exit-2 split") repointed both at `598a7c3`, which `main` reaches. The citations now sit at `scripts/skill-size-check.sh:121` and `tests/test-skill-size-check.sh:737` — `:737`, not the `:729` above. `grep -rn a8cd5e8 scripts/ tests/` returns nothing. The blocker itself is unaffected: the exit-2 precedent still exists, under a hash a branch reaches. Same correction as §12's follow-up, closed there on the same date. 25. [x] **lint: delete the `lint-runner` agent.** Its body is "call `vale-run`, reformat output", which `--output=JSON` already gives; it exists for backends that do not exist. It is the example boundary clause in three `agent-author` templates and ADR-0016, so those need a new example. About 40% of `vale-config` is install tables and settings lists the model can fetch from vale.sh. Keep the house-verified matrices (`E100`/`E201`, `Packages` below glob, frontmatter, ignore paths). `lint/docs/research/docs/vale/` overlaps the skill's own references by about two thirds. Effort S. > **Decision (2026-09-16):** Not proceeding — the human declined this finding. The `lint-runner` agent stays. @@ -654,8 +658,8 @@ Seven parallel reviewers went over the whole branch against `main`, each coverin **Open follow-ups:** -- **Gitea #101.** Close it through this branch's PR with `Closes #101`. A comment is posted. +- **~~Gitea #101.~~ Closed — the instruction was already a no-op when written (2026-09-20).** ~~Close it through this branch's PR with `Closes #101`.~~ A comment is posted. Read back from the Gitea API on 2026-09-20, #101 is `"state": "closed"` with `"closed_at": "2026-09-16T16:03:35Z"` — closed on 2026-09-16, this §12 note's own date, so there is nothing left for a `Closes #101` trailer to do. No PR change needed. The comment on the issue stands. - **Gitea #66.** It needs re-scoping, because its `.mcp.json` target is gone. A comment is posted. - **The dropped `LESSONS.md` entry.** The entry saying that "read at session start" is only a hope was removed, and no issue tracks it. - **The ADR-0020 constants.** They could move into the shared library that `skill-size-check` now sources, which would remove the last duplicated copy. -- **The `a8cd5e8` citations.** `scripts/skill-size-check.sh` and `tests/test-skill-size-check.sh` still cite `a8cd5e8`, which no branch reaches. `598a7c3` is the reachable equivalent. +- **~~The `a8cd5e8` citations.~~ Closed (2026-09-20, by `e4ed343`).** ~~`scripts/skill-size-check.sh` and `tests/test-skill-size-check.sh` still cite `a8cd5e8`, which no branch reaches. `598a7c3` is the reachable equivalent.~~ `e4ed343` ("docs(gates): cite the reachable squash commit for the exit-2 split", 2026-09-16 15:39 UTC) landed after this follow-up was written and repointed both at `598a7c3`: `scripts/skill-size-check.sh:121` and `tests/test-skill-size-check.sh:737`. Verified at `1614bce` — `grep -rn a8cd5e8 scripts/ tests/` returns nothing. §4.4's finding 24 note carried the same stale citation (with `:729`, a wrong line number) and is corrected in place there.