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 to620f20b(identical parent tree, reachable from the PR branch), with a note that neither is reachable from origin/main. The parser-drift paragraph now credits598a7c3(the reachable PR #129 squash) and keeps484357aonly 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 sinceef27c97. - #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 inc96ca9c); 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
461 lines
36 KiB
Markdown
461 lines
36 KiB
Markdown
# `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.
|