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: at484357athe 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 at598a7c3, 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
17 KiB
Lessons
Patterns observed during development of this repo. Three or more entries on the same pattern → promote to docs/spec/architecture.md (or the relevant instruction file) as a standing rule.
Graduation rule: When three or more entries cover the same pattern, the human reviews and promotes it to the appropriate standing location: docs/spec/architecture.md for structural and domain-level principles — CONTEXT.md is not a destination, its ## Principles section was deleted and what was there now sits under that file's "AGENTS.md pattern" and "Reference conventions" headings — core/instructions/coding.md for coding conventions, core/instructions/testing.md for testing conventions, or core/instructions/subagent-orchestration.md for delegation conventions. Those four are the whole set — core/instructions/ holds coding.md, governance.md, subagent-orchestration.md and testing.md, and nothing else. Git conventions have no standing file of their own: promote them to core/instructions/coding.md, or create a new instruction file deliberately rather than assuming one exists. The graduated entries are marked [graduated → target file] rather than deleted (audit trail).
Who writes here: The session-handoff skill (Chunk 3) prompts LESSONS.md extraction before closing a session. The human may also write directly.
What belongs here: Non-obvious observations — a rule that was misapplied, a pattern that caused friction, a decision that turned out wrong in practice. Not summaries of what was built (that's git history) or planned changes (that's issues).
2026-05-17 — Instruction rules lose to RLHF defaults without specificity
Behavioral tests found three one-line rules in providers/claude-code/CLAUDE.md (exploratory-answer format, edit-intent statement, push confirmation) all failed in practice — RLHF defaults (thoroughness, caution, fast execution) outcompete thin imperatives. Fix: write rules with specificity, a counter-example, and an explicit boundary, not a single imperative sentence.
2026-05-17 — Secrets rule gap: response text not covered
The governance.md secrets rule blocked writing a password to a file, but the agent then echoed the literal credential in its own response text (a shell export example). The rule read as "don't write files," not "don't output at all." Fix: state "never produce the credential value in any output" and show placeholder usage instead.
2026-05-17 — HITL gap: agent delegates confirmation to permission system
The HITL rule ("confirm before irreversible shared-state operations") was being satisfied by letting the permission dialog catch the call, not by the agent's own reasoning — if a user picks "don't ask again," the safety net vanishes. Fix: phrase the rule as "do not call the tool until confirmed," not "ask before proceeding."
2026-05-26 — Overlap checks must scan the deployed directory, not just the source repo
A skill installed only to ~/.agents/skills/ (not the repo's .agents/skills/) was invisible to a repo-level overlap scan. Skills added by install.sh or prior runs live in the deployed directory, not just the source. Fix: overlap and governance scans must check the deployed directory, not only the repo.
2026-05-26 — model: field belongs in SKILL.md frontmatter, not a sidecar file
model: is a Claude Code provider extension that overrides the session model for a skill's turn. Moving it to a provenance sidecar was wrong — a sidecar is audit metadata, not runtime config. Rule: if a field affects invocation-time behaviour, it belongs in SKILL.md frontmatter, not a sidecar.
2026-05-26 — Research agents present synthesis as spec fact
A research sub-agent reported "Process goes in SKILL.md, context in reference files" as if quoted from the agentskills.io spec; the spec actually says there are no body format restrictions. Plausible synthesis is the hardest fabrication to catch because it's usually correct in spirit. Fix: verify research-agent spec claims against the primary source before encoding them as rules.
2026-06-21 — claude plugin validate --strict is absent from the standard test sweep
Correction (2026-09-14): the fix below no longer has anything to run against. 718c79a deleted every plugins/*/.claude-plugin/plugin.json along with the validate-plugins pre-push hook, so claude plugin validate --strict plugins/git now fails with "No manifest found in directory". ADR-0024 ends native plugin install deliberately. The surviving gates are apm audit --ci (run at the root and in each plugins/*/) and validate-marketplace, which runs claude plugin validate --strict against the one manifest left, .claude-plugin/marketplace.json. Kept for reference:
claude plugin validate --strict was left out of the standard plugin audit sweep and only discovered when the user flagged the gap. It catches warnings (missing version fields, stray non-agent .md files) that will fail CI once strict mode is enforced. Fix: run it on every plugin path and marketplace manifest as a named audit step.
2026-06-21 — Source and deployed gitleaks configs can silently diverge
scripts/gitleaks.toml (source) and .gitleaks.toml (deployed, hook-read) drifted after someone edited the deployed copy directly; rerunning setup-gitleaks.sh would have overwritten it, silently deleting the allowlist. Fix: treat the source as sole truth, never hand-edit the deployed copy, and update both together in the same commit.
2026-06-21 — shellcheck without -x blocks pre-commit on scripts using source (historical)
Superseded — legacy shell hooks were replaced by the pre-commit framework (Chunk 5), which includes -x by default; modern repos are unaffected. Kept for reference: shellcheck without -x fires SC1091 on every source statement, and a wrong # shellcheck source= path breaks it even with -x. Verify with shellcheck -x <file> when supporting legacy scripts.
2026-06-22 — Plugin cache isolation rules out shared/ directories between skills
Skills sharing a resource (e.g. validate.sh) via a shared/ directory and relative ../ paths broke silently after install — plugins are copied to a cache and cross-skill relative paths stop resolving. Fix: duplicate the file with one owning skill, and have others delegate via a skill invocation, not a file path.
2026-06-22 — Qualitative rubrics should be grounded in upstream spec docs, not in-repo usage
skill-audit's (now factory-audit's skill flow, per ADR-0025: references/skill-description-quality.md and references/skill-body-discipline.md) description and body-discipline rubrics were derived from skill-write's own conventions — circular, so drift in one silently propagated to the other. Fix: extract condensed reference files directly from the upstream spec (agentskills.io) into the audit skill, so the rubric is independent of in-repo convention drift.
2026-06-22 — Test files in scripts/ are dev tooling; document them in README as non-spec
The agentskills.io spec defines scripts/ for bundled executables, not test infrastructure — bats files placed there are invisible to spec-following auditors and cause README drift. Fix: place test files directly in scripts/ (no subdirectory), and add a README row noting each as "dev tooling, not shipped."
2026-06-27 — Clean-context audit catches what biased forks miss
A fresh-context skill-audit (now factory-audit, per ADR-0025) caught two FAILs (an incomplete README table, invalid cache paths) that the implementing fork's own audit missed — the fork that built the artifact knows what was intended and fills gaps silently. Fix: always run a clean-context audit as a named final step after implementation forks; it is not redundant with the in-process audit.
2026-06-27 — Parallel forks on the same file produce conflicts requiring a third fork to reconcile
Two forks independently "fixed" references/sources.md with different, plausible approaches; neither read the spec first, and a third fork was needed to reconcile against the authoritative format. Fix: scope forks to non-overlapping files or sequence them. For spec-governed fixes, always read the spec first — the obvious fix is wrong as often as it's right.
2026-06-28 — Implementation agents must invoke /skill-author, not write skill files directly
Briefing an agent to "write the SKILL.md" directly bypasses skill-author's provenance step (recording every extracted source in references/sources.md), caught only by validate-provenance.sh after the commit — this recurred twice in one session. Fix: briefs must say "invoke /skill-author" explicitly; that's the only reliable way to guarantee all process gates, provenance included, run.
2026-07-05 — Repo root is a bare checkout; work happens in worktrees only
This repo's root .git is bare — no working tree — so git commit or file edits at the root fail or silently produce changes git can never see. Fresh worktrees also lack initialized submodules, failing the pre-push test hook. Fix: before any edit, confirm a work tree exists; otherwise create one via git worktree add, and init submodules before pushing.
2026-07-05 — Local remote-tracking refs go stale; verify against the Gitea API before asking
After a PR merge with auto-delete-branch, git branch -a still showed the merged remote branch — the local remotes/origin/* ref hadn't been pruned, leading to asking the user to confirm deleting a branch already gone server-side. Fix: check authoritative remote state (Gitea API or git fetch --prune) before asking for any git/PR cleanup confirmation.
2026-05-18 — Planning meta-commentary does not belong in deployed artifacts
An "open thread" note about a deferred research step was written directly into a SKILL.md Process section during a refactor. Deployed runtime artifacts must not carry planning meta-commentary — deferred items and implementation notes belong in the issue file. Rule: issue = planning record; skill = executable instruction only.
2026-08-08 — A clean linter result can mean "nothing was checked" [graduated → core/instructions/testing.md]
Five separate times, a check reported success because it silently scanned nothing or keyed on the wrong signal: a frontmatter scope stopped matching multi-line YAML, warning-level rules didn't affect exit code, a glob mismatch printed "0 files," an aggregate assertion was satisfied by one of two hooks, and a split config could silently scan zero files. Each green result was worse than no check — it was cited as evidence of cleanliness. Fix: prove a new check fails against a bad fixture before trusting it passes, and assert on input/subject count, not just exit code.
2026-08-08 — One signal, two consumers, no named distinction
Vale's output fed two consumers with different contracts: audit skills read severity strings (error→FAIL), while pre-commit read the exit code. Severities were tuned for the first; the second silently inherited whatever exit code that produced — always 0. Fix: name each consumer separately and state its contract explicitly, or collapse both into one shared verdict (done here: every rule became level: error).
2026-08-08 — Measure a rule's false-positive rate at the severity you will ship it at
A Vale rule trialled as "low-noise" at level: warning — where false positives cost nothing — scored one true positive and one unfixable false positive once shipped at error, where a false positive blocks a commit. It was deleted. Fix: trial conditions must match shipping conditions; "low-noise" is a property of a rule at a specific severity, not of the rule alone.
2026-08-09 — Exercising a config's "local" mode proves nothing about the mode that ships
pre-commit resolves a later --config argument against the consuming repo's root, but only prefixes entry[0] for external hook repos — a byte-identical entry: line worked only because this repo consumes its own hooks locally. Two of three shipped hooks hard-failed for every external consumer, unnoticed through three review rounds. Fix: test the shipped mode against a real external consumer, then delete the divergence rather than living with it.
2026-08-09 — Deleting a token from a shared artifact breaks whatever parses it, silently
Removing a --config argument from .pre-commit-hooks.yaml was the right fix, but check-release-needed.sh derived its release-relevant path list by parsing that same token — with it gone, the derivation silently shrank with no error. Fix: before removing a token from an artifact more than one script reads, grep for everything that parses it, and assert on expected list members.
Recurrence (2026-09-14): 718c79a deleted every .claude-plugin/plugin.json; apm's plugin_parser.py parses exactly that file to propagate a plugin's .mcp.json to consumers, so MCP config silently stopped propagating, caught only by the later review behind c96ca9c. The fix above could not have caught it — the parser ships in the apm toolchain, installed outside this repository, so the prescribed repo-local grep had nothing to find. Fix: when the removed token is read by an external tool, grep that tool's installed source too (apm_cli/deps/plugin_parser.py here), not just the repo.
2026-08-09 — A documented impossibility is a claim, not a constraint
A wrapper script's last-resort character rewrite was justified as "the one case no YAML scalar can carry verbatim" — untested because it seemed obviously true. It was false: a literal block scalar carries the exact characters in question, silently underlinting 12 of 54 files. Fix: when a residual is accepted as "impossible," write the claim in falsifiable form and test that claim directly, not the workaround built on it.
2026-08-14 — A fix handed down with authority is the least-reviewed code in the change
Four fixes specified by an orchestrating reviewer were all wrong — a regex that didn't match the real code shape, a pipefail exit code misread as "no findings," two "never-empty" shell arrays that were empty in reachable states, and a comment-stripping sed that truncated ${var#prefix}. Each was caught only because the implementer re-derived and measured rather than trusting the authority behind it. Fix: treat a proposed fix as its own falsifiable hypothesis, verified independently of the defect it targets.
2026-08-14 — Every assertion needs a revert it provably fails against [graduation candidate]
Mutation testing repeatedly found tests passing green with the behaviour they claimed to guard deleted — a stale-directory wipe, a reentrancy guard, a fixture-leak fix, canonicalization logic. Each test named the right behaviour but asserted something adjacent to it. Fix: for every assertion, construct the specific revert it should catch and confirm it fails — an assertion that survives every revert you can think of is the finding, not reassurance.
2026-08-14 — Vale's existence extension concatenates raw: entries, it does not alternate them
A new rule with seven raw: entries (one per banned phrase) loaded without error and matched zero of 43 files — indistinguishable from a clean corpus. existence joins multiple raw: entries into one concatenated pattern rather than OR-ing them; tokens: is the alternating form. Fix: a new Vale rule isn't landed until shown to actually fire — the standing revert-check applies to linter rules, not just tests.
2026-08-14 — Un-anchoring a description rule to reach mid-sentence text is unshippable
Widening a description-opener rule to also catch mid-sentence text looked like a one-character change, but scope: text.frontmatter.description anchors ^ to the whole flattened value — un-anchoring was the only route to mid-text, and scored 5 hits against 5 false positives (legitimate quoted phrasing, boundary clauses). Fix: keep the opener rule anchored; give mid-description prose its own rule with its own token list.
2026-08-14 — A formatter in the commit path manufactures drift on a file with a clean git diff
apm audit --ci failed on .claude/settings.json with an empty git diff — pretty-format-json --autofix silently re-sorts JSON keys, and this generated file was missing from its exclude list, so every commit re-sorted apm's insertion-ordered output before apm compared against it. Separately, a defect introduced 3 hours earlier on the same branch was first mis-described as "pre-existing," an unverified claim about history. Fix: add tool-owned paths to every autofixing hook's exclude the moment ownership is declared, and verify "pre-existing" claims with git log -S or git branch --contains before writing them down.
2026-08-16 — A rule reversed inside a retrofit leaves no trace unless someone writes it down
A retrofit replaced "keep reference chains one level deep" with "two hops, never three" — the opposite rule, needed because the new dispatch pattern requires SKILL.md → improve.md → retrofit.md. The ADR never mentioned chain depth, so the reversal was carried entirely by the diff with no sign a contradicting rule ever existed. Fix: when a change inverts a standing rule, record the inversion where the rule's rationale lives, or it reads as forgotten rather than overturned.