Files
holocron/docs/adr/0025-skill-audit-and-agent-audit-merge-into-factory-audit.md
Defame1297 82b7bbcf5c docs: close the PR #135 documentation review findings
Group 3 of the validated PR #135 review fixes. Every figure and commit
citation below was re-verified at HEAD before being written.

ADR and architecture:
- #7 ADR-0025 cited 61b0b9c, which no published branch reaches. Repointed
  to 620f20b (identical parent tree, reachable from the PR branch), with a
  note that neither is reachable from origin/main. The parser-drift
  paragraph now credits 598a7c3 (the reachable PR #129 squash) and keeps
  484357a only as a pre-squash parenthetical.
- #8 architecture.md dropped the pointer at the LESSONS.md entry this
  branch deleted.
- #9 architecture.md's ADR entry points now name ADR-0015 (the one
  compiler) and ADR-0024, and list ADR-0024 as superseding ADR-0017.
- #10 ADR-0024 section 4 rewritten: the standing patch-bump rule is
  apm-workflow's configure.md, not ADR-0006's, and this change does not
  trigger it. ADR-0015:93 carries a correction for the misattribution.
- N5 ADR-0021 gained a Correction note for the deleted
  scripts/check-manifests.sh (e647f14).

gates.md:
- #11a the four ADR-0020 constants live in lib-checks-skill.sh:313-316 and
  lib-checks-agent.sh:164-165, not in validate.sh.
- #11b the pretty-format-json exclude is two alternations expanding to
  three tracked files, including .claude/apm-hooks.json.
- #11c the ADR-0020 contract suite runs 28 -> 27 -> 29 (620f20b,
  4de5b6b, ef27c97), 29 at HEAD; the unverifiable 25 is dropped.
- #11d the boundary resolver is one copy since ef27c97.
- #12 apm-audit-ci documents the 10 root checks and the 1 plugin check
  apm 0.28.0 actually runs, that content-integrity IS the hidden-Unicode
  scan, that manifest-parse is not a named check, and that the hook needs
  a completed apm install. The offline claim is qualified accordingly.
- N9 gates.md:142-146 verified to still match the hook description.

AGENTS.md:
- #12 the no-network session rule is qualified to a populated
  apm_modules/.

Audit note:
- A1 hook counts corrected to 27/9 -> 26/8 -> 27/9 -> 26/8 (26 and 8 at
  HEAD) and the dangling pointer dropped.
- A2 skill-size-check.sh is 509 lines with the resolver sourced, not 1,522
  embedded; citations repointed to skill-size-check.sh:323-335 and
  lib-checks-skill.sh:235-283 (fail() at :265 and :280), and that library
  is 627 lines.
- A3 consumers receive 15 test files across 5 skills; 16 tracked test
  paths repo-wide.
- A4 the "do not run apm update on this branch" instruction is marked
  superseded, with the branch-aware guidance in its place.
- Finding 31's "true orphans" claim corrected for HOTL and Sycophancy,
  both still used in core/ai-constitution.md.

Same class, found during group 2:
- skill-author's deployment-modes.md no longer points at .mcp.json
  configs (deleted in c96ca9c); metadata.version 1.0.2 -> 1.0.3.
- git-orchestrate's context contract clarifies that
  user_config_overrides is caller-supplied session state, not a config
  read. The field name is unchanged.
- B3 root apm.yml's executables.allow comment: grants are version-blind
  in apm 0.28.0, so the #2.0.0 suffix is cosmetic to apm and a bump does
  not break the hook; the suffix stays because
  check-executables-allow-sync.sh requires it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NwD8Egs5r4ndqeFLmhusX2
2026-09-19 21:30:29 +00:00

461 lines
36 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# `skill-audit` and `agent-audit` merge into one `factory-audit` with a Step 0 dispatch
**Status: accepted (2026-09-15).** Implements ADR-0020's "Merging siblings" rule, which named this
exact pair, scoped itself to them, and then deferred the work as issue #101. The deferral is closed
here. `skill-author` and `agent-author` stay separate — ADR-0020 excluded the author pair
deliberately, and nothing in this change touches that exclusion.
**Amended by ADR-0014 (2026-09-16).** The published `.pre-commit-hooks.yaml` is retired. Point 5
below (both exported hook IDs survive) and the case 33 port no longer describe the repo. Case 33 is
deleted, and its one-plugin narrowing guard is now a property of case 32. See
[ADR-0014's amendment](0014-vale-prefilter-ships-from-the-plugin.md#amendment-2026-09-16-the-external-hook-contract-is-retired).
**Amended (2026-09-16): the root hook sources the resolver.** Point 6 below records sourcing the
resolver into `scripts/skill-size-check.sh` as refuted, because the hook was consumed through
`.pre-commit-hooks.yaml`. That manifest is retired (above), so the reason no longer holds: the hook now
sources `lib-boundary-resolver.sh`, the repo holds one resolver copy, and the contract test's
assertion 1 pins that copy rather than hashing two. Assertion 1a's "exactly those two files" is now
exactly one. Point 6 is left as the record of the decision at the time.
## Context
Every figure below was measured against the worktree on 2026-09-15. Re-derive rather than quote; the
commands are given where a number is load-bearing.
The two skills duplicate content because they cannot share a file.
`plugins/kyberforge/.apm/skills/skill-author/references/deployment-modes.md`, sourced from the
agentskills.io spec, states the constraint for APM package mode: file references inside
`.apm/skills/<name>/` must not reach outside that skill's own directory, and the spec defines no
cross-skill sharing mechanism. apm deploys skills flat into `.claude/skills/<name>/` with no plugin
tier above them, so there is no directory a second skill could read from. ADR-0024 re-confirmed this
after deleting the native install path, specifically to stop the constraint being re-litigated as a
Claude-Code artifact. It is not one. It binds under the only install path that survives.
What that constraint costs, between these two skills:
| Duplicated artifact | Lines | Verification |
|---|---|---|
| ADR-0020 boundary resolver, embedded in both `validate.sh` copies | 1,061 | marker block `115..1175` (skill) and `189..1249` (agent); `tests/test-adr0020-contract.sh` assertion 1 hashes them |
| `scripts/vale-wrap.sh` | 526 | `diff -q` clean |
| `assets/vale/styles/Kyberforge/`, five rules | 44 | `diff -r` clean |
| Contributing-files parser, embedded in both `validate-provenance.sh` copies | 93 | marker block `300..392` (skill) and `294..386` (agent); `diff -q` clean on the extracted blocks |
| **Removable by merging** | **1,724** | |
On top of that, `scripts/check-vale-style-sync.sh` (413 lines) and
`tests/test-check-vale-style-sync.sh` (797 lines) go with the merge. That is **not because the whole
gate was a copy diff**; it was not, and saying so would overstate the case for deleting it. The
script has **17 assertion sites**: 13 `err` calls and 4 hard-fail exits. Its closing
`exit 1` only reports the `err` count, so it is not an assertion. Count them with
`git show 620f20b^:scripts/check-vale-style-sync.sh`. An earlier revision of this ADR said 18. No
reproducible counting rule gives 18, and it is corrected here.
> **Repointed (2026-09-19):** this ADR originally cited `61b0b9c^`. `61b0b9c` is a pre-squash commit
> that no published branch reaches, so the `git show` failed for anyone but its author. `620f20b` is
> the reachable squash of the same work on `docs/simplification-audit`, and `61b0b9c^` and `620f20b^`
> have identical trees (`git diff 61b0b9c^ 620f20b^` is empty), so every figure taken at the old
> parent reproduces at the new one. Note that `620f20b` is not reachable from `origin/main` either —
> fetch the PR branch (`git fetch origin docs/simplification-audit`) before running the command.
| Class | Old line | What it asserted | Now |
|---|---|---|---|
| **Moot (6)** | 19 | `REPO_ROOT` is a directory | nothing to guard; no script |
| | 42 | the `.apm/` paths are not stale | no copies to locate |
| | 52, 56 | neither copy is missing | one copy |
| | 60 | the two `vale-wrap.sh` copies are identical | one copy |
| | 64 | the two `styles/Kyberforge/` copies are identical | one copy |
| **Rehomed (10)** | 95, 113 | `.vale.ini` exists and is readable | case 0 |
| | 122 | `StylesPath = styles` is set | case 0 |
| | 127 | some section's `BasedOnStyles` names `Kyberforge` | case 28 (Part B proves it fails) |
| | 174 | no Kyberforge rule is overridden below a bare `YES`/`error` | case 31 |
| | 191 | `KyberforgeCopilot` ships and is loaded | case 30 |
| | 308 | `vale` is installed | suite-level: exit 77, which `run-tests --strict` fails |
| | 341 | every probe path matches some vale hook's `files:` regex | case 28 |
| | 347 | every probe path raises a Kyberforge alert under the config | cases 28 and 29 |
| | 397 | at least one probe row was checked | case 28's section floor |
| **Ported (1)** | 343 | local and published `files:` regexes agree per probe | case 33 |
Six are moot. Two diffed the copies, and four guarded the script's own ability to locate them: a
real `REPO_ROOT`, non-stale `.apm/` paths, and both copies present. With one copy and no script
there is nothing left to diff or locate. The other ten read `.vale.ini`, the style directory and
the hook manifests on their own terms, so they are **rehomed rather than retired**, into `tests/test-vale-wrap.sh`, which already owns the
wrapper's behaviour against this config.
Two rehomed checks got stronger, because a text grep became a behavioural Vale probe:
- line 127: case 28 Part B drops `Kyberforge` from a copy and requires vale to report the style as
not loaded;
- line 191: case 30 requires the Copilot rule to fire on `.agent.md` and nowhere else, and Part B
proves both an unload and a leak fail.
The rest moved at equal strength. Case 31 is the same grep as before. An earlier revision claimed
"7 of 11 stronger"; that claim is withdrawn.
**The cross-manifest check is ported, not dropped.** It extracts each vale hook's `files:` regex
from `.pre-commit-hooks.yaml` (the external-facing manifest) and from `.pre-commit-config.yaml`
(this repo's own copy of the same hooks) *independently*. It then asserts that a probe path in scope
of one is in scope of the other. That catches this repo narrowing its local hook without narrowing
the published one, or the reverse.
The original selected each hook's record by matching `entry:` against the owning skill's
`scripts/vale-wrap.sh` path. After the merge both vale hooks point at the same entry, so that
selector can no longer tell them apart. Case 33 pairs the hooks by `id:` instead, from an explicit
table: `kyberforge-vale-audit-skill` ↔ `vale-audit-prefilter-skill`, and
`kyberforge-vale-audit-agent` ↔ `vale-audit-prefilter-agent`. It carries the original six probe rows
unchanged. It also fails by name on a missing hook id, and on a class with no shared probe.
An earlier revision of this ADR shipped *without* that port and called the gap half-closed by case
32. It was not. Narrowing `vale-audit-prefilter-skill` from `^plugins/[^/]+/...` to
`^plugins/kyberforge/...` still matches tracked files of the right class. That clears case 32 while
silently dropping every other plugin's skills from this repo's prefilter, and it was measured
leaving the whole suite green. Case 33's Part B now makes exactly that mutation, the agent-hook
equivalent and a renamed hook id, and requires each to fail.
Case 32 stays, for the separate zero-match question: each local hook must still select at least one
tracked file, and only files of its own artifact class.
**Line count.** Merging removes the 1,724 duplicated lines above. Deleting the two sync-gate files
(413 + 797 = 1,210) removes more, for **2,934 lines** in total, plus one pre-push hook,
`check-vale-style-sync`, formerly at `.pre-commit-config.yaml:166`. Two smaller deletions are not in
that figure:
- `scripts/sync-vale-styles.sh` (21 lines), the helper that regenerated skill-audit's copy from
agent-audit's, now has nothing to sync.
- `agent-audit/scripts/README.md` (47 lines) has no successor. Nothing referenced it, and the only
README `references/skill-file-structure.md` mandates is `tests/README.md`, which survives.
The duplication is not symmetrical across the whole tree, and the asymmetry is what shapes the
decision. Outside the shared resolver the two `validate.sh` copies total 1,293 lines (616 skill, 677
agent) and have **91 distinct lines** in common. The two validators are not one script with a mode
flag; they are two genuinely different scripts that happen to embed one identical block.
The bodies are the binding constraint on the merge. `skill-audit`'s body is 724 words and
`agent-audit`'s is 808 — 1,532 together against `BODY_MAX_WORDS = 900`. Only 211 words are common
to both (47 byte-identical body lines). A merged body that simply concatenated the two flows would
fail the gate its own plugin enforces by a factor of 1.7, and there is no trimming route to 900:
1,321 of the 1,532 words are flow-specific.
Both skills already carry `category: factory` in their metadata, and both carry five
`source_keys` — ten in total, disjoint, because they audit against different specs.
## Decision
**The two skills become one, named `factory-audit`.**
**1. The name.** `factory` is what both already declare as their `category`, so the merged skill is
named for the thing it audits rather than for the two input types it now dispatches between. Two
alternatives were live and both are rejected below for naming reasons rather than substance:
`audit` collides with the unrelated `agentsmd-audit`, and `forge-audit` makes a bare skill name a
family prefix of a live sibling — `forge` exists at
`plugins/kyberforge/.apm/skills/forge/`. No bare skill name may be a family prefix of another.
**2. `SKILL.md` becomes a dispatch body.** Steps 1-3 move out to `references/skill-flow.md` and
`references/agent-flow.md`. The body carries the Gotchas that apply to both branches, the dispatch
table, and Step 4 — Report, which is shared. This is ADR-0020's own rule ("Dispatch is mandatory at
two or more mutually exclusive flows") applied to the file that defines it, and the word arithmetic
above is why it is mandatory here rather than stylistic.
**Dispatch happens at Step 0, keyed on the target path, before Step 1 runs.** The table accepts
exactly the shapes `scripts/validate.sh` detects:
- A directory containing `SKILL.md`, or a `SKILL.md` file (its parent directory is audited), takes
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. `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
fallback row. It could not route a `SKILL.md` file path, a trigger its own description advertised.
Its agent row ("a path under `.apm/agents/`… or an agent markdown file") was both wider than the
script and circular.
**3. One entry point per script, auto-detecting, with the mode-specific half sourced.**
- `scripts/validate.sh` detects the target type itself, then sources `scripts/lib-boundary-resolver.sh`
and one of `scripts/lib-checks-skill.sh` / `scripts/lib-checks-agent.sh`.
- `scripts/validate-provenance.sh` does the same, sourcing `scripts/lib-contributing-files.sh` and
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 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.
The flow file drives Steps 2-4, so a misdispatched audit would still apply the wrong Step 3 rubrics,
print the wrong coverage line and recommend the wrong author skill. The body closes that gap with an
explicit guard under the Step 0 table: if `validate.sh` reports on the other artifact type than the
row taken, discard the run and restart at Step 0.
**4. Reference files are prefixed by flow, with one exception.** Every flow-specific file becomes
`skill-*` or `agent-*` — `skill-description-quality.md`, `agent-description-quality.md`,
`skill-finding-criteria.md`, `agent-finding-criteria.md`, and so on. The exception is `sources.md`,
which stays singular and carries all ten `source_keys`, because the skill-side provenance check
hard-codes `os.path.join(skill_dir, "references", "sources.md")` (pre-merge
`skill-audit/scripts/validate-provenance.sh:180`, now `scripts/lib-provenance-skill.sh:215`). A per-flow sources file would mean
changing the provenance contract to get a cosmetic gain.
**5. Both exported Vale hook IDs survive unchanged.** `.pre-commit-hooks.yaml` keeps
`kyberforge-vale-audit-skill` and `kyberforge-vale-audit-agent`, keeps both `files:` regexes
(`(^|/)SKILL\.md$` and `(^|/)agents/[^/]+\.md$|\.agent\.md$`), and re-points both `entry:` lines at
the one surviving `vale-wrap.sh`. Nothing in the published hook-repo contract changes: an external
consumer's `.pre-commit-config.yaml` keeps working byte-for-byte across the merge. Two IDs pointing
at one script is not a redundancy — it is what keeps the two `files:` scopes addressable
independently, which is exactly ADR-0014's "one hook per file-scope" finding.
**6. `tests/test-adr0020-contract.sh` changes in three ways, and the third is a conversion, not a
deletion.** Assertion 1 drops from three resolver copies to two: the merged `factory-audit` holds
one, and `scripts/skill-size-check.sh` keeps its embedded copy. A new assertion 1a gives the
resolver the same protection 1b already gave the parser. It asserts that `validate.sh` sources
`lib-boundary-resolver.sh` in both mode branches, and that the resolver's BEGIN marker and
`def _authoring_root(` appear in exactly those two files and nowhere else. A byte-identity hash alone
would miss a third pasted copy, or an entry point that quietly stopped sourcing the library. Sourcing the resolver from the
plugin tree into `skill-size-check.sh` was considered and refuted — that script is a repo-root hook
consumed through `.pre-commit-hooks.yaml`, where `entry[0]` is the only token pre-commit rewrites,
so it cannot reach a file inside the plugin at a path any consumer has. Assertion 1b is **converted**:
it stops pinning that two `validate-provenance.sh` copies of the Contributing-files parser are
byte-identical, and starts pinning that `lib-contributing-files.sh` is a single sourced copy that has
not been re-inlined into either mode library. The claim it protects is the same one — the parser has
exactly one authority — stated against the new structure. The drift history behind it is smaller than
an earlier revision of this ADR implied. `598a7c3` (2026-09-01, the squash of PR #129) is where the
bullet-form parser landed on a published branch, in both copies, already carrying the
`SHARED CONTRIBUTING-FILES PARSER` markers that 1b hashed. The drift it is named for happened inside
that PR's own history: `484357a` (2026-08-30, pre-squash, not on any published branch) added the
parser with two different spellings of the loop — a temporary `rest` in skill-audit and an inline
slice in agent-audit — which were behaviourally identical, and a later commit on the same branch
unified the spellings before the squash. From then until
the merge's parent the two marker blocks were byte-identical (`md5 0857272d…` both). So the parser
never *parsed* differently. What the gate never covered was the prose around the block, and a
docstring there asserted identity the loop did not have. One sourced library removes the question.
**7. Two things this change does not do.** `skill-author` and `agent-author` are **not** merged
here. ADR-0020 excluded the pair on the grounds
that they emit genuinely different artifacts, and nothing measured in this session revisits that.
*(Updated 2026-09-16.)* It was an open, unmeasured finding when this was written; it has since been
measured at about 150–180 shared lines and refuted, and ADR-0020's rejected alternative records the
figures.
And **no audit criterion changes.** Every check, tier, threshold, regex and branch is carried across
as-is. The Python payloads reassembled from the new libraries differ from the pre-merge heredocs only
in comments. The one exception is three lines naming `references/agent-field-inventory.md`, a
byte-identical rename of `field-inventory.md`. Byte-level differential runs over every live skill
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.
**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, 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` 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 |
| `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.
## Considered options
**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 **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.
**One monolithic dispatching `validate.sh` (rejected).** Dropping one resolver copy from the
concatenation of the two current files gives roughly 2,354 lines in a single script. It is the
straightforward reading of "merge the scripts", and it is wrong on the evidence: the two validators
share only 91 distinct lines outside the resolver, so a monolith would be two near-disjoint
implementations behind one `if`, with every future edit to either half requiring a reader to hold
both in context. Sourcing per-mode libraries gets the same single entry point and keeps the halves
readable apart.
**Genuinely merging the three colliding reference files into two-section files (rejected).**
`description-quality.md`, `finding-criteria.md` and `validation-scripts.md` exist under both skills
today, and folding each into one file with a skill section and an agent section is the tidier-looking
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
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
`audit` alongside it reads as the general case of a skill it is unrelated to, which is precisely the
routing confusion a merge is supposed to reduce.
**Naming it `forge-audit` (rejected).** It matches the plugin and reads well. `forge` is a live skill
in the same plugin, so `forge-audit` makes one bare skill name a prefix of another — a router asked
to distinguish `forge` from `forge-audit` is being asked to disambiguate on a suffix, and a user
typing `forge` gets an ambiguity that does not exist today.
**Collapsing the two exported Vale hook IDs into one (rejected).** With a single `vale-wrap.sh` and a
single `.vale.ini`, one hook ID looks sufficient. It is a breaking change to a published hook-repo
contract: any external repo pinning `kyberforge-vale-audit-agent` breaks on upgrade, for no gain.
It also re-creates ADR-0014's measured failure in a new place — that ADR confirmed empirically that a
single hook entry pointed at one config silently scanned 0 files of the other type. Two IDs cost two
manifest stanzas and keep both file scopes explicit.
## Consequences
**The single-file `.vale.ini` comes back, and this does not reverse ADR-0014.** ADR-0014 split one
root config into two skill-scoped copies because two skills each needed their own, and no
plugin-level shared directory exists to hold one. Its reasoning is untouched; the merge removes the
condition that reasoning operated on. One skill needs one config, so the union is written back into
one file. **The union is behaviour-neutral and this was checked rather than assumed:** skill-audit's
config has a single `[**/SKILL.md]` section, agent-audit's has `[**/agents/*.md]` and
`[**/*.agent.md]`, and no file in the corpus matches more than one of the three. Where an overlap is
constructible at all (`agents/SKILL.md`), both matching sections assign `BasedOnStyles = Kyberforge`,
so even then no verdict moves. `KyberforgeCopilot` stays scoped to `[**/*.agent.md]` exactly as it is
now, which is what keeps the merged config from widening Copilot-specific rules onto `SKILL.md`.
**`scripts/check-scope-walkup-sync.sh` survives, and confusing it with `check-vale-style-sync.sh` is
the obvious mistake here.** The two look like the same kind of gate and are not. The walk-up checker
covers **four** independent ports of the scope walk-up, and only two of them live in the audit pair:
the other two are `agent-author/scripts/new-agent.sh` and `skill-author/scripts/new-skill.sh`, which
this change does not touch. They are also Bash where the audit pair's are Python, so as its own header
records, it can never become a text diff — it asserts behavioural agreement across a fixture matrix
instead. Merging two of four ports leaves three ports and the same job.
**Roughly 71 files carry inbound references to the two skill names and must be re-pointed.**
Derived as `git grep -l -E "skill-audit|agent-audit" | wc -l` — it includes ADRs, `LESSONS.md`,
`docs/spec/gates.md`, both author skills' routing targets, `forge`'s dispatch, the test suite and the
two manifests. Boundary clauses naming `skill-audit` or `agent-audit` are the sharp end: ADR-0020's
resolvable-target check is a blocking ERROR on a dangling route, so a missed rename fails the push
rather than degrading quietly. Historical references inside ADRs describing the pre-merge state stay
as they are; the resolver reads boundary clauses in descriptions, not ADR prose.
**The dispatch body carries only the gotchas common to both flows, and ships with no SUGGESTION.** An
earlier revision of this change shipped the Gotchas section at **229 of 548 body words, 42%**,
against `GOTCHA_MAX_BODY_FRACTION = 0.25`. It accepted that as standing output, arguing that moving
a gotcha to `references/` meant an extra file read on every invocation. That argument was wrong for
the two gotchas that were over budget, because neither was shared:
- the `Agent flow, plugin/APM scope only` provider-safety bullet names its one branch in its own
text;
- the 112-word body-word-gate bullet was two separate pre-merge gotchas welded together, a skill half
and an agent half.
A dispatch body is the dispatch table *plus the gates common to every branch* (CONTEXT.md; the
skill-flow rubric `references/skill-body-discipline.md`). Keeping a single-branch gotcha in it
contradicts that definition. Moving it into its flow file costs no read either, because the body
already loads exactly one flow file on every invocation by construction.
So the skill half now sits under `## Gotchas` in `references/skill-flow.md`. The agent half and the
provider-safety bullet sit under `## Gotchas` in `references/agent-flow.md`. The body keeps three
gotchas: the no-narration rule, the `disable-model-invocation` exemption and the Vale `0 files`
trap. Measured with `scripts/skill-size-check.sh` thresholds zeroed to force the figures out, the
section is now **91 of 555 body words, 16%**.
**Every invocation now reads one extra `references/` file.** The dispatch body names the flow file
and the agent loads it, where today Steps 1-3 arrive with the body. This is the cost the progressive-
disclosure trade always carries, and it is paid against a saving: an invocation loads the dispatch
body plus one flow instead of a body that would have to carry both. It is also the reason the
two-section reference file was rejected above.
**The original audit's figures for this finding were wrong in three ways, and each is worth naming
so the correction is not re-derived from scratch later.**
- It claimed roughly 3,300 duplicated lines and **two** pre-push hooks. The measured removal is
**2,934 lines and one hook**. The second hook it counted was `check-scope-walkup-sync`, which
survives for the reason above.
- It claimed the two validators were one script hard-wired per mode. They are not. Outside the shared
resolver they total 1,293 lines with 91 distinct lines in common. That error matters because it is
what made the monolithic `validate.sh` look like the obvious implementation.
- It named the **merged `description`** as the blocker on merging. It is not. Merging deletes
description content rather than accumulating it: the `Not a skill directory -> skill-audit` clause
loses its referent, and the `"is this ready to ship"` trigger was duplicated verbatim across both.
The two descriptions it replaces measure **239** (skill-audit) and **250** (agent-audit) at
`620f20b^` (see the repointing note above). The description this skill ships measures **241**,
inside the 250 SUGGESTION target.
It carries one arrow per boundary target (`Not applying skill fixes -> skill-author. Not applying
agent fixes -> agent-author.`), because ADR-0020 resolves only the first target after an arrow, so
a one-arrow form would leave `agent-author` checked by nothing. The real blocker was the body: 1,532
words against `BODY_MAX_WORDS = 900`, with only 211 words shared. Diagnosing the description would
have produced a merge with a concatenated body that failed its own plugin's gate.
**Correction to an earlier revision of this bullet.** It shipped the description at **319**
characters and accepted the SUGGESTION. It said the excess paid for the second arrow and for
"both flows' artifact-specific trigger phrases carried in full". Only the arrow was worth it.
The trigger phrases stated one trigger twice in two registers: "a skill directory or agent
definition audited", then quoted `audit this skill`, `review my SKILL.md`, `audit this agent` and
`review my agent file`. ADR-0020 makes that a FAIL ("Stating the same trigger twice in two
registers is a FAIL"), so it was not a cost of merging. Dropping the quoted duplicates, and keeping
the one indirect trigger that omits the domain word (`is this ready to ship`), gives 241 with both
arrows kept. The same revision's "240 characters" figure for a hypothetical single-arrow merge was
never reproduced, and is withdrawn rather than re-derived.
**`factory-audit` shipped at `metadata.version: "1.0.0"`, not ADR-0022's `0.1.0` for a new skill.**
It is a new directory, but not a new skill in the sense ADR-0022's starting version encodes: it
carries every check, rubric and reference of two skills that were both already at `1.0.0`, and
resetting to `0.1.0` would signal an immaturity that the merged content does not have. The fixes
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.