refactor(kyberforge)!: merge skill-audit and agent-audit into factory-audit

Why

The two audit skills carried 1,724 lines of byte-identical duplication: the ADR-0020 boundary
resolver (1,061), vale-wrap.sh (526), the Vale style rules (44) and the Contributing-files parser
(93). Nothing shared them — they were held in sync by a 413-line pre-push gate and its 797-line
test suite. Sync-by-gate had already failed once: at 484357a the two parser copies drifted into
different spellings of the bullet loop while a docstring asserted they were identical. That drift
was behaviour-neutral and was re-unified by hand at 598a7c3, so the copies were identical at merge
time — but nothing had caught it, and the next drift need not be neutral.

Implementation Notes

Self-containment binds BETWEEN skills, not within one. The agentskills.io spec forbids reaching
across skill directories, which is why two separate skills needed embedded copies; two files inside
ONE skill may source a third. That is the whole reason the merge removes duplication rather than
relocating it.

The union of both bodies measured 1,532 words against BODY_MAX_WORDS=900, and only 211 of those
words were shared, so SKILL.md is a dispatch body. Step 0 resolves the flow from the target path
before any validation, and its table mirrors validate.sh's detection exactly: a directory holding
SKILL.md or a SKILL.md file (skill); a *.agent.md, or a .md directly under an agents/ directory
(agent); anything else stops without running a validator. Steps 1-3 live in
references/skill-flow.md and references/agent-flow.md, and gotchas that apply to one flow live in
that flow's file, since it is loaded on every invocation anyway. If validate.sh reports on the
other artifact type, the body restarts at Step 0.

Named factory-audit rather than forge-audit because forge is a live skill, and a family prefix that
matches a live sibling reads as ownership rather than membership.

The description carries one arrow per boundary target, because ADR-0020 resolves only the first
target after an arrow. It drops the quoted "audit this skill"-style phrases, which restated
"audited" in a second register (ADR-0020's duplicate-register rule). 241 characters, Gotchas 16%
of the body: no size SUGGESTIONs.

The boundary resolver stays embedded in two files rather than imported: a cache-installed plugin
cannot read outside its own directory, and the repo-root hook resolves via .pre-commit-hooks.yaml
where entry[0] is the only token pre-commit rewrites, so no single file is reachable by both.
tests/test-adr0020-contract.sh hashes both copies for byte-identity, and asserts validate.sh sources
the resolver and that no third copy exists.

The entry scripts classify the target from its resolved parent directory, so a bare agent filename
typed inside agents/ works; resolve SCRIPT_DIR CDPATH-safely; and exit 2 when a lib-*.sh is
missing, rather than dying with exit 1, the tier the flows relay as real findings.

The provenance run functions stash their findings code in KYBERFORGE_PROV_RC and
return 0, so validate-provenance.sh calls them UNTESTED. Testing a function's
status (`f || RC=$?`) disables errexit for its entire body, and no subshell or
`set -e` inside can re-arm it once the call sits in a condition context
(measured, both spellings). Their error paths use `exit`, which is unaffected
either way; this keeps errexit armed for anything added later.

Case 0's readability guard reads the file instead of asking `[[ -r ]]`. `-r` is
access(2), which answers yes for uid 0 even on a mode-000 file, and this repo's
dev environment is root -- so the guard could never fire where it exists to fire.
A read attempt is also the stricter question, catching EIO. This is the reasoning
scripts/check-vale-style-sync.sh carried before this commit deleted it; the
hazard did not go with it.

All three entry scripts are CDPATH-safe, vale-wrap.sh included: both of its cd sites are cleared,
the --config resolution and the directory-mirror walk, where an exported CDPATH would otherwise
print a decoy path into the -print0 stream and build the mirror from the decoy's files. The two
remaining bare cd calls take absolute paths, which CDPATH is never consulted for.

Impact

BREAKING: skill-audit and agent-audit no longer exist as invocable skills. kyberforge goes to
2.0.0 (catalog 0.4.7).

Check logic is unchanged: differential runs of the old and new validators across every skill and
agent produced byte-identical stdout, stderr and exit codes, and the reconstructed Python payloads
differ only in comments and the references/field-inventory.md -> agent-field-inventory.md rename.
One doctrine governs the tiers: exit 0 is audited and clean, exit 1 is audited with findings OR a
target present but unreadable, exit 2 is that nothing was audited at all. Edge paths DID change,
deliberately (full table in ADR-0025):
- a missing target exits 2 (never ran), not 1, under its own "does not exist" message; detection is
  by path shape, so a shape-matching path that is simply absent used to reach the validator and come
  back as a FAIL against a file that never existed;
- an unshaped target exits 2 under the generic "matches neither" message, and a directory with no
  SKILL.md under a third, distinct one -- three exit-2 messages, not one;
- a dangling symlink or a symlink loop stays exit 1: it is present but broken, which is a finding
  about the artifact rather than a usage error;
- a SKILL.md file path is audited as its skill directory instead of refused;
- a .md agent outside an agents/ directory is refused rather than audited;
- a missing script library, a missing python3, a missing PyYAML, and no argument at all each exit 2.
  validate-provenance.sh already exited 2 for the last two; validate.sh now matches it.

.pre-commit-hooks.yaml is a published contract consumed by external repos. Both hook IDs and both
files: regexes are unchanged; only entry: and description: moved.

scripts/check-vale-style-sync.sh (413), scripts/sync-vale-styles.sh (21),
tests/test-check-vale-style-sync.sh (797) and agent-audit/scripts/README.md (47) are deleted. The
checker made 17 assertions: 6 compared the two Vale copies and are moot; 10 are rehomed into
tests/test-vale-wrap.sh (case 0, cases 28-31, and the suite's Vale-absent skip); and the
cross-manifest files: agreement check, which selected hooks by entry: and so could not survive both
hooks sharing one, is ported as case 33 pairing hooks by id:. Cases 28, 30 and 33 carry mutation
self-tests; narrowing the local skill prefilter to 6 of 38 SKILL.md files now fails the suite.

Skills go 39 to 38. Pre-push goes 9 repo-authored hooks to 8.

ADR: 0025
BREAKING-CHANGE: the skill-audit and agent-audit skills are removed. Both flows are served by
  factory-audit, which auto-detects whether it was handed a skill directory or an agent file.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YR2CjVumUbEGWcMikcoXBD
This commit is contained in:
2026-09-15 18:39:43 +00:00
parent a5962ba773
commit 620f20b0fd
119 changed files with 6308 additions and 5487 deletions

View File

@@ -1,5 +1,10 @@
# Add INFO as a third finding level in skill-audit reports
**Skill renamed per ADR-0025 (2026-09-15):** `skill-audit` and `agent-audit` merged into
`factory-audit`, which dispatches to a skill flow and an agent flow at Step 0. Read `skill-audit`
below as `factory-audit`'s skill flow. The decision itself is unchanged — ADR-0025 carried every
audit criterion, tier and finding level across as-is.
`skill-audit` shipped with two finding levels: FAIL (blocks shipping) and
SUGGESTION (optional improvement). Provenance validation introduced observations
that are worth surfacing but not actionable: a `references/*.md` file with no

View File

@@ -1,5 +1,11 @@
# agent-audit takes a single file path and derives the counterpart by scope detection
**Skill renamed per ADR-0025 (2026-09-15):** `agent-audit` merged with `skill-audit` into
`factory-audit`. Read `agent-audit` below as `factory-audit`'s agent flow. The single-file
invocation contract this ADR sets survives the merge intact — `factory-audit` dispatches at Step 0
on the target path, and an `*.agent.md` or a path under `.apm/agents/` takes the agent flow, so the
caller still names one file and the script still derives the rest.
`agent-audit` validates agent definition file pairs (Claude Code `.md` + Copilot `.agent.md`). The skill accepts a path to either file and derives the counterpart using scope detection rather than requiring the caller to name both files or supply a root directory.
## Considered options

View File

@@ -1,5 +1,10 @@
# agent-audit reads field lists from a reference file, not hardcoded script arrays
**Skill renamed per ADR-0025 (2026-09-15):** `agent-audit` merged with `skill-audit` into
`factory-audit`. Read `agent-audit` below as `factory-audit`'s agent flow; the reference file this
ADR is about is now `factory-audit/references/agent-field-inventory.md`. The decision is unchanged —
the field lists still live in a reference file read at runtime, not in script arrays.
`agent-audit`'s `validate.sh` checks for Claude Code-only fields in Copilot files and
silently-ignored fields in plugin agents. Rather than hardcoding those field lists in the
script, the script reads `references/field-inventory.md` at runtime. This keeps field list

View File

@@ -15,6 +15,9 @@ ADR's own conclusion is unaffected by that move: the provenance file still belon
auto-scans, and `.apm/agents/` is, if anything, further removed from plugin-root than the old
flat `agents/` directory was, so the reasoning below still holds. References below to
`<plugin-root>/agents/` describe the pre-APM layout in effect when this decision was made.
**Skill renamed per ADR-0025 (2026-09-15):** `agent-audit` merged with `skill-audit` into
`factory-audit`; read the `agent-audit` references below as `factory-audit`'s agent flow, whose
`validate-provenance.sh` still resolves `<plugin-root>/sources.md` exactly as this ADR decided.
**Scope boundary (per ADR-0016):** this path change is plugin scope only. Project scope
(`.claude/agents/` + `.github/agents/`) and user scope (`~/.claude/agents/` +
`~/.copilot/agents/`) are unaffected — they are not APM packages and keep the dual-file

View File

@@ -6,7 +6,7 @@
Three skills in the `core` plugin (`core`'s first active skills):
- **`agentsmd-author`** — creates/updates a target repo's `AGENTS.md`, including nested monorepo placement (nearest-file-wins). Closes out by invoking `agentsmd-audit` inline, mirroring the `skill-author`/`skill-audit` pattern. When it detects an existing provider-specific file (`CLAUDE.md`, etc.) with content that duplicates what AGENTS.md should own, it calls `provider-adapter-author` via skill composition.
- **`agentsmd-author`** — creates/updates a target repo's `AGENTS.md`, including nested monorepo placement (nearest-file-wins). Closes out by invoking `agentsmd-audit` inline, mirroring the `skill-author`/`skill-audit` pattern (**skill renamed per ADR-0025, 2026-09-15:** `skill-audit` is now `factory-audit`'s skill flow; the author-then-audit pattern is unchanged). When it detects an existing provider-specific file (`CLAUDE.md`, etc.) with content that duplicates what AGENTS.md should own, it calls `provider-adapter-author` via skill composition.
- **`agentsmd-audit`** — a single combined pass checking three mandatory baselines against `AGENTS.md` only: secrets/credentials (governance.md hard prohibition), structural completeness (common-sections checklist from the agents.md spec), and accuracy/drift (do referenced commands/paths resolve against the repo). Never inspects provider adapter files.
- **`provider-adapter-author`** — detects and converts a provider-specific instruction file into a thin adapter that imports `AGENTS.md` (mirroring this repo's own two-tier `CLAUDE.md` pattern). Self-validates via its own bundled deterministic script (`scripts/validate-adapter.sh`) rather than a separate paired audit skill, since the check (import present, no duplicated headings, size threshold) is mechanical.

View File

@@ -109,7 +109,8 @@ every rule to `level: error` is what actually implements this decision.
1.81 tokens per word, so a worst-case `SKILL.md` at the ceiling still lands under 5,000 tokens —
`wc -w` is not BPE tokenization). Either one exceeded fails the hook. Both are
inclusive: a file at exactly 500 lines or exactly 2,770 words passes, and only one past a ceiling
fails. `skill-audit/scripts/validate.sh` enforces the same pair on the same inclusive terms, so
fails. `skill-audit/scripts/validate.sh` (now `factory-audit/scripts/validate.sh`, see ADR-0025)
enforces the same pair on the same inclusive terms, so
the audit and the commit hook cannot disagree about whether a given `SKILL.md` is over size.
- `styles/KyberforgeTrial/` and `.vale.trial.ini` were deliberately not created — noted here so a
future reader doesn't wonder if a trial tier was forgotten.

View File

@@ -5,6 +5,28 @@ out of the repo root was deliberately deferred there, not fixed. ADR-0013's othe
(rule scope, `level: error` model, `SentenceOpenerThereIs`/`VagueQualifier` trial outcomes) is
unaffected and remains in force.
**Amended by ADR-0025 (2026-09-15).** The reasoning below is not reversed; its *precondition* is
gone. The two skill-scoped Vale copies this ADR mandates — `agent-audit/assets/vale/` (canonical)
and `skill-audit/assets/vale/` (subset) — existed because the no-cross-skill-sharing rule made it
impossible for one audit skill to read the other's config. ADR-0025 merges the two skills into
`factory-audit`, so there is no boundary left to duplicate across: there is now **one** copy, at
`plugins/kyberforge/.apm/skills/factory-audit/assets/vale/`, carrying both styles and the
single-file `.vale.ini` — `[**/SKILL.md]`, `[**/agents/*.md]`, `[**/*.agent.md]` — that this ADR's
"One hook per file-scope" section had split in two. `scripts/check-vale-style-sync.sh`, decided on
below and wired at pre-push, is deleted with the copy it diffed. Nothing it asserted about the
config was lost. Its six-row glob-coverage probe table is now `tests/test-vale-wrap.sh` cases 28-30,
run against the merged config. Case 31 carries across the per-rule override allowlist, and case 0
carries across the "config loads" guards. Its **cross-manifest `files:` drift check** is ported as
case 33. The original keyed each hook's record on `entry:`, which stopped working once both vale
hooks shared one entry, so the port pairs the hooks by `id:` instead. Of the script's 17 assertion
sites, 6 compared the two copies and are moot, 10 are rehomed and 1 is ported. ADR-0025 gives the
per-assertion mapping; read the "six" here as probe *rows*, not as a share of those 17.
What does **not** change: the two exported hook IDs, `kyberforge-vale-audit-skill` and
`kyberforge-vale-audit-agent`, keep their IDs and their `files:` regexes — external consumers pin
them by name — and the argument-free `entry:` contract is untouched. Read the two-copy table, the
sync-check paragraph, and the `tests/test-vale-wrap.sh` Consequences bullet below as the state this
ADR established, not as current layout.
`skill-audit`/`agent-audit`'s Step 1 called
`"$(git rev-parse --show-toplevel)/scripts/vale-wrap.sh" --config "$(git rev-parse --show-toplevel)/.vale.ini"`
— which resolves to whichever repo the skill happens to be running in. Inside `ai-development`
@@ -128,7 +150,9 @@ doesn't wonder if it was overlooked.
`.pre-commit-config.yaml` stays byte-identical to the shipped manifest on those `entry:` lines
so the local gate keeps exercising the same resolution path a consumer does.
- `tests/test-vale-wrap.sh` now exercises skill-audit's copy specifically — its fixtures are all
`SKILL.md`-shaped, and only skill-audit's `.vale.ini` has the matching glob section.
`SKILL.md`-shaped, and only skill-audit's `.vale.ini` has the matching glob section. (State as of
this ADR. Since ADR-0025 there is one `vale-wrap.sh` and one `.vale.ini` under `factory-audit/`,
and that suite exercises all three glob sections of the merged config — see cases 28-30.)
- The first `vX.Y.Z` tag is cut once this change and its tests pass, giving external
`.pre-commit-hooks.yaml` consumers something to pin.
- **Cutting the tag is not left to memory.** `scripts/check-release-needed.sh`, wired at

View File

@@ -123,7 +123,8 @@ correction) sorted what they document into three buckets:
- ADR-0016 (a narrower decision discovered while designing issue #89) turned out to gate how
issue #90 had to re-author plugin-scope agents: `.apm/agents/*.agent.md` compiles verbatim to
both Claude and Copilot, so those files carry only the fields in the `apm-agent-allowlist` section
of `plugins/kyberforge/.apm/skills/agent-audit/references/field-inventory.md` (as amended
of `plugins/kyberforge/.apm/skills/agent-audit/references/field-inventory.md` (now
`factory-audit/references/agent-field-inventory.md`, see ADR-0025) (as amended
2026-08-14: `name`/`description`/`model`/`source_keys`/`disallowedTools`) — existing dual-file
`<name>.md`+`<name>.agent.md` pairs could not be raw-moved, only re-authored.
- Two follow-up issues tracked the remaining work: #89 (`skill-author`/`agent-author` routing

View File

@@ -143,7 +143,8 @@ below is narrowed accordingly.
Enforcement follows the decision: `agent-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`, and that line now reads
`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
`name description model source_keys disallowedTools`. `disallowedTools` also stays in that file's
`claude-code-only-fields` list, which is not a contradiction — that list governs whether a field
may cross the CC/Copilot boundary in a real project/user-scope *pair*, a different question from

View File

@@ -65,7 +65,9 @@ Three sub-decisions inside that:
## Consequences
**Skills gain an unnamespaced name.** apm deploys plain project skills, so `git:git-commits` also
answers to `git-commits` and `kyberforge:skill-audit` to `skill-audit`. This is not configurable —
answers to `git-commits` and `kyberforge:skill-audit` to `skill-audit` (**skill renamed per ADR-0025,
2026-09-15:** that skill is now `factory-audit`, so the live example is `kyberforge:factory-audit` to
`factory-audit`; the rule is unchanged). This is not configurable —
a project skill has no plugin to prefix. `AGENTS.md` and `CONTEXT.md` are updated to name the bare
form, which is what apm deploys and the only form a repo consuming holocron through apm gets.

View File

@@ -8,6 +8,23 @@ gates that hold them.
**Status: accepted (2026-08-14).**
**Amended by ADR-0025 (2026-09-15).** The contract, the tiers and every verdict rule below stand
unchanged. What moved is the **number and location of the scripts that carry them**. This ADR names
three: `scripts/skill-size-check.sh`, `skill-audit/scripts/validate.sh` and
`agent-audit/scripts/validate.sh` — "all three validators" (Decision), "all three scripts"
(Enforcement table footnote), "`scripts/skill-size-check.sh` and its two mirrored copies"
(the `_add()` amendment). ADR-0025 merged the two audit skills, so there are now **two**: the root
`scripts/skill-size-check.sh`, which still embeds the 1,061-line block between `BEGIN`/`END ADR-0020
SHARED BOUNDARY RESOLVER` markers, and one plugin copy — extracted out of the merged validator into
`plugins/kyberforge/.apm/skills/factory-audit/scripts/lib-boundary-resolver.sh` and sourced by
`factory-audit`'s `validate.sh` and `validate-provenance.sh` rather than pasted into each. The
Enforcement table's "constants mirrored in `skill-audit/scripts/validate.sh` and
`agent-audit/scripts/validate.sh`" is one path now, `factory-audit/scripts/validate.sh`, which
auto-detects the artifact type; the skills/agents columns are unaffected, since the merged validator
applies the body tiers on the skill path only. The two copies must still stay byte-identical — a
plugin script cannot source the root one, which is why a second copy exists at all. Read every
"three" below as the count at the time of writing.
## Context
Every `file:line` citation in this ADR is against the base commit the decision was taken on,
@@ -189,7 +206,8 @@ Agents take the same description gates — they are preloaded identically — an
A skill body is loaded into the caller's context, competing with the live conversation; an agent body
becomes the system prompt of a fresh context. The rationale for the 900-word FAIL does not transfer.
That exemption is expressed in `agent-audit/scripts/validate.sh`, which has no body constant, and in
That exemption is expressed in `agent-audit/scripts/validate.sh` (now `factory-audit`'s
auto-detecting `validate.sh` on its agent path, see ADR-0025), which has no body constant, and in
the `files:` pattern of the `skill-size-check` pre-commit hook, which is `SKILL.md`-only. It is *not*
expressed in `scripts/skill-size-check.sh` itself, which measures whatever path it is handed —
running it directly over `plugins/*/.apm/agents/*.agent.md` exits 1 with 900-word body FAILs on
@@ -201,7 +219,8 @@ file pattern, not by the script knowing the difference. Anyone widening that pat
would silently enforce a gate this ADR declines to set.
A plugin-scope agent is a single file with no sibling `references/` directory, so it cannot disclose
to itself — it can only delegate to skills. `agent-audit` therefore gains a **delegation check**: an
to itself — it can only delegate to skills. `agent-audit` (now `factory-audit`'s agent flow, see
ADR-0025) therefore gains a **delegation check**: an
agent body that restates a procedure owned by a skill it can invoke is a FAIL, with the fix being
"invoke `<skill>` instead". Length falls out of delegation rather than being gated directly.
@@ -232,18 +251,20 @@ type of input they take should be **one skill with a dispatch table**. This catc
one-or-two-file agent pair, per ADR-0005 and ADR-0016) and their overlap is in the improve flow
rather than the core job.
**DEFERRED — not implemented in the change that carries this ADR. Tracked as issue #101.** Both
skills still exist separately, and this change made the split deeper rather than shallower: retrofit
**DEFERRED when this ADR was written — not implemented in the change that carries it. Tracked as
issue #101. IMPLEMENTED by ADR-0025 (2026-09-15), which merged the pair into `factory-audit` with a
Step 0 dispatch and closed the deferral.** At the time of writing both
skills still existed separately, and this change made the split deeper rather than shallower: retrofit
to the dispatch pattern took `skill-audit` from 3 reference files to 7 and `agent-audit` from 4 to 8,
and their two same-named `references/description-quality.md` files now differ on 100 of ~120 lines
after normalising `skill`/`agent`, where before they were closer. It has kept deepening since: the
#99 retrofit added `finding-criteria.md` to `skill-audit`, drawing it level with `agent-audit`. Both
figures move with the next retrofit, so measure rather than quote —
`ls plugins/kyberforge/.apm/skills/<name>/references/ | grep -c '\.md$'`. The merge stays the
`ls plugins/kyberforge/.apm/skills/factory-audit/references/ | grep -c '\.md$'`. The merge stayed the
decision; it reopens ADR-0008 (agent-audit's single-file invocation contract) and touches every call
site in `skill-author`, `agent-author` and `forge`, which is why it is its own change and not a rider
on this one. Recorded here rather than dropped, so the gap between the rule and the tree is deliberate
and dated instead of discovered later.
site in `skill-author`, `agent-author` and `forge`, which is why it was its own change and not a rider
on this one. Recorded here rather than dropped, so the gap between the rule and the tree was deliberate
and dated instead of discovered later — and ADR-0025 is where it was closed.
### Enforcement and rollout
@@ -266,7 +287,7 @@ which tier each rule is in, because the failure this ADR is most exposed to is a
| description opener, composition notes in a description | skills, agents | prose pattern | `plugins/kyberforge/.apm/skills/*/assets/vale/styles/Kyberforge/` |
| a Gotcha paraphrasing a body step | skills | **auditor judgment** | `references/body-discipline.md` |
| dispatch at two or more mutually exclusive flows | skills | **auditor judgment** | `references/body-discipline.md` |
| delegation: an agent body restating a skill's procedure | agents | **auditor judgment** | `agent-audit` |
| delegation: an agent body restating a skill's procedure | agents | **auditor judgment** | `agent-audit` (now `factory-audit`'s agent flow, see ADR-0025) |
| capability enumeration, restatement, trigger quality | skills, agents | **auditor judgment** | `references/description-quality.md` |
The rows in bold are stated as FAILs in the Decision above and are FAILs an *auditor* issues. None of
@@ -399,16 +420,18 @@ over the same 39 files now reports 0 errors, 0 warnings and 0 suggestions, so
independent of `skill-size-check`, so a new description can reintroduce it; `skill-size-check` does
not cover the Vale half, and no `references/` file is linted by anything (`docs/spec/gates.md` has
both causes, issue #117 tracks them). Re-derive rather than quote —*
`bash plugins/kyberforge/.apm/skills/skill-audit/scripts/vale-wrap.sh plugins/*/.apm/skills/*/SKILL.md`.
`bash plugins/kyberforge/.apm/skills/factory-audit/scripts/vale-wrap.sh plugins/*/.apm/skills/*/SKILL.md`
*(path re-pointed by ADR-0025; the `skill-audit` copy this ADR originally named no longer exists).*
**A ceiling does not produce an average.** If every author writes to the 400-character FAIL, the
preload lands at 39 × 400 = 15,600 chars — a 33% cut off 23,427, not the ~50% intended. Writing to
the 250-character SUGGESTION instead lands at 9,750, a 58% cut. The halving depends entirely on the
250-character SUGGESTION tier being visible and respected. That tier works here in a way it does not
elsewhere in this repo: `skill-audit` already reports `PASS (N suggestions)` as a first-class
elsewhere in this repo: `skill-audit` (now `factory-audit`'s skill flow, see ADR-0025) already
reports `PASS (N suggestions)` as a first-class
outcome. This is explicitly **not** the failure ADR-0013 records — Vale warnings are invisible
because vale's exit code keys on `error` alone, but these gates live in `validate.sh` and
`skill-audit`, where a SUGGESTION reaches the report. Realistic landing is somewhere in that 33-58%
because vale's exit code keys on `error` alone, but these gates live in `validate.sh` and the
audit skill itself, where a SUGGESTION reaches the report. Realistic landing is somewhere in that 33-58%
band, not a guaranteed 50%.
**A word gate cannot detect the defect it is standing in for.** `git-commits` carries twelve Gotchas

View File

@@ -83,7 +83,8 @@ unnamed in `git`'s corrected description, though `65bac15`'s own commit message
**Nothing checks any of this.** `scripts/check-manifests.sh` does not contain the string
`description`. The three ADR-0020 validators (`scripts/skill-size-check.sh` and skill-audit's and
agent-audit's `validate.sh`) gate on SKILL.md and agent frontmatter; they do open `apm.yml`, but only
agent-audit's `validate.sh` — two since ADR-0025 merged the audit pair into `factory-audit`, whose
single auto-detecting `validate.sh` carries both) gate on SKILL.md and agent frontmatter; they do open `apm.yml`, but only
to read `dependencies.apm` when resolving the boundary-target universe — none of them reads the
`description:` key, and their hook globs match `SKILL.md` and `*.agent.md` only. `apm audit --ci`,
`apm pack --check-clean` and `scripts/sync-plugin-content.sh --check --all` all compare compiled

View File

@@ -0,0 +1,376 @@
# `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.
## 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 61b0b9c^: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.
| 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.
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 three skill directories cannot read
each other's files; two files inside one skill directory have no such problem. Sourcing is available
the moment the directory boundary between them disappears. Second, **a single auto-detecting entry
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. `484357a` (2026-08-30) added the bullet-form parser to both
copies with two different spellings of the loop: a temporary `rest` in skill-audit and an inline
slice in agent-audit. The two were behaviourally identical. `598a7c3` (2026-09-01) unified the
spellings and added the `SHARED CONTRIBUTING-FILES PARSER` markers that 1b hashed. 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. That remains an open finding and it is unmeasured; ADR-0020 excluded the pair on the grounds
that they emit genuinely different artifacts, and nothing measured in this session revisits that.
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. Every change below is deliberate:
| Input | Pre-merge | Now |
|---|---|---|
| a missing path, a directory with no `SKILL.md`, a non-agent `.md` (e.g. `README.md`) | exit 1, or a mode-specific exit-2 message | **exit 2** with one generic "matches neither" Error/Why/Fix. Exit 2 is the never-ran tier, so the flow files report the section as unverified and quote the reason. |
| a `SKILL.md` file path | exit 1 or 2 (`…/SKILL.md/SKILL.md not found`, "not a directory") | **accepted**; its parent directory is audited |
| an agent `.md` *not* under an `agents/` directory (e.g. `~/drafts/my-agent.md`) | audited | **refused, exit 2**. Detection never guesses. No tracked file in this repo is affected. |
| a bare or `./`-relative agent filename, run from inside its `agents/` directory | audited | audited. The parent directory's name is read from the real path, not the typed string. |
| a `lib-*.sh` missing or unreadable, or the script directory unresolvable | did not apply (single file) | **exit 2** with Error/Why/Fix, never a raw bash error at exit 1, which is the real-findings tier |
| `CDPATH` exported | did not apply (no `cd`) | no effect. `SCRIPT_DIR` resolves with `CDPATH=''` and `cd -- … >/dev/null`. |
| no argument | `Error: skill-dir is required.` / `agent-file is required.` | one combined message and usage block; exit code unchanged (1 from `validate.sh`, 2 from `validate-provenance.sh`) |
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 6 of its 17 assertion
sites exist because there are two copies. The other 11 do other work, and are rehomed or ported above rather than being an
argument for the status quo. On the duplication itself it polices drift rather than removing the thing that drifts,
and it pays 2,934 lines plus a pre-push hook to do so. It also leaves
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
these same files at 100 of ~120 differing lines after normalising `skill`/`agent`, so the merged file
would also be mostly disjoint text under one heading.
**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
`61b0b9c^`. 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.

View File

@@ -79,7 +79,7 @@ This repo also has a `CLAUDE.md` at its root — the Claude Code entry point for
## Reference conventions
The stated convention is that files referencing other files declare those references explicitly: the referencing file carries the forward reference (the content index in `core/AGENTS.md`, `references:` in frontmatter), the referenced file carries a `when:` field describing when it is loaded, and divergence between the two signals staleness. It is aspirational, not a description of the repo today — no file under `core/instructions/` carries frontmatter at all, `when:` appears in exactly one of the 39 `SKILL.md` sources under `plugins/*/.apm/skills/`, and the reference scanner script meant to derive the reverse map ("what files reference this file?") does not exist; `docs/notes/skill-implementation-workflow.md` still lists it as unbuilt work. Treat it as intent for instruction files, skills, and workflow documents, not as a rule the repo enforces.
The stated convention is that files referencing other files declare those references explicitly: the referencing file carries the forward reference (the content index in `core/AGENTS.md`, `references:` in frontmatter), the referenced file carries a `when:` field describing when it is loaded, and divergence between the two signals staleness. It is aspirational, not a description of the repo today — no file under `core/instructions/` carries frontmatter at all, `when:` appears in exactly one of the 38 `SKILL.md` sources under `plugins/*/.apm/skills/`, and the reference scanner script meant to derive the reverse map ("what files reference this file?") does not exist; `docs/notes/skill-implementation-workflow.md` still lists it as unbuilt work. Treat it as intent for instruction files, skills, and workflow documents, not as a rule the repo enforces.
## Provider model

View File

@@ -21,24 +21,24 @@ Install hooks via `pc-run`, wiring **all three stages**. This repo's `.pre-commi
`default_install_hook_types`, so a plain install silently skips `commit-msg` (Conventional Commits)
and `pre-push` (everything below).
The pre-push command reports **11** hooks, not 9. The extra two are pre-commit's own `meta` hooks,
The pre-push command reports **10** hooks, not 8. The extra two are pre-commit's own `meta` hooks,
`check-hooks-apply` and `check-useless-excludes`: they declare no `stages:`, so they run at every
stage including this one. Both are declared in this repo's `.pre-commit-config.yaml` like everything
else — what separates them is `repo: meta` (pre-commit's own built-ins) from `repo: local`. Nine
else — what separates them is `repo: meta` (pre-commit's own built-ins) from `repo: local`. Eight
is the count of hooks this repo authors itself.
**The caveat: one of those 9 is a silent no-op under that invocation.**
**The caveat: one of those 8 is a silent no-op under that invocation.**
`check-release-needed` exits 0 immediately unless `PRE_COMMIT_REMOTE_BRANCH` equals
`refs/heads/main`, and pre-commit exports that variable only from the real pre-push git hook during
an actual `git push`. Running the stage by hand — or from a CI runner — therefore reports it
`Passed` having checked nothing. That is by design for feature branches — pushing WIP must not be
blocked on cutting a premature tag — but it means `--hook-stage pre-push --all-files` is a full
rehearsal of 8 hooks and a skip of the ninth. The script's own header records the same gap for
rehearsal of 7 hooks and a skip of the eighth. The script's own header records the same gap for
a PR merged through Gitea's merge button, where no local push happens at all.
## The pre-push gate
Nine hooks, grouped below by what they guard rather than by the order `.pre-commit-config.yaml` declares them in.
Eight hooks, grouped below by what they guard rather than by the order `.pre-commit-config.yaml` declares them in.
**Core checks**
@@ -50,7 +50,6 @@ Nine hooks, grouped below by what they guard rather than by the order `.pre-comm
| Hook | Guards |
|---|---|
| `check-vale-style-sync` | skill-audit's Vale copy matches agent-audit's canonical copy, plus six glob-coverage probes (see [Vale](#vale)) |
| `check-scope-walkup-sync` | `validate.sh`, `validate-provenance.sh`, `new-agent.sh` and `new-skill.sh`'s four independent `$HOME`/`.git`/`apm.yml` walk-up ports still agree behaviorally |
| `check-executables-allow-sync` | root `apm.yml`'s `executables.allow` key names kyberforge's actual version (see [apm gates](#apm-gates)) |
@@ -61,7 +60,7 @@ drift in generated text.
| Hook | Guards |
|---|---|
| `check-apm-agents-valid` | runs agent-audit's `validate.sh` over every real `plugins/*/.apm/agents/*.agent.md` (see [Agent files](#agent-files-take-the-description-gates-not-the-body-gate)) |
| `check-apm-agents-valid` | runs `factory-audit`'s `validate.sh` over every real `plugins/*/.apm/agents/*.agent.md` (see [Agent files](#agent-files-take-the-description-gates-not-the-body-gate)) |
**apm's own gates**
@@ -204,9 +203,9 @@ gets promoted.
### Target resolution walk
Resolution walks up **from the file being checked** — never from the script's own location. Deriving
it from `${BASH_SOURCE}` leaked holocron's 39-skill universe into every consumer repo running the
hook through pre-commit, so a consumer skill routing to `skill-audit` resolved against a plugin it
had never installed.
it from `${BASH_SOURCE}` leaked holocron's own skill universe into every consumer repo running the
hook through pre-commit, so a consumer skill routing to a holocron skill (`skill-audit` at the time,
now `factory-audit`) resolved against a plugin it had never installed.
The walk finds an **authoring root**: the nearest ancestor holding `plugins/*/.apm/skills` or
`plugins/*/.apm/agents`, falling back to the nearest ancestor holding `.git`. **Two passes, not one
@@ -354,14 +353,19 @@ findings.
### Duplicated constants
`skill-audit`'s `validate.sh` holds a second copy of the four ADR-0020 constants
(`DESC_SUGGEST_CHARS` / `DESC_MAX_CHARS` / `BODY_SUGGEST_WORDS` / `BODY_MAX_WORDS`), and
`agent-audit`'s `validate.sh` holds a third copy of the two description constants. They are copied
rather than imported because a cache-installed plugin's scripts cannot read files outside their own
plugin directory. `tests/test-skill-size-check.sh` asserts the copies agree, so drift fails CI rather
than silently letting an audit bless a skill the commit hook then rejects. The shared boundary
resolver block is embedded verbatim in all three scripts between `BEGIN`/`END ADR-0020 SHARED
BOUNDARY RESOLVER` markers and must stay byte-identical.
`factory-audit`'s `validate.sh` holds a second copy of the four ADR-0020 constants
(`DESC_SUGGEST_CHARS` / `DESC_MAX_CHARS` / `BODY_SUGGEST_WORDS` / `BODY_MAX_WORDS`) — the two
description constants apply to both artifact types it handles, the two body constants only to
skills. They are copied rather than imported because a cache-installed plugin's scripts cannot read
files outside their own plugin directory. `tests/test-skill-size-check.sh` asserts the copies agree,
so drift fails CI rather than silently letting an audit bless a skill the commit hook then rejects.
**The shared boundary resolver is now two copies, not three** (ADR-0025). `scripts/skill-size-check.sh`
still carries it embedded between `BEGIN`/`END ADR-0020 SHARED BOUNDARY RESOLVER` markers; the two
plugin copies that used to sit inside `skill-audit`'s and `agent-audit`'s `validate.sh` collapsed
into the single `factory-audit/scripts/lib-boundary-resolver.sh`, sourced by that skill's scripts.
The two remaining copies must still stay byte-identical — a plugin script cannot source the root
one, which is the constraint that forces a copy to exist at all.
### `python3` and PyYAML are hard requirements
@@ -387,7 +391,7 @@ fold.
## Agent files take the description gates, not the body gate
`check-apm-agents-valid` runs agent-audit's `validate.sh` over every real
`check-apm-agents-valid` runs `factory-audit`'s `validate.sh` over every real
`plugins/*/.apm/agents/*.agent.md`. It derives its expected file set from `git ls-files` — the pattern
`tests/run-bats.sh` established — so an agent file deleted from the worktree but still tracked fails
the run, and **discovering zero agent files is an error, not a pass**. An untracked *new* agent file
@@ -399,12 +403,13 @@ against synthetic `mktemp` fixtures — it had never run against the agent files
how ADR-0016 could be amended to bless a `disallowedTools` frontmatter field while `validate.sh`'s
allowlist still rejected it: spec and enforcer disagreed and every gate stayed green.
Agents take the ADR-0020 **description** gates (agent-audit's `validate.sh` holds its own copy of
Agents take the ADR-0020 **description** gates (`factory-audit`'s `validate.sh` holds its own copy of
those two constants) and, deliberately, **no body word gate**. A skill body is loaded into the
caller's context and competes with the live conversation; an agent body becomes the system prompt of
a *fresh* context. The rationale for the 900-word FAIL does not transfer. A bats test pins that
absence in agent-audit's validator — adding a body gate there contradicts the ADR rather than fixing
an inconsistency.
absence for the agent path of `factory-audit`'s validator — adding a body gate there contradicts the
ADR rather than fixing an inconsistency. The merge did not change this: the validator auto-detects
the target type, and the body gate applies on the skill path only.
**Be precise about the scope of that guarantee: it holds for the *validator*, not for the shared
script.** `scripts/skill-size-check.sh` applies its body gate to whatever path it is handed, and
@@ -424,7 +429,7 @@ knows the difference; doing so silently enforces a gate ADR-0020 declines to set
## Current retrofit status
The ADR-0020 gates ship hot, with no baseline file — a shrinking baseline was considered and
rejected. The corpus is currently clean on both: 0 of 39 descriptions/bodies exceed their FAIL tier,
rejected. The corpus is currently clean on both: 0 of 38 descriptions/bodies exceed their FAIL tier,
0 dangling targets, 0 `Kyberforge.CompositionNote` (Vale) errors. History: issue #99.
Nothing is grandfathered — a new skill, or an edit that crosses a FAIL tier, is blocked on first
@@ -503,7 +508,7 @@ boundary, and a stricter form would only move the same trust to a different stri
in list items, not fences. Those are clause-1 sites the gate cannot see, because it cannot
distinguish them from clause-2 mentions in the same list.
- **`README.md`, excluded by pattern.** A skill-directory README is consumer-facing prose no agent
loads, and the `git clone https://github.com/bats-core/…` lines in the seven `tests/README.md`
loads, and the `git clone https://github.com/bats-core/…` lines in the six `tests/README.md`
files are setup instructions for a third party who has no `rtk`. Prefixing those would be actively
wrong, not merely noisy — see ADR-0023's consumer section.
- **Quoting.** The line splitter breaks on `;`, `|`, `&&`, `||`, `$(` and backticks without tracking
@@ -526,29 +531,92 @@ it was written for.
Install the `vale` binary — `brew install vale` (macOS), `snap install vale` (Linux),
`choco install vale` (Windows), or see <https://vale.sh/docs/vale-cli/installation/>. No `vale sync`
is needed: the `Kyberforge` styles are **committed** under
`plugins/kyberforge/.apm/skills/{skill-audit,agent-audit}/assets/vale/styles/`, not downloaded
packages (ADR-0014).
`plugins/kyberforge/.apm/skills/factory-audit/assets/vale/styles/`, not downloaded packages
(ADR-0014).
### Two copies, one canonical
### One copy, one config
Wiring Vale as a deterministic prefilter for `skill-audit`/`agent-audit`'s Description dimension
(motivation: issue #84) is repo-specific, not part of the generic `lint` plugin, so it does not live
in `plugins/lint/` — and per ADR-0014 it no longer lives at the repo root either. It lives **twice**,
one copy per skill, both under `plugins/kyberforge/.apm/skills/`:
Wiring Vale as a deterministic prefilter for `factory-audit`'s Description dimension (motivation:
issue #84) is repo-specific, not part of the generic `lint` plugin, so it does not live in
`plugins/lint/` — and per ADR-0014 it no longer lives at the repo root either. It lives **once**,
under `plugins/kyberforge/.apm/skills/factory-audit/assets/vale/`, carrying both the `Kyberforge`
and `KyberforgeCopilot` styles and a single `.vale.ini` with all three glob sections:
`[**/SKILL.md]`, `[**/agents/*.md]`, `[**/*.agent.md]`.
| Copy | Styles | `.vale.ini` sections |
|---|---|---|
| `agent-audit/assets/vale/` — **canonical** | `Kyberforge`, `KyberforgeCopilot` | `[**/agents/*.md]`, `[**/*.agent.md]` |
| `skill-audit/assets/vale/` — smaller duplicate | `Kyberforge` | `[**/SKILL.md]` |
ADR-0014 split this into two skill-scoped copies because a plugin's cache-install copies only each
skill's own files and `skill-audit` could not reach across the skill boundary into `agent-audit`'s
copy. Merging the two audit skills removed the boundary, so the copy went with it and the single-file
`.vale.ini` ADR-0014 split apart is restored (ADR-0025). `KyberforgeCopilot` stays scoped to
`.agent.md` files alone, for the Copilot-only "`Use proactively` has no effect" check.
Duplicated rather than shared because a plugin's cache-install copies only each skill's own files —
there is no cross-skill sharing to point at. `check-vale-style-sync` at pre-push is what keeps them
from drifting; `KyberforgeCopilot` is the one deliberate inequality, being scoped only to `.agent.md`
files for the Copilot-only "`Use proactively` has no effect" check.
With one copy there is nothing left to diff, so the `check-vale-style-sync` pre-push hook,
`scripts/check-vale-style-sync.sh` and `tests/test-check-vale-style-sync.sh` are deleted — one hook
off the push gate. **Read what went with it, not just what became vacuous.** The script had 17
assertion sites. ADR-0025 maps each one; the short version follows.
**Genuinely moot (6):**
- the `vale-wrap.sh` diff and the `styles/Kyberforge/` diff, which compared two copies that are now
one;
- the four hard-fail guards that located those copies (`REPO_ROOT` is a directory, the `.apm/` paths
are not stale, neither copy is missing).
Its `StylesPath` and `BasedOnStyles` checks were **not** diffs. They were per-file greps of each
`.vale.ini`, so they survive: case 0 below checks that the config loads, and case 28 checks that the
`Kyberforge` style is actually loaded.
**Rehomed or ported (11).** The largest group is the **six-row glob-coverage probe table**, which
invoked `vale --config` on one representative path per file shape. It was the only assertion
anywhere that catches a `.vale.ini` glob typo (`[**/SKILL.md]` → `[**/SKILLS.md]`), the failure mode
where every other check stays clean while Vale lints zero files. One config does not make that
impossible: a typo in any one of the three sections still 0-file-skips that shape.
**Case 0** runs before any Vale-dependent case and needs no Vale binary. It asserts that the shipped
`.vale.ini` exists and is readable, sets a `StylesPath` that resolves to a directory, and names only
styles that ship. A config that cannot load used to surface as nine generic "vale printed no summary
line" failures across cases 28–31. It now fails once, names the cause, and holds the Vale-dependent
cases back.
The probes now live in `tests/test-vale-wrap.sh` (cases 28–30), rehomed against the merged config:
one representative path per file shape, each asserted to produce a Vale scan of more than zero files
*and* a Kyberforge alert (case 28). Case 28 also checks that each probe path is in scope of a
published vale hook, and that every `.vale.ini` section has a probe row. Its Part B drops
`Kyberforge` from each section's `BasedOnStyles` in a copy and requires that section's probes to
fail as "style not loaded". Case 29 is a mutation case: it typos each section in a copy of the
assets and requires that section's isolating probes to drop to zero. Case 30 asserts that
`KyberforgeCopilot` reaches `.agent.md` files alone. Its Part B requires both an unload (dropped from
`[**/*.agent.md]`) and a leak (added to `[**/SKILL.md]`) to fail. Case **31** is the third class that went with the
script and is not a glob probe at all: the per-rule override allowlist, which pins every Kyberforge
rule at a blocking bare `YES`/`error`. It is not redundant with the probes above — those key on
`Kyberforge.VagueWording` and `KyberforgeCopilot.ProactivePhrase`, so the other four rules
(`DescriptionOpener`, `PaddingPhrase`, `SentenceOpenerThereIs`, `CompositionNote`) can each be
overridden out of `error` underneath a passing probe. That gap is closed.
Two cases cover the hook manifests.
**Case 33** is the original's cross-manifest `files:` drift check, ported. It extracts each vale
hook's `files:` regex from `.pre-commit-hooks.yaml` and from `.pre-commit-config.yaml`
*independently*, compares them per hook and never as a union, and asserts that each shared probe path
is in scope of both or neither. The original selected each hook's record by matching `entry:`
against the owning skill's `vale-wrap.sh` path. After the merge both hook IDs share one `entry:`, so
the port pairs them by `id:` from an explicit table: `kyberforge-vale-audit-skill` ↔
`vale-audit-prefilter-skill`, and `kyberforge-vale-audit-agent` ↔ `vale-audit-prefilter-agent`. A
missing hook id or a class with no shared probe fails by name. Part B requires three mutations to
fail: the skill hook narrowed to one plugin, the agent hook narrowed the same way, and a renamed
local hook id.
This was briefly a real hole. Narrowing `vale-audit-prefilter-skill` from `^plugins/[^/]+/...` to
`^plugins/kyberforge/...` left 6 of 38 skills prefiltered, and the whole suite green, before case 33
existed.
**Case 32** covers the separate zero-match question on the local manifest alone. Each
`.pre-commit-config.yaml` vale hook's `files:` regex must still match at least one tracked file, and
every path it matches must be in that hook's own artifact class. A hook narrowed to zero files never
runs, and pre-commit reports no error.
### What Vale owns, and what stays LLM judgment
Eleven rule files across the two copies, six distinct rules:
Six rule files, six distinct rules:
| Rule | Vale scope | Bans | From |
|---|---|---|---|
@@ -591,7 +659,7 @@ analogue here — Vale has no tier to make audible.
### External consumers: the root `.pre-commit-hooks.yaml`
The root `.pre-commit-hooks.yaml` exposes both Vale copies (`kyberforge-vale-audit-skill`,
The root `.pre-commit-hooks.yaml` exposes two Vale hook IDs (`kyberforge-vale-audit-skill`,
`kyberforge-vale-audit-agent`) plus `kyberforge-skill-size-check`, so any external repo can enforce
the same rules with `repo: <this-repo-url>, rev: <tag>` in its own `.pre-commit-config.yaml`.
pre-commit clones the pinned rev into its own cache, independent of whether Claude Code or the
@@ -599,8 +667,14 @@ pre-commit clones the pinned rev into its own cache, independent of whether Clau
--all-files`. `skill-size-check` has no external asset dependency, so it needed no relocation under
ADR-0014 — only exposure.
**The two IDs survive the merge even though they now point at the same wrapper.** Both
`kyberforge-vale-audit-skill` and `kyberforge-vale-audit-agent` keep their IDs and their `files:`
regexes, because an external repo pins them by name in its own `.pre-commit-config.yaml` and
collapsing them to one would break every such consumer silently. What changed is only the `entry:`
target: both now name `factory-audit/scripts/vale-wrap.sh`.
This repo's own `vale-audit-prefilter-skill` / `-agent` hooks consume the **identical**
plugin-bundled copies via `repo: local`. Deliberately not a third root copy, and deliberately **not a
plugin-bundled copy via `repo: local`. Deliberately not a second root copy, and deliberately **not a
pinned self-reference** — a pinned self-reference would lint working-tree edits against the last
tagged release rather than against the change being made.
@@ -619,18 +693,22 @@ vendored research-corpus `SKILL.md` files match neither pattern (see
for `skill-size-check`), so prose findings surface only when you edit a file this repo actually
authors. Without the binary the hooks fail with a bare "command not found" and no install pointer.
**Two hooks, not one combined hook.** Both manifests split the prefilter in two precisely because a
single hook can point at only one copy, and that copy would silently 0-file-skip the other file
shape (see [A 0-file Vale run is NOT RUN](#a-0-file-vale-run-is-not-run)).
**Two hooks, not one combined hook — for a different reason than ADR-0014 gave.** The original
reason was mechanical: with a config per skill, a single hook could point at only one copy and would
silently 0-file-skip the other file shape (see
[A 0-file Vale run is NOT RUN](#a-0-file-vale-run-is-not-run)). One `.vale.ini` carrying all three
sections removes that constraint. The split stays anyway because the two IDs are an exported
contract external consumers pin by name, and because the `files:` regexes still have to differ —
each hook hands Vale only the file shape it is scoped to.
### The `.vale.ini` globs do no scoping
Each `.vale.ini`'s section globs are **path-agnostic** — `[**/SKILL.md]` for skill-audit's copy,
`[**/agents/*.md]` and `[**/*.agent.md]` for agent-audit's — and constrain filename *shape*, not
The `.vale.ini`'s section globs are **path-agnostic** — `[**/SKILL.md]`, `[**/agents/*.md]` and
`[**/*.agent.md]` — and constrain filename *shape*, not
location: Vale's `*` crosses `/`. A `SKILL.md` outside `plugins/` (a project-scope
`.claude/skills/foo/SKILL.md`, say) still matches `[**/SKILL.md]` and gets linted normally.
All scoping therefore comes from the pre-commit hook's own `files:` regex and from the audit skills
All scoping therefore comes from the pre-commit hook's own `files:` regex and from `factory-audit`
passing one explicit file per invocation. The two manifests scope **differently on purpose**:
| Manifest | `-skill` | `-agent` |
@@ -638,8 +716,10 @@ passing one explicit file per invocation. The two manifests scope **differently
| `.pre-commit-config.yaml` (pins this repo's layout) | `^plugins/[^/]+/\.apm/skills/[^/]+/SKILL\.md$` | `^plugins/[^/]+/\.apm/agents/[^/]+\.agent\.md$` |
| `.pre-commit-hooks.yaml` (layout-agnostic for consumers) | `(^\|/)SKILL\.md$` | `(^\|/)agents/[^/]+\.md$\|\.agent\.md$` |
Narrowing a `.vale.ini` glob to a `plugins/`-shaped path to "tighten" it breaks the consumer case,
and `check-vale-style-sync`'s probe set is built to catch exactly that.
Narrowing a `.vale.ini` glob to a `plugins/`-shaped path to "tighten" it breaks the consumer case.
`check-vale-style-sync`'s probe set was built to catch exactly that; it moved to
`tests/test-vale-wrap.sh` with the hook's deletion, and two of the six probes exist specifically to
pin this location independence — see [One copy, one config](#one-copy-one-config).
### The blind spot: `references/` is unlinted, for two independent reasons
@@ -647,16 +727,19 @@ Every `references/*.md` file in the corpus is outside the prose gate. Count them
`git ls-files | grep -cE '^plugins/[^/]+/\.apm/skills/[^/]+/references/.*\.md$'` rather than reading
a figure here; it moves with every retrofit. This is the gap that matters most, because the context
contract's own remedy for an over-long body is to move prose **into** `references/` — the gate pushes
text across its own boundary and then stops watching it.
text across its own boundary and then stops watching it. `factory-audit` is the live example. Its
dispatch body keeps only the gotchas common to both flows, and the flow-specific gotchas live under
`## Gotchas` in `references/skill-flow.md` and `references/agent-flow.md` (ADR-0025). Handing both
flow files to `vale-wrap.sh` prints `0 errors … in 0 files` and exits 0.
**Closing either cause alone changes nothing.** There are two, and they are independent:
| Cause | Where | Effect on a `references/` file |
|---|---|---|
| the `Kyberforge` style is scoped `[**/SKILL.md]` | `skill-audit/assets/vale/.vale.ini` | matches no section, so Vale lints 0 files and exits 0 |
| the `Kyberforge` style is scoped `[**/SKILL.md]` | `factory-audit/assets/vale/.vale.ini` | matches no section, so Vale lints 0 files and exits 0 |
| the hook's `files:` regex is `^plugins/[^/]+/\.apm/skills/[^/]+/SKILL\.md$` | `vale-audit-prefilter-skill` in `.pre-commit-config.yaml` | the file is never handed to Vale at all |
Verified both ways. Handing skill-audit's `vale-wrap.sh` a reference file directly — bypassing
Verified both ways. Handing `factory-audit`'s `vale-wrap.sh` a reference file directly — bypassing
pre-commit entirely, so only the style scope is in play — prints `0 errors … in 0 files` and exits 0,
where the same wrapper on a `SKILL.md` reports `in 1 file`. And the hook's `files:` regex, applied to
`git ls-files`, selects only the skill-directory `SKILL.md` files scoped at the top of this page, so
@@ -672,8 +755,8 @@ The consumer manifest is a third axis and does not rescue this either: `.pre-com
### `vale-wrap.sh`, never bare `vale`
Both audit skills' Step 1 and both pre-commit hooks call **each copy's own**
`scripts/vale-wrap.sh`, not `vale`. It works around a confirmed **Vale 3.15.2** limitation:
`factory-audit`'s Step 1 and both pre-commit hooks call
`factory-audit/scripts/vale-wrap.sh`, not `vale`. It works around a confirmed **Vale 3.15.2** limitation:
`text.frontmatter.description` silently stops matching on most — not all — multi-line descriptions.
Verified by reproduction on a deliberately-bad fixture, not assumed:
@@ -718,8 +801,9 @@ alongside it would resolve against the cwd instead, yielding `E100 Runtime error
and exit 2 — which both skills' fallback misreads as "vale unavailable" and silently downgrades to
full LLM judgment.
`tests/test-vale-wrap.sh` regression-tests this against **skill-audit's** copy specifically: its
fixtures are all `SKILL.md`-shaped, and only skill-audit's `.vale.ini` carries that glob section.
`tests/test-vale-wrap.sh` regression-tests this against `factory-audit`'s copy — the only one left.
Its fixtures are all `SKILL.md`-shaped, and that copy's `.vale.ini` carries the matching glob section
along with the two agent ones.
### A 0-file Vale run is NOT RUN
@@ -736,25 +820,18 @@ clean.
### Pre-push
`vale` is a **pre-push** dependency too, not only pre-commit. `check-vale-style-sync` runs **six
glob-coverage probes** by invoking `vale --config` — one representative path per file shape the
prefilter is supposed to cover. They are the only assertions in the script that catch a `.vale.ini`
glob typo (`[**/SKILL.md]` → `[**/SKILLS.md]`), the failure mode where every text-level check stays
clean while vale lints zero files. As a warning this self-disabled on exactly that mutation and
exited 0, and since pre-commit swallows a passing hook's output the stderr line was never seen — the
hook reported `Passed`. Missing `vale` is therefore a hard failure here.
`vale` is still a **pre-push** dependency, but no longer through a hook of its own.
`check-vale-style-sync` — the hook that ran the six glob probes, and whose
`CHECK_VALE_STYLE_SYNC_ALLOW_MISSING_VALE=1` opt-out downgraded them audibly rather than skipping the
hook — is deleted with the second Vale copy (ADR-0025). The six glob probes survive it inside
`test-vale-wrap.sh`, so `run-tests --strict` is now the gate that runs them. That is also what keeps
`vale` a pre-push requirement: `test-vale-hooks-consumer.sh` exits 77 without the binary, and so does
`test-vale-wrap.sh` once its static cases pass, and a skip fails the push.
The opt-out is `CHECK_VALE_STYLE_SYNC_ALLOW_MISSING_VALE=1`, and **it is not `SKIP=`**: the hook
still runs and still asserts everything verifiable from file text, but the six probes do not, and its
summary says so explicitly —
```
Vale style sync check passed (text-level only, vale unavailable): … 0 glob probe(s) verified.
```
Use it only on a machine that genuinely cannot install `vale`, and read that line as "the glob axis
was not checked", not as a pass. The hook is `verbose: true` for exactly that reason — its clean
output is a single line, so it costs one line per push.
`test-vale-wrap.sh` without Vale skips only its Vale-dependent cases, not the whole suite. The cases
that are plain greps and awk over the config and the two hook manifests still run: case 0, 16, 26,
27, the static halves of 28, 31 Parts A and B, 32 and 33. A static failure exits 1, because a real
defect is not a setup error. Only an all-static-pass run exits 77.
### Mentioning banned phrasing without tripping the rule
@@ -780,9 +857,10 @@ run. The pre-push hook invokes the same script as `--strict` (`RUN_TESTS_STRICT=
where a skip **does** fail the push: at pre-push a skip means one of the documented dependencies is
absent on this machine, so the gate would otherwise report success having run fewer suites than it
appears to. Without `--strict` the gate once went green having verified 15 of 17 suites on a
vale-less PATH, with the skip list swallowed. Without vale, three suites skip —
`test-check-vale-style-sync.sh`, `test-vale-hooks-consumer.sh`, `test-vale-wrap.sh` — and the strict
failure names each one and what to install.
vale-less PATH, with the skip list swallowed. Without vale, two suites skip —
`test-vale-hooks-consumer.sh` and `test-vale-wrap.sh` — and the strict failure names each one and
what to install. (It was three until `test-check-vale-style-sync.sh` was deleted with its hook; see
[One copy, one config](#one-copy-one-config).)
`tests/run-bats.sh` derives the set of `.bats` files it expects from `git ls-files`, so a `.bats`
file deleted from the worktree but still tracked in the index fails the run rather than silently
@@ -903,6 +981,9 @@ this remote before any network call.
- `docs/adr/0015-apm-replaces-plugin-marketplace-authoring.md`,
`docs/adr/0014-vale-prefilter-ships-from-the-plugin.md` — apm-generated manifests, committed Vale
styles
- `docs/adr/0025-skill-audit-and-agent-audit-merge-into-factory-audit.md` — the audit-pair merge that
collapsed the two Vale copies to one, removed the `check-vale-style-sync` hook, and took the shared
boundary resolver from three copies to two. It amends ADR-0014 and ADR-0020 on those points
- `docs/spec/architecture.md` — directory structure, install pipeline, what is generated and what is
hand-authored
- `.pre-commit-config.yaml` — the hooks themselves, with inline rationale comments