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

@@ -0,0 +1,209 @@
---
source_keys:
- agentskills-spec
- agentskills-best-practices
---
# Body Discipline Reference
Upstream source: agentskills.io — skill-authoring, best-practices.
House contract: the context budget.
## The core test
For every sentence in the body, ask: **"Would the agent get this wrong without this instruction?"**
If no — cut it. The agent already knows it from general training. Adding it wastes tokens and
dilutes the signal of what matters.
## What the body is for
The body carries the **decision procedure only**: ordered steps, decision branches, gates, and
which reference to load when.
Include content the agent lacks:
- Project-specific conventions and domain procedures it cannot infer
- Non-obvious edge cases and environment-specific gotchas
- The specific tools or sequences to use — not the full range of options
- One default per decision point with one escape hatch
Move to `references/`, behind an explicit "If X, read `references/<file>.md`" trigger — the literal
conditional form, never a generic pointer. Write the real filename in the skill under audit; the
angle brackets are a placeholder here, and a literal `references/file.md` in a body is an ERROR
from the gate because no such file exists on disk.
**A dispatch table satisfies this requirement on its own.** A table row already pairs a condition
with a target, which is exactly what the literal form encodes; restating each row underneath as a
prose conditional duplicates the routing in the one body whose whole purpose is to be short. Where a
body dispatches, audit the table for condition/target completeness and stop there — do not require
the conditional form as well. The literal form is what a body needs when it loads a reference
*without* a dispatch table: a single mid-procedure deepening, an escape hatch, an error path.
Move:
- Lookup tables and spec restatements
- Output schemas, templates and example blocks
- Rationale and justification prose
- Anything only one branch of the procedure ever reaches
Do not include at all:
- Concepts the agent already knows (what JSON is, how HTTP works, what a CSV is)
- Exhaustive option lists — pick a default; the agent does not benefit from choosing
- Steps the agent handles independently — over-specifying leads to unproductive paths
- Restatements of the description, which is already in context
## Two length families, measured differently
Do not conflate these, and do not report them as one finding.
| Gate | SUGGESTION | FAIL | Counts |
|---|---|---|---|
| Body budget (house) | 600 words | 900 words | the **body only** — everything after the frontmatter's closing `---` |
| Spec conformance (agentskills.io) | — | 2,770 words / 500 lines | the **whole file**, frontmatter included |
The 2,770-word ceiling is a token-conformance backstop calibrated to the densest prose in the
corpus; it says nothing about quality and a file can sit a thousand words inside it while failing
the body budget. The 900-word ceiling is the quality gate: a body is loaded into the caller's live
context and competes with the conversation already there. `validate.sh` reports both. Cite whichever
one actually fired.
A word count cannot detect the defect it stands in for. Treat both numbers as backstops to the
dispatch rule and the Gotchas constraint below, never as a substitute for them.
## Dispatch is mandatory at two or more mutually exclusive flows
If a skill handles two or more flows that a single invocation cannot both take — separate
subcommands, separate input types, separate lifecycle stages — the body carries a **dispatch
table** plus the gates common to every branch, and each flow lives in its own self-contained
`references/` file. Inlining all of them is a FAIL regardless of word count, because every
invocation then pays for every branch it did not take.
The reference shape in this repo is `apm-workflow`: a **294-word body** dispatching to 3,154 words
of references across five mutually exclusive flows. Its whole-file count is 348 words — cite 294
when calibrating a body, or the conflation this section warns against reappears in the finding
itself. The 3,154 counts the five flow files only; `references/sources.md` is a provenance record
and is never loaded at runtime, so counting it inflates the dispatched total.
### What earns the wiring exemption
A dispatch table earns the exemption above on its properties, not on which skill it appears in.
Audit any dispatching body against these four:
- Every flow the skill handles has a row, and every row names a target file that exists on disk.
- Each row pairs a condition the agent can evaluate from the request with exactly one target. A row
keyed on a literal slash invocation fails this: a model-invoked activation never produces that
string, so the routing silently falls to whatever else the row carries.
- One line after the table tells the agent to read the file its row matched, and only that one.
- The gates every branch needs sit in the body, not inside one flow's file — see the reachability
precondition below.
A table missing any of the four is not exempt, and the literal-conditional requirement applies to it
as written. The exemption covers the wiring form only: every other rule in this file applies to a
dispatching skill exactly as it applies to any other.
## Gotchas sections
The highest-value construct in a body, and the easiest to fill with noise. A Gotcha must state a
fact that **contradicts a reasonable default** — something the agent gets wrong precisely by acting
sensibly.
```markdown
## Gotchas
- The `users` table uses soft deletes. Always include `WHERE deleted_at IS NULL`.
- User ID is `user_id` in the database, `uid` in auth, `accountId` in billing. Same value.
```
Constraints:
- **More than five entries is a SUGGESTION** — five is the guideline, not a ceiling. Past five, the
section is usually a summary of the body rather than a set of traps, and the agent stops reading
it as a warning. It stays advisory because whether a given gotcha earns its place is judgment;
`validate.sh` emits it through `suggest()` and the run still exits 0.
- **A Gotcha that paraphrases a step in the body below it is a FAIL.** It has no independent
content, and it teaches the agent that Gotchas can be skimmed because the real instruction is
coming. This one is the auditor's call — no script detects it. The Fix is conditional: delete the
Gotcha only if the surviving copy is reachable from every branch that needs it — see the
reachability precondition below.
- **A Gotchas section exceeding 25% of the body is a SUGGESTION** — the body has been inverted into
a preamble. Same tier and same reasoning as the entry count, and independent of it: either can
fire without the other.
- Place the section near the top. A gotcha read after the mistake is worthless, which is also why
Gotchas is the one construct exempt from moving to `references/`.
Worked negative example — **`git-commits` v0.1.2 at commit `5e23250`, a fixed pre-retrofit
snapshot, not the current file.** The live skill is v0.1.3 and matches none of the citations below;
they are quoted as they stood in that snapshot, and are not to be refreshed against
`HEAD`. The snapshot is reachable only from a checkout of the authoring repo — an installed plugin
cache holds no git history and no such path — so read the citations below as quoted rather than
going to look for the file. From a checkout:
```text
git show 5e23250:<the git plugin>/.apm/skills/git-commits/SKILL.md
```
That body carried twelve Gotchas, four of which restated content already below them or already in
the description:
| Gotcha | Restates |
|---|---|
| `:31` "SemVer mapping is not optional" | the description |
| `:32` "Confirmation gates are mandatory for destructive operations" | step 9 at `:52` |
| `:33` "Never skip hooks with `--no-verify`" | step 9 at `:52` |
| `:36` "Never commit secrets" | step 2 at `:45` |
All four are FAILs under the paraphrase rule. The entry count and the section's share of the body
(387 of 1,102 words, 35%) are two further SUGGESTIONs on top — the script reports both, and neither
fails the run on its own. What makes this worth auditing directly is that the four paraphrase FAILs
pass every word gate there is; only reading the construct finds them.
### The paraphrase rule has a reachability precondition
**A Gotcha that restates a step may be deleted only when the surviving copy is reachable from every
branch that needs it.** In a dispatch body it usually is not: each flow file is loaded alone, so a
step in one is invisible to an invocation that took another branch. When the restated rule is a
safety gate more than one flow needs, the Fix is to **move it into the body's common-gates section**,
never to drop it in favour of the per-flow copy.
Row four is the case that proves it. Following the rule literally, the retrofit deleted the
always-loaded secrets Gotcha and kept step 2 of `references/create-commit.md` — but `git-commits`
dispatches to exactly one flow file, and `references/rewrite-history.md` stages changes and runs
`--amend`, which commits newly staged content exactly as a fresh commit does. A grep for `secret`
across the skill in that state returned one hit, on a path two of three branches never reach: that
branch could commit a credential with no check anywhere in its loaded context, against this repo's
governance hard prohibition. v0.1.3 carries the rule as gate 2 of "Gates on every flow" instead.
So check reachability before writing the Fix. Rows one to three are unaffected — the description is
loaded on every invocation, and confirmation is likewise a common gate rather than a per-flow step.
## Calibrating control
**Be prescriptive** when operations are fragile, consistency matters, or a specific sequence must be
followed:
```markdown
Run exactly:
\`\`\`bash
python scripts/migrate.py --verify --backup
\`\`\`
Do not modify the command or add additional flags.
```
**Give freedom** when multiple approaches are valid. Explaining *why* outperforms rigid directives —
agents make better decisions when they understand the purpose.
## Defaults not menus
Never present a list of equivalent options — pick one and mention the alternative briefly:
```markdown
# Too many options
Use pypdf, pdfplumber, PyMuPDF, or pdf2image...
# Default with escape hatch
Use pdfplumber for text extraction. For scanned PDFs requiring OCR, use pdf2image instead.
```
The FAIL and SUGGESTION criteria for this dimension live in `references/skill-finding-criteria.md`,
which Step 3 loads on every run.