- reach the MCP ${VAR} secrets rule from the compile flow
- restore the Gotcha remedy for type: coverage
- route .apm/instructions and .apm/prompts to primitive-author
- agent-author: add the primitive-author boundary and the already-bumped
skip, bump to 1.0.4
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01KkT7RSDwDbmrM9T34b6sTi
- say apm hook/instruction/prompt so pre-commit hooks stay with pc-author
- restore the idea-without-a-home trigger
- cite checkpoints forge's apm routes actually hit
- diff version bumps against the remote default branch
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01KkT7RSDwDbmrM9T34b6sTi
- annotate the research Must demoted to hook Should 12; narrow Must 5 note
- extend Must 6 to scripts run via an interpreter -c string
- add a fallback dispatch row and comma-joined --target guidance
- fall back when agentsmd-author is not installed
- add the apm-workflow boundary; pin upstream apm source URL
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01KkT7RSDwDbmrM9T34b6sTi
Why: repair_placeholders ran inside a command substitution, so a failing
sed left an emptied file behind and the script still exited 0 reporting
success.
- write via tmp file and abort on sed or mv failure
- re-check the target before moving staging into place
- stage in a dot-prefixed mktemp dir so a killed run leaves no fake skill
- source apm claims in deployment-modes.md; tag untyped code blocks
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01KkT7RSDwDbmrM9T34b6sTi
Why: PR #144 review round 4 reproduced hooks referencing $PLUGIN_ROOT or
${PLUGIN_ROOT} without a path separator passing the audit, although apm
only rewrites ${TOKEN}/ and the deployed hook points nowhere.
- FAIL unbraced or unseparated plugin-root tokens
- check the exec bit for scripts run via an interpreter -c string
- skip env NAME=value prefixes when locating bare relative paths
- correct input: and empty-frontmatter messages, depth-walk applyTo braces
- INFO on unrecognised targets; failing-case tests for untested checks
- document tiers, blind spots and crash exit 2; drop rtk from portable flow
- restore the after-a-hand-edit trigger; pin upstream apm source URL
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01KkT7RSDwDbmrM9T34b6sTi
- factory-audit: hook events judged per deployed target after apm's
rename (Claude/Copilot event sets FAIL, others SUGGESTION); Claude
plugin layouts accepted as hook sources; interpreter options and
sh -c strings checked; bats 378 -> 386
- primitive-author: Must 4/5 match the audit; reference hand-back
points at the right steps; Step 4.2 --target all fallback
- skill-author: new-skill.sh repair only on the template marker line,
so complete skills stay a no-op; provenance and calibration text
- forge: restore "already named" qualifier; drop false HITL claim
- apm-workflow: token example uses an env var
- docs/hooks.md: the apm-hooks.json sidecar is committed, not ignored
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01KkT7RSDwDbmrM9T34b6sTi
- factory-audit: no-op hooks, ./ after interpreters, split-quote and
spaced ${PLUGIN_ROOT} paths, camelCase events in Claude-targeted flat
files, case-insensitive routing stems, and non-string YAML keys are
now caught; input: forms and prompt boundary clauses align with
primitive-author; bats 347 -> 367
- primitive-author: routing forms, quoting guidance, install exit on
hidden Unicode, argument-hint exception
- forge: drop duplicated gotcha, fit description and body budgets (#143)
- skill-author: primitive-author boundary, Claude-only env vars
- hook: exit unless CLAUDE_PROJECT_DIR is set, so Copilot/Codex never
run apm update; ADR-0019 correction, ADR-0025 amendment, docs fixes
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01KkT7RSDwDbmrM9T34b6sTi
deployment-modes.md told authors to use ${CLAUDE_PLUGIN_ROOT} in hook
commands. Hook authoring now belongs to primitive-author, which
specifies the target-neutral ${PLUGIN_ROOT}; point there instead.
Refs #94
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01KkT7RSDwDbmrM9T34b6sTi
lib-boundary-resolver.sh and the provider-adapter-author BOM fixtures
carried literal U+FEFF characters, which apm install reports as "files
contain hidden characters". Both sites feed the text to Python, which
interprets the '' escape identically, so behaviour is unchanged:
the resolver's strip_bom and all five validate-adapter BOM cases pass.
No core package bump: the fixture writes the same bytes, so the edit is
not substantive under the apm-workflow version policy.
Refs #94
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01KkT7RSDwDbmrM9T34b6sTi
Switch the SessionStart hook to apm's target-neutral ${PLUGIN_ROOT} token.
Two scratch packages differing only in the token deploy byte-identical
SessionStart entries with apm 0.28.0, matching the committed
.claude/settings.json, so the deployed output does not change. The test
pin in tests/test-apm-current-hook.sh moves with it.
Correct the claim that Copilot loads no hooks from kyberforge. targets:
is package-wide, so apm also writes .github/hooks/kyberforge-hooks.json
(nested shape passed through, runtime unverified) and merges into
.codex/hooks.json when .codex/ exists. Recorded as accepted in an
ADR-0019 amendment dated 2026-09-28; README and docs/hooks.md updated.
Refs #94
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01KkT7RSDwDbmrM9T34b6sTi
Second clean-context audit found author Must/Should and audit FAIL/SUGGESTION
tiers drifting apart, and author Musts the audit never checked.
- factory-audit: FAIL on absolute or bare relative hook script paths, an
applyTo present but empty, and unbalanced braces/brackets in applyTo;
judgment steps for dependency stem collisions, helper .json in hook dirs,
unresolvable instruction links, prompt model slugs and second-person
bodies; an unmatched glob drops to SUGGESTION; deliberate tier deviations
recorded in hook-flow.md; validate.sh --help lists the three new modes;
DescriptionOpener message no longer prescribes "Use when".
- primitive-author: deprecated routing, extra prompt keys and the prompt
description contract become Shoulds; hook Musts gain "contributes an
entry", no bare relative paths, and executable-when-run-directly;
prompt Must 1 covers hardlinks; Vale prose FAILs resolved at close.
- forge: say "hook, instruction or prompt" rather than "apm primitive".
Refs #94
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01KkT7RSDwDbmrM9T34b6sTi
primitive-author:
- description excludes read-only review (-> factory-audit)
- validation Gotcha now matches the research: compile never reads
prompts, install fails only on a bad Copilot hook payload and warns on
prompt input names and dropped keys
- instruction fold-in into AGENTS.md/CLAUDE.md stated as conditional on
dedup and --force-instructions
- hook checklist gains the wrapped-shape Must, drops hardlinks, notes
why executable is stricter than the research, and states the
separate Copilot-targeted package route instead of a blanket "don't"
- prompt Must 5 keeps the research's Copilot-only-key exception; adds
model-slug and 250-char Shoulds; descriptions name skills or agents
- placeholder instruction covers both FILL IN and FILL_IN_ tokens
factory-audit: hardlink FAIL scoped to instructions and prompts
(find_hook_files skips symlinks only), with bats cases; prompt-flow
description rubric names skills or agents.
forge: version-bump, apm-routes and sources references updated for the
primitive route.
Refs #94
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01KkT7RSDwDbmrM9T34b6sTi
New skill that creates or improves an apm hook, instruction or prompt.
Its SKILL.md holds the shared procedure (dispatch on primitive, boundary
gate, create-or-improve, factory-audit close); one self-contained
reference per primitive carries its gate, checklist and template, drawn
from the microsoft-apm research docs and ADR-0029.
forge gains a route row sending a hook, instruction or prompt to
primitive-author through author-routes.md, and no longer lists hooks as
unroutable. factory-audit's description adds the primitive-author
boundary now that the target resolves.
Fixes#94
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01KkT7RSDwDbmrM9T34b6sTi
factory-audit gains three Step 0 rows and flows for the apm primitives
that have no container of their own: a .json file under hooks/, a
*.instructions.md and a *.prompt.md. apm validates almost none of them
(invalid hook JSON is skipped silently, instruction validate() only
warns, input: names are never checked against ${input:x}), so the
deterministic checks live in a new scripts/lib-checks-primitive.sh,
wired into validate.sh's path-shape detection. Each check and tier
traces to the Authoring checklists in the microsoft-apm research docs.
- Hook: JSON/shape/event-list checks mirroring the Copilot payload
validator, never-firing event casing, missing/escaping/non-executable
scripts (FAIL); deprecated filename routing and ${CLAUDE_PLUGIN_ROOT}
(SUGGESTION).
- Instruction: location, frontmatter, description, body, stem clash
(FAIL); missing or list applyTo and unread keys (SUGGESTION).
- Prompt: location/name, frontmatter, description, input names, the
upstream `- name: x` docs bug, declared-vs-used ${input:x} (FAIL);
ADR-0029 description length and trigger clause, dropped keys,
camelCase aliases, argument-hint with input (SUGGESTION). Whether a
prompt carries procedure is judgment in prompt-flow.md, not a script
heuristic.
Vale now lints *.instructions.md and *.prompt.md with the Kyberforge
style; test-vale-wrap.sh gains their probe rows. New
tests/validate-primitive.bats (31 cases). kyberforge 2.0.1 -> 2.1.0 with
the executables.allow key, catalog 0.5.1 -> 0.5.2, marketplace.json
regenerated.
Refs #94
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01KkT7RSDwDbmrM9T34b6sTi
Record the house rule for the apm prompt primitive: a single-intent,
user-triggered, parameterised message that steers existing skills or
agents by name and carries no procedure of its own. It also sets the
prompt description contract (one plain sentence, no trigger clause).
Add the glossary terms settled in the same grill to CONTEXT.md: apm
primitive, Prompt, Instruction and Hook. Narrow the Skill entry's Avoid
list and log the prompt ambiguity as resolved.
Refs #94
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01KkT7RSDwDbmrM9T34b6sTi
Re-verify the three primitive schema docs against the installed apm-cli
0.28.0 source and live installs, and add an authoring checklist to each.
Corrections to the earlier docs:
- Copilot hooks are not reshaped: events are renamed, paths rewritten and
version: 1 added, but command/timeout are not renamed to bash/timeoutSec.
- A malformed .claude/settings.json is overwritten on install, losing
user content.
- Instruction validate() messages are warnings only; apm compile
--validate never fails on them.
Refs #94
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01KkT7RSDwDbmrM9T34b6sTi
Why
tests/run-bats.sh's discovery walk already excludes apm_modules/ and
.claude/skills/ because those hold apm-installed copies of the same
*.bats files one directory level shallower than their plugins/*/.apm/
source, which overshoots the hardcoded six-levels-up REPO_ROOT walk
each test's setup() does and fails to find the bats-support helper.
build/ was missing the same exclusion: apm pack stages an identical
copy under build/<package>-<version>/ before archiving, hitting the
exact same failure mode from a different apm subcommand. A stray
local `apm pack` run leaves that directory on disk (gitignored,
regenerable) and silently doubles the suite (846 tests instead of
423) with 423 of them failing.
Implementation Notes
Added `-not -path "*/build/*"` to the find walk and the matching
git ls-files grep exclusion, mirroring the existing apm_modules/ and
.claude/skills/ entries. Extended tests/test-run-bats.sh with a case
following the same pattern as the existing exclusion-bug fixtures.
Impact
Unblocks the run-tests pre-commit/pre-push hook for any checkout that
has ever run a bare `apm pack` locally.
Why
The repo's git host moved from git.dev.rkdr.net to git.rkdr.net. The
`origin` remote was already repointed; this commit brings every
in-repo reference in line so cloning, submodule init, and apm install
all resolve against the new host.
Implementation Notes
- .gitmodules: docs/wiki submodule URL repointed (tests/* submodules
stay on github.com, untouched).
- Root apm.yml: 7 dependency entries and marketplace.owner.url
repointed; executables.allow key updated to kyberforge#2.0.1 to
match kyberforge's bump below (scripts/check-executables-allow-sync.sh
enforces this pairing).
- Each plugin's apm.yml (bin, core, git, gitea, kyberforge, lint,
onedev): author.url/homepage/repository repointed. Per this repo's
apm versioning policy, these fields compile verbatim into
plugin.json, so each package took a patch version bump alongside
the URL change.
- Root apm.yml version and marketplace.version bumped 0.5.0 -> 0.5.1
to match (a marketplace-block field and every listed package's
version moved).
- apm.lock.yaml regenerated via `apm install`; .claude-plugin/marketplace.json
regenerated via `apm pack --marketplace=claude` so compiled output
stays in sync with the manifests.
Impact
docs/adr/0015, 0017, and 0018 intentionally keep the old host in their
issue links and examples — they are historical decision records, not
live config. Verified clean: apm pack --check-clean, apm audit --ci,
check-executables-allow-sync.sh, and pre-commit --all-files all pass.
The ADR-0020 boundary resolver (boundary_targets()/unresolved_targets())
only ever read a SKILL.md's description. A target named in the BODY -- a
dispatch table row, a "run X" step, both routine in a 900-word procedure
-- was checked by nothing. Two real instances shipped before either was
caught by reading rather than by a gate: bin/write-docs routed twice to a
deleted `to-prd` skill, and bin/triage told an agent to run a nonexistent
`/setup-matt-pocock-skills` (both fixed in 03abcff; that fix was the
symptom, this gate is the actual ask per #124).
Added a separate, narrower extractor -- body_targets() /
unresolved_body_targets() in the shared lib-boundary-resolver.sh -- rather
than reusing the description resolver at wider scope. The description
gate's sentence-level heuristics (BOUNDARY_MARKER, the follower test,
in-sentence corroboration) are tuned for a one-to-three-sentence routing
clause and misfire on dispatch-table/procedure prose in both directions,
so the body gate reads only explicit route notation (`/name`,
backticked-or-slash-prefixed `-> name` / `-> name`), already the
description gate's own unconditionally-blocking tier.
Three guards were added after running the extractor over the real
39-skill corpus and reading every hit rather than assuming the design was
correct:
- a target must be hyphenated, even in notation -- single-word citations
like `/fork` (forge, citing Claude Code's own /fork command) and
`/name` (skill-author, a placeholder) are not routes.
- a bare hyphenated word after any arrow is not notation -- only
ARROW_MARKED (backticked/slash-prefixed) is used, not NOTATION_ARROW's
bare form, so ordinary process-chain prose ("prop -> new ref ->
re-render", caveman) is not read as a route.
- a name immediately preceded by `<` is a closing tag
(`</what-to-do>`, grill-with-docs), not /name notation.
Wired into both consumers that must agree by contract: scripts/
skill-size-check.sh (the pre-commit hook) and factory-audit's
lib-checks-skill.sh (the audit). Verified identical findings across both
over the whole corpus.
tests/test-adr0020-targets.sh gains a dedicated section pinning the two
live true positives and all three guards. docs/spec/gates.md and
ADR-0020 get a matching amendment.
Fixes: #124
ADR: 0020
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01EGHFJextYtVQseaHPDDhxB
The `house-vale-3-15-2-repro` provenance entry claimed behaviours were
reproduced against purpose-built fixtures, but no fixtures existed, so
the earlier commit in this PR removed it. Commit the fixtures.
tests/test-vale-3-15-2-behaviours.sh builds its fixtures in a temp dir
and runs the real Vale. It exits 77 (skipped) when vale is missing or is
not 3.15.2. It asserts the six vale-config behaviours and the vale-run
ones (unmapped .mdx, `vale off` variants, the spelling ignore file, and
the ls-* commands never naming a rule).
Restore the entry in both sources.md files as `Research doc: none` with
`Basis:` naming the test, and re-add its source_keys. Two behaviours are
not asserted: the native-MDX suppression column (needs mdx2vast) and the
`vale sync` row that adds to Packages (needs the network). The wording in
configuration-reference.md and troubleshooting.md now says so.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01EGHFJextYtVQseaHPDDhxB
- ADR-0004: add the "Amended by ADR-0028" note, following the ADR-0025
convention.
- ADR-0028: correct Q5 (parse_status is gone), the skill count (38, not
39), and the question order. Q7 records the anchored, format-only sha
check. Q8 records the decision to commit real Vale fixtures. A new
consequence covers path confinement and list rejection.
- CONTEXT.md: the `_Avoid_` entry means the bare noun, not the field.
- gates.md: correct the authored-hook counts after the corpus gate.
- create.md: a `none` entry backed by a reproduction must name committed
fixtures in `Basis:`; use the `(digest: <full path>)` form.
- gitea-releases: use the `(digest: <full path>)` form.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01EGHFJextYtVQseaHPDDhxB
The gate took its root from `git rev-parse --show-toplevel || pwd`, so
running it by absolute path from another directory found no skills and
exited 2. Derive the root from the script's own location; the optional
argument still overrides it.
The real-corpus test accepted exit 0 or 1, so it only caught a crash.
It now asserts exit 0. New cases cover a foreign cwd, a skill without
references/sources.md being skipped, several failing skills all being
reported, and an errored skill alongside a failing one.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01EGHFJextYtVQseaHPDDhxB
Review of PR 139 found list-rejection and confinement holes that let the
exact malformed entries the grammar forbids pass check 7.
- Reject comma, space-separated and backticked path lists, so
`a/sources.md (x), b/topic.md` no longer exits 0 unchecked.
- FAIL absolute paths and any path whose realpath leaves the repo, for
both `Research doc:` and `Basis:`.
- Anchor `(removed in <sha>)` to the end of the value with a 7-40 hex
sha. The sha is format-checked only, not resolved with git cat-file.
- Read `* ` bullets and `- **X**` bullets correctly under a `**Basis:**`
header, and strip backticks from Basis paths.
- Stop the semicolon rule firing on annotation prose, and stop `none`
matching `none/foo.md`.
- Update the stale field messages to the new grammar and report an empty
field as empty, not missing.
- Skip a removed Basis silently when there is no repo root.
Adds 40 tests. Each guarded line was mutated in place and every mutant
is caught.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01EGHFJextYtVQseaHPDDhxB
Raise the PATCH version of each skill whose references/sources.md,
references, or validator changed in the Research registry migration, as
ADR-0022 requires. factory-audit and skill-author changed behaviour and
docs; the rest changed provenance metadata only.
Refs: #121
ADR: 0022
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01EGHFJextYtVQseaHPDDhxB
ADR-0028 records the grammar decided for #121 and the alternatives
rejected at each step: what `Research doc:` refers to, how an entry with
no registry declares that (`none` plus `Basis:`), the FAIL and INFO
tiers, the corpus-wide sweep gate, parser parity, retiring check 8, the
`(removed in <sha>)` escape for Basis paths, and removing the lint entry
that had no verifiable basis.
Add the Research registry term to CONTEXT.md, since "registry" had no
definition and "research doc" was being used for both the registry and
the topic docs it digests.
Refs: #121
ADR: 0028
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01EGHFJextYtVQseaHPDDhxB
Nothing ran validate-provenance.sh across the real corpus, so the 36
INFOs it reported for Research doc mismatches were found only by a
manual loop, and a FAIL tier would have been inert. Add
scripts/check-provenance-corpus.sh, which runs the validator over every
plugins/*/.apm/skills/*/ that has references/sources.md.
Exit 1 when any skill FAILs, naming them; INFO lines are printed but do
not fail; exit 2 when the gate cannot run (missing validator, validator
exit 2, or no skills found). Registered as a pre-push hook shaped like
check-scope-walkup-sync, documented in docs/spec/gates.md, and pinned in
test-adr0020-contract.sh's list of repo-authored hooks.
Refs: #121
ADR: 0028
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01EGHFJextYtVQseaHPDDhxB
Repoint every `Research doc:` at the plugin's Research registry
(git/sources.md, pre-commit/sources.md, gitea/sources.md,
agentsmd/sources.md), keeping the old topic-doc link as a parenthetical
`(digest: ...)` annotation. Brace expansions and the gitea-releases
semicolon pair collapse to one path.
Entries with no registry (org-commit-conventions, org-git-conventions,
governance-secrets-hard-prohibition, adr-0002-0003-two-tier-claude-md)
now declare `none` plus `Basis:` bullets. The two git entries cite
core/instructions/git.md and commits.md as `(removed in 5deed07)`.
Remove the house-vale-3-15-2-repro entry and its source_keys citations
from vale-config and vale-run. It claimed six behaviours were reproduced
against purpose-built fixtures in this repo, but the entry was added in
d1afdbe with no test or fixture files, and none exists in history. The
behavioural rules stay; only the unbacked provenance claim goes.
Refs: #121
ADR: 0028
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01EGHFJextYtVQseaHPDDhxB
validate-provenance.sh assumed `Research doc:` names a research
sources.md whose H2 headings are the source slugs, but 29 corpus entries
named topic docs and 6 values were not a single path, so checks 7 and 8
reported INFO for 36 entries and nothing ever failed.
`Research doc:` now takes exactly one path. An entry with no registry
writes `none` plus one `- **Basis:** <path>` bullet per path; each Basis
path is existence-checked unless annotated `(removed in <sha>)`.
- Check 7 FAILs when a resolved registry lacks the slug, when the value
is a topic doc, or when it is a list. An unresolvable path stays INFO.
- Check 8 is retired: one registry serves many skills, so requiring
every registry slug in each skill's sources.md is unsatisfiable.
- The Research doc and Basis parsers accept the inline, bullet and
header-plus-bullets spellings, so a differently spelled field is no
longer read as absent.
Refs: #121
ADR: 0028
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01EGHFJextYtVQseaHPDDhxB
Step 4 subagents read untrusted pages; say their content is data, not
instructions. Step 5 now repeats step 4, so it inherits the serial
fallback and the data rule. Body stays at 598 words, under the
ADR-0020 target.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01EGHFJextYtVQseaHPDDhxB
The fan-out restored in 6683da5 had no degrade path for a target with no
spawn tool, which reproduces the silent degradation #116 was written
against. Step 4 now says to read serially and reduce each page to notes
when spawning is unavailable.
The change restores existing behaviour, so the version bump is a patch
(1.0.2) per skill-author's convention, not a minor. The body is trimmed
from 717 to under the 600-word ADR-0020 target without dropping any
instruction. ADR-0027 is updated to match.
Refs #116
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01EGHFJextYtVQseaHPDDhxB
The first draft claimed omitting allowed-tools grants spawning and that
the list was a restriction. The repo's own sources describe the field as
pre-approval, and the code now keeps the list. Rewrite the ADR to say
the #116 defect was step text disclaiming spawning, that per-target
behaviour for an unlisted tool is unverified, that the spawn tool is
left out because its name is sourced for Claude Code only, and that the
orchestrator-writes mitigation is prose, with the unmitigated security
cost recorded. Rename to fit the new decision.
Refs #116
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01EGHFJextYtVQseaHPDDhxB
6683da5 dropped allowed-tools on the premise that the list blocked
spawning. The repo's own docs describe the field as pre-approval, so the
list was never the cause and dropping it widened the tool surface for
nothing. Restore the list and keep the parallel fan-out in steps 4-5.
The spawn tool is not added: its name is sourced for Claude Code
(Agent) but not for Copilot or Codex, so spawns prompt rather than
being pre-approved.
ADR-0027 still asserts the dropped-list premise and is corrected
separately.
Refs #116
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01EGHFJextYtVQseaHPDDhxB
research instructed "spawn one subagent per URL" while its allowed-tools
granted no spawn tool, so it silently degraded to serial fetches. Three
other skills spawn subagents without trouble because they declare no
allowed-tools. The defect was the mismatch, not the spawning.
ADR-0027 records the agreement rule. research drops allowed-tools and
gets its steps 4-5 fan-out and the orchestrator-writes gotcha back
(1.0.1 -> 1.1.0).
Closes#116
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01EGHFJextYtVQseaHPDDhxB
plugins/onedev is the first plugin package to declare a real dependency, and that arms a
check every previous plugin left vacuous. apm treats any directory holding both apm.yml
and apm.lock.yaml as an install root; a package is not one, so there is no green state for
it. Without a package lockfile, lockfile-exists fails outright. With one, it passes and
thereby arms the other nine checks, where drift then demands the dependency's skills be
deployed inside the package and apm lock leaves an apm_modules/ tree behind.
scripts/apm-audit-ci.sh replaces the inline bash -c loop and waives that single check for a
non-root manifest. It fails closed on three axes: the root is never waived; the failing
check must be lockfile-exists and no other, asserted by matching "1 of 1 check(s) failed";
and unrecognised output fails.
Dropping --ci for package directories was the smaller change and is wrong. Verified on apm
0.28.0 against a scratch package whose dependency entry carried no git/path/registry field:
apm audit --ci exits 1 naming it, while plain apm audit exits 0 and says nothing.
Malformed-dependency detection is the reason gates.md gives for auditing packages at all,
and a package with dependencies is the only kind that can carry a malformed dependency
entry.
The waiver matches on apm's stdout, so an apm upgrade rewording either line turns it off.
That fails the push rather than hiding a defect.
Also records the onedev entry in apm.lock.yaml, which PR #136 could not carry because the
plugin was not yet resolvable from the remote's main.
ADR: 0026
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01NwD8Egs5r4ndqeFLmhusX2
OneDev ships TOD, an official CLI, and eight SKILL.md files alongside it. apm installs raw
SKILL.md sources straight from a git repo, so those skills need no reauthoring — but a
`marketplace.packages` entry takes a local `source:` path, so a third-party repo cannot be
listed for redistribution on its own.
plugins/onedev is that wrapper. It carries no primitives yet: it pins
code.onedev.io/onedev/tod#v4.3.4 so consumers installing `onedev` from the holocron
marketplace pick up TOD's eight skills transitively, and it is where this repo's own OneDev
skills and orchestrator agent will live once there is a gap worth filling.
The pin is deliberate. The six first-party dependencies stay unpinned for default-branch
parity because they are this repo's own content; tracking a third-party project's main
would import an outside project's drift instead.
Impact: root apm.yml consumes the wrapper by git+path, so `apm install` does not resolve
until this is on the remote's main — including the copy kyberforge's SessionStart hook runs
on launch. Accepted deliberately; this merges immediately. Gitea remains the tracker of
record and ADR-0007 is untouched.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01NwD8Egs5r4ndqeFLmhusX2
- LESSONS.md's 2026-06-22 test-placement entry told authors to put test
files directly in scripts/ with a README row. The file-structure contract
the repo now enforces permits tests/ as one of four directories, requires
a tests/README.md when it exists, and FAILs test files in scripts/.
- LESSONS.md's 2026-08-16 entry described a dispatch chain ending at
skill-author/references/retrofit.md in the present tense. This branch
deleted that file. Sibling entries whose referents the branch removed
were marked historical; this one was not.
- VISION.md's Phase 1 now puts stack, framework and deployment choices out
of scope for this repo, while Phase 3 still named React Native and Tauri.
README.md was checked and needed no change: its offline guarantee already
carries the populated-apm_modules condition from 8cfd54f and agrees with
gates.md and AGENTS.md.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01NwD8Egs5r4ndqeFLmhusX2
The document has been re-measured four times and each pass moved figures
the next pass had to chase -- three of the last five commits on this branch
were figure corrections to it, and correcting it changes the line counts it
reports about itself. It is now a dated record frozen at 1ec3e8a. Figures
stand as measured at the commit each one names and are not maintained.
Freezing covers staleness. It does not cover a figure that never
reproduced or a claim that says verified for a check that fails, so those
are struck:
- Finding 11's provenance-validator counts, 320/1,145/572/134 = 2,171, were
true at no commit. The files are 324/1,152/576/134 = 2,186 and have been
since 620f20b created them. The derived 5,380 and 7,136 follow.
- Finding 11's line citations into lib-provenance-skill.sh, stated as
re-derived at HEAD, were uniformly seven low and none landed on the code
named.
- The tests/ line total pinned to 1614bce is that commit's suite count with
384756b's line count.
- Finding 33's "all five instruction-level citations still resolve at HEAD,
verified with sed -n", dated 2026-09-19, is false. Four resolve.
improve.md:82 stopped carrying the content at baa2f5d, three days before
the verification was claimed.
Also reconciled: finding 11's 242-file effort total against its own struck
46, finding 16's two different deltas for ef27c97, a clause pinned to
baa2f5d carrying c07ca07's figures, and the preload-tax row's 39 skills
against the census row's 38.
Notes that date themselves "at HEAD" name no fixed commit, and this commit
moves HEAD under them, so the banner now says so rather than re-deriving
twenty of them.
This review round is recorded on the pull request, not here. A frozen
document that grows another section is not frozen.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01NwD8Egs5r4ndqeFLmhusX2
Seven ADRs described code that no longer exists or behaviour the gates do
not have. Where the wrong text came from main it carries a dated
correction; where this branch introduced it, it is fixed in place, because
main never published it and there is no record to preserve.
Fixed in place, branch-introduced:
- ADR-0021's 2026-09-14 correction asserted apm audit --ci "was never a
drift gate at all". It is one: it replays the install and diffs. The
claim contradicted this branch's own AGENTS.md and gates.md.
- ADR-0015 said unconditionally that no pre-push hook needs the network.
The guarantee holds only once apm install has populated apm_modules/.
- ADR-0014's 2026-09-16 correction said restoring .pre-commit-hooks.yaml
would ship a hook that fails for every consumer, because their checkout
has no lib-boundary-resolver.sh. pre-commit clones the whole hook repo
and skill-size-check.sh resolves the library from BASH_SOURCE, so the
hook would work.
- ADR-0019's "twelve hooks pass under unshare -rn" matched neither HEAD
(8) nor main (14), and stated the offline guarantee unconditionally.
Corrected, inherited from main:
- ADR-0022 and ADR-0013 named skill-frontmatter's pre-commit hook as the
enforcer of mandatory metadata.version. That hook was deleted on this
branch; the check lives in skill-size-check.sh.
- ADR-0022 enumerated the tip rule's carve-outs as a closed list and
described a single merge-base. The gate also exempts a tree-identical
skill and intersects every base from merge-base --all, and emits a third
failure form. 8cfd54f said the documented behaviour did not change; it
did. The gate is correct and is unchanged -- the record was not.
- ADR-0020's Decision still routed description overflow to README.md, its
ADR-0025 amendment pointed the mirrored constants at validate.sh, which
holds none, and its Enforcement table still named the two deleted
validate.sh paths.
- ADR-0015's Status claimed every plugin's plugin.json is pack output;
none exist.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01NwD8Egs5r4ndqeFLmhusX2
boundary_clause_status() ran BOUNDARY_ARROW.search() and _arrow_targets()
over the whole description, so one arrow clause that parsed suppressed the
diagnostic for every other clause in it. A backticked hyphenated routing
target wrapped across lines in a folded scalar was therefore silently
unchecked -- no error, no suggestion, exit 0 -- whenever the description
carried one other clause that parsed. Written bare, the same wrap errors
correctly. That is the shape #100 regressed on.
The check is now per clause. Nothing that passed starts failing: all 68
routing targets across the 38 SKILL.md files resolved before and still do.
26 of those descriptions carry more than one arrow clause, so the
suppression was live across two thirds of the corpus, not an edge case.
validate-skill.bats pins the shape. test-adr0020-targets.sh's comment
described the #100 regression as a backticked wrap; the historical text was
unbackticked, which is precisely the shape the gate did not catch.
Also closes three README misroutes the branch left in the enforcement
layer: CompositionNote.yml's message, agent-description-quality.md:58 and
vale-wrap.sh's header still sent overflow to a skill-root README.md and
named the two skills ADR-0025 merged away. 1ec3e8a fixed the prose and
missed the rules that enforce it.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01NwD8Egs5r4ndqeFLmhusX2
Four claims in the spec and the hook config stated as fact what the tools
do not do:
- gates.md:485 said factory-audit's validate.sh "holds its own copy of"
the ADR-0020 constants. gates.md:411-413, twenty lines earlier, said it
carries none of them and named the mode libraries. The libraries are
right: lib-checks-skill.sh:313-316 and lib-checks-agent.sh:164-165.
architecture.md repeated the same error.
- gates.md stated the case count for test-adr0020-contract.sh as "29 at
HEAD", explicitly presented as measured. Running it prints 44; 384756b
added the hook-wiring assertions after the text was written.
- gates.md:913 and :916 described "Both audit skills'" behaviour in the
present tense, three and six lines above :919 saying factory-audit's is
the only copy left.
- The apm-audit-ci block named manifest-parse as a check, said the hook
does not scan for hidden Unicode, and called root lockfile-exists
vacuous. apm 0.28.0 runs ten checks, content-integrity does scan for
hidden Unicode, and there is no manifest-parse row.
Also: the version-bump gate's baseline is documented as the single
merge-base it is not -- it resolves every base with merge-base --all,
intersects the changed-skill sets, exempts a tree-identical skill, and
emits a third sha-suffixed failure form. The gate's own header documents
this correctly; the spec did not. Behaviour is unchanged.
The "none of them need the network" line added on this branch cited a
README section that says the opposite for a fresh clone, and the
check-vale-style-sync rationale said 6 of 17 assertions diffed the Vale
copies where ADR-0025 says 2 diffed and 4 more only located them.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01NwD8Egs5r4ndqeFLmhusX2
skill-author still told authors to move description overflow "to the
body or to README.md" while this branch deleted every per-skill
README.md, every references/README.md and the README scaffold template.
factory-audit's skill-file-structure.md bans non-spec files at the
skill root, and the line that used to carve README out of that rule
went with them. So skill-author created the file, factory-audit failed
it, and nothing read it. 8ce5392 fixed the two scripts and missed the
reference prose.
The two contract.md files now differ deliberately: a skill's overflow
goes to the body or a references/ file, an agent's to the body alone,
because an agent is a single file with no references/ directory to
disclose to. agent-description-quality.md's "the plugin's README.md" is
left alone, plugin READMEs being the ones that survive.
Deleting retrofit.md also dropped three instructions baa2f5d did not
restore with the cut list, two of which retrofit.md itself recorded as
having no validator behind them: re-cite sources.md's Contributing
files after content moves, since validate-provenance exits 0 on exactly
that drift, and re-check a relocated gate's reachability, since a
Gotcha moved into one flow's file is invisible to the others and the
word counts improve either way. The third is that boundary clauses are
plural — contract.md read as a cap where git-remotes carries four.
Also: contract.md named an unqualified scripts/validate.sh that does
not exist in skill-author, which skill-file-structure.md calls a hard
error; and agent-body-and-delegation.md's simile pointed at a stale
README row as the characteristic skill defect, a defect class that can
no longer occur, replaced with a SKILL.md naming a references/ file
that is not there.
skill-author 1.0.4, agent-author 1.0.3, factory-audit 1.0.2.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01NwD8Egs5r4ndqeFLmhusX2
CONTEXT.md used two terms it no longer defines. This branch deleted the
Preload tax and Skill context contract entries as audit finding 31, but
the Hand-invoked skill definition and the example dialogue still used
both, bolded, which is this file's convention for a defined term. The
definitional file contradicted itself while AGENTS.md tells every
session to read it as authoritative. Rephrased in place, the way
e2e957e handled the one the audit's own note records.
ADR-0024 said 10 .bats files deploy across 6 skills; ADR-0025 merged
two of those directories the next day, on this branch, leaving 5. It
was also the only ADR ADR-0025 invalidated without an amendment banner,
as was ADR-0016, which still named agent-audit in the present tense as
the live enforcer. Both get the banner the other nine carry, and the
figure and names are corrected in place as well, since these sit in
text asserting present fact rather than a superseded decision.
ADR-0019's correction block from 1614bce was inserted mid-paragraph and
swallowed the original's trailing sentence, leaving the quote malformed
and the next line starting lowercase mid-sentence. gates.md took the
same correction and is not affected.
In the audit note: two of §12's five open follow-ups were already
closed (e4ed343 repointed the a8cd5e8 citations at 598a7c3; #101 closed
2026-09-16, so Closes#101 is a no-op), the same stale hash sat at :330
with a wrong line number, the vale-wrap counts had drifted from 63/19
to 65/14 and are now pinned to a commit per §1's own convention, and
the deleted-suite tally said eight where the diff shows nine.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01NwD8Egs5r4ndqeFLmhusX2
The repo's gates were not pinned to their wiring. Deleting the
check-skill-version-bump block from .pre-commit-config.yaml left the
whole suite green; deleting eight blocks at once, run-tests among them,
also left it green. Only 4 of 20 hook ids had their wiring pinned
anywhere, so a merge conflict resolved badly could stop the suite
running at pre-push forever while every test still reported green.
test-adr0020-contract now derives the repo-authored hooks from the
repo: local entries and pins each one's id, entry and stages against an
explicit expected set, both directions, with the same non-vacuity
guards the file already applies to its own fixtures. Upstream hooks and
their rev: values are untouched, so a rev bump does not churn the test.
Mutation-checked: a removed block, a repointed entry and a hook moved
off pre-push each go red; a rev bump, a comment edit and reordering
stay green. 29 -> 44 assertions.
run-bats computed each file's TAP plan and then discarded it, so a
process printing "1..10", three ok lines and exit 0 was counted as
"3 tests, 0 failures" with seven tests silently gone. That is exactly
the wrapper-swallows-the-status case the runner's own comment puts in
its threat model, and the plan was the only surviving signal. The plan
is now enforced in both directions when a file emits exactly one.
Also: test-no-pipefail-early-exit-grep's live-tree floor goes from 20 to
50 against an actual 57, matching test-vale-wrap's per-glob discipline,
and test-vale-wrap's header names the real path to vale-wrap.sh.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01NwD8Egs5r4ndqeFLmhusX2
Two reproduced bugs in check-skill-version-bump:
- The origin/main-tip check fired even when the pushed skill was
byte-identical to main's tip, so a cherry-pick or backport failed a
push that ships nothing. The merge-base intersection ea119d8 added
covers that only when some base carries the content, which a
criss-cross history gives and a linear one does not. A new
same_subtree compares tree object ids, so the exemption holds
whatever route the history took.
- The failure line reported "baseline: none" when the skill was absent
at every merge-base but present at the tip, and the Fix: line then
named no version. The author writes the natural 1.0.0 and gets a
second blocked push. It now falls back to the tip's version.
ADR-0022 is not amended: the documented behaviour does not change, and
ea119d8 set the precedent by fixing the same failure class script-only.
1614bce verified that executables.allow grants are version-blind and
corrected ADR-0019, gates.md and apm.yml, but missed the gate script's
own header and its operator-facing FAIL message, which still told the
reader deployment was silently broken, and gates.md's hook summary,
which still called it a silent-failure guard. All three now match.
Also: README's offline guarantee carries the populated-apm_modules
condition gates.md and AGENTS.md already state; the scripts/ layout row
drops "sync" for the three deleted sync scripts; the check-rtk-prefix
README rationale names the 12 subdirectory READMEs that survive rather
than the skill-root ones this branch deleted; gates.md re-cites its
three head -1 sites by enclosing function per its own :238 rule; and
deploy-manifest drops a pointer to a provider-manifest.sh that has
never existed on main.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01NwD8Egs5r4ndqeFLmhusX2
gates.md's check-executables-allow-sync section and ADR-0019 both stated
that apm matches executables.allow on an exact `<package>#<version>`
dictionary lookup with no wildcard and no version-less form, and drew the
conclusion that a kyberforge version bump silently stops the entry
matching and the SessionStart hook deploying.
Verified against apm 0.28.0: is_package_approved is an exact lookup, but
install/exec_gate.py calls it across a candidate list carrying the
version-blind name, materialize_exec_map stores each approved key under
its version-blind name as well, and _map_grants matches exact key,
version-blind name, or any stored key sharing that name. Approving
kyberforge#2.0.0 therefore keeps covering kyberforge#2.1.0.
The decision is unchanged: check-executables-allow-sync stays, justified
by this repo's own requirement that the key track plugins/kyberforge/
apm.yml's version:, rather than by an apm-level failure mode. ADR-0019
keeps its original text with a dated correction, since whether apm
behaved this way when it was written was not established.
Follows the same correction applied to root apm.yml's comment in 82b7bbc.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01NwD8Egs5r4ndqeFLmhusX2
Group 3 of the validated PR #135 review fixes. Every figure and commit
citation below was re-verified at HEAD before being written.
ADR and architecture:
- #7 ADR-0025 cited 61b0b9c, which no published branch reaches. Repointed
to 620f20b (identical parent tree, reachable from the PR branch), with a
note that neither is reachable from origin/main. The parser-drift
paragraph now credits 598a7c3 (the reachable PR #129 squash) and keeps
484357a only as a pre-squash parenthetical.
- #8 architecture.md dropped the pointer at the LESSONS.md entry this
branch deleted.
- #9 architecture.md's ADR entry points now name ADR-0015 (the one
compiler) and ADR-0024, and list ADR-0024 as superseding ADR-0017.
- #10 ADR-0024 section 4 rewritten: the standing patch-bump rule is
apm-workflow's configure.md, not ADR-0006's, and this change does not
trigger it. ADR-0015:93 carries a correction for the misattribution.
- N5 ADR-0021 gained a Correction note for the deleted
scripts/check-manifests.sh (e647f14).
gates.md:
- #11a the four ADR-0020 constants live in lib-checks-skill.sh:313-316 and
lib-checks-agent.sh:164-165, not in validate.sh.
- #11b the pretty-format-json exclude is two alternations expanding to
three tracked files, including .claude/apm-hooks.json.
- #11c the ADR-0020 contract suite runs 28 -> 27 -> 29 (620f20b,
4de5b6b, ef27c97), 29 at HEAD; the unverifiable 25 is dropped.
- #11d the boundary resolver is one copy since ef27c97.
- #12 apm-audit-ci documents the 10 root checks and the 1 plugin check
apm 0.28.0 actually runs, that content-integrity IS the hidden-Unicode
scan, that manifest-parse is not a named check, and that the hook needs
a completed apm install. The offline claim is qualified accordingly.
- N9 gates.md:142-146 verified to still match the hook description.
AGENTS.md:
- #12 the no-network session rule is qualified to a populated
apm_modules/.
Audit note:
- A1 hook counts corrected to 27/9 -> 26/8 -> 27/9 -> 26/8 (26 and 8 at
HEAD) and the dangling pointer dropped.
- A2 skill-size-check.sh is 509 lines with the resolver sourced, not 1,522
embedded; citations repointed to skill-size-check.sh:323-335 and
lib-checks-skill.sh:235-283 (fail() at :265 and :280), and that library
is 627 lines.
- A3 consumers receive 15 test files across 5 skills; 16 tracked test
paths repo-wide.
- A4 the "do not run apm update on this branch" instruction is marked
superseded, with the branch-aware guidance in its place.
- Finding 31's "true orphans" claim corrected for HOTL and Sycophancy,
both still used in core/ai-constitution.md.
Same class, found during group 2:
- skill-author's deployment-modes.md no longer points at .mcp.json
configs (deleted in c96ca9c); metadata.version 1.0.2 -> 1.0.3.
- git-orchestrate's context contract clarifies that
user_config_overrides is caller-supplied session state, not a config
read. The field name is unchanged.
- B3 root apm.yml's executables.allow comment: grants are version-blind
in apm 0.28.0, so the #2.0.0 suffix is cosmetic to apm and a bump does
not break the hook; the suffix stays because
check-executables-allow-sync.sh requires it.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01NwD8Egs5r4ndqeFLmhusX2
Closes four PR #135 review findings in skill content.
#2 — plugins/git/config.example.json was deleted in f5e4d0d, but four
git-plugin files still told the agent to read it. The file only ever
carried branching_pattern, commit_style and rebase_strategy, so the
`base_branch` and scope instructions were wrong even before the
deletion. Each site now describes what the skill actually does: base is
`main` under GitHub Flow or `develop` when Gitflow is inferred, the
Gitflow fallback keys off the repo's own branches, the orchestrator
contract's `base` defaults to the inferred base branch, and the commit
scope is inferred from the changed files.
N6 — gitea-prs was the one gitea skill with no permission-scope caveat
on a 404. Added one alongside the existing issue/PR number-space
guidance rather than replacing it: a 404 is only evidence of
"that number is an issue" once write:repository scope is confirmed.
N4 — plugins/kyberforge/bin/README.md pointed at `.mcp.json`, but all
six plugin-root .mcp.json files were deleted in c96ca9c (ADR-0018).
${CLAUDE_PLUGIN_ROOT} itself is still live, so the sentence now points
at .apm/hooks/hooks.json, which kyberforge's own hook already uses.
N7 — not applied. The finding claimed a marketplace field override
emits a verbose BuildDiagnostic that `apm pack -v` surfaces, so
"silently wins" was wrong. apm 0.28.0 does construct the diagnostic in
marketplace/output_mappers.py, but nothing renders it:
_render_marketplace_result in commands/pack.py iterates `warnings`
only, and BuildReport.diagnostics has no consumer. Confirmed on a
fixture — neither `apm pack -v` nor APM_LOG_LEVEL=DEBUG prints the
override, and --check-versions reports [matches]. The existing wording
in configure.md and marketplace.md is correct, so both are unchanged.
Version bumps required by check-skill-version-bump.sh: git-branches
1.0.4 -> 1.0.5, git-commits 0.1.6 -> 0.1.7, gitea-prs 0.1.4 -> 0.1.5.
bin/README.md is outside any skill directory and needs no bump.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01NwD8Egs5r4ndqeFLmhusX2
B1: check-skill-version-bump.sh resolves every merge-base with `git merge-base
--all` instead of the single base git happens to pick. A criss-cross history has
two, so the verdict turned on that choice: a skill byte-identical to main's tip
could still be reported "not above merge-base" / "not above main tip" and fail a
push that should pass. A skill now counts as changed only when it differs from
EVERY base, and its version must exceed the version at every base it exists at
as well as at the main tip; with more than one base the failure names which one.
Case 40 in tests/test-skill-version-bump.sh builds the criss-cross fixture and
pins both directions.
B2: check-apm-current.sh no longer assumes the remote default branch is `main`
when origin/HEAD is unset. A checkout whose default is `master` was standing on
its default branch and being told "this is a feature branch, so discard it" --
to throw away a real lock update. With origin/HEAD unset nothing is asserted and
the neutral advice stands. tests/test-apm-current-hook.sh covers the unset case
on both `main` and `master`.
#4: the required-frontmatter checks folded into skill-size-check.sh by c8a7c9e
were untested apart from the leading-zero shape -- mutating the missing-version
ERROR into a no-op left every suite green. tests/test-adr0020-frontmatter.sh now
pins name presence and non-emptiness, metadata.version presence and semver
shape, and the four grep defects the deleted test-skill-frontmatter.sh named.
#5: nothing asked whether a Vale rule still MATCHES anything -- rewriting
CompositionNote.yml's tokens to match nothing left test-vale-wrap.sh at 63/63.
Case 35 enumerates the rule files under the Kyberforge* style directories at run
time, requires an alert from each on its own fixture, and fails when a
discovered rule has no fixture row. The stale comment at case 31 is corrected.
#6: tests/run-tests.sh --strict exited 0 when discovery found no test-*.sh at
all; strictness only ever acted on skips, and with no suites there were none. It
now cross-checks the git index the way run-bats.sh does and fails
unconditionally on an empty set, naming the search root.
N9: the skill-size-check hook description in .pre-commit-config.yaml covered
only the size, context-budget and boundary-target gates. It now also names the
required frontmatter fields, matching docs/spec/gates.md.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01NwD8Egs5r4ndqeFLmhusX2
The accepted ADR still called the skill-author/agent-author duplication
an open input to #101. The branch decided it: continued drift, no sync
gate, on the measured 150-180 line overlap. State that decision so the
ADR stops contradicting the PR that closes#101, and mark the old
skill-audit citation historical.
Refs: #101
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
a8cd5e8 was squashed into 598a7c3 (#129) and is reachable from no
branch, so the comments now cite the commit that exists on main.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Correct the hashes left by the branch rewrite (467bbd7 -> 620f20b,
4059cb4 -> ffcbed6), annotate a8cd5e8 and c59e4bf as reachable only
through the 598a7c3 squash, strike the case 33 claims that 4de5b6b
made stale, and re-measure the section 1 table, the gates.md length and
the ADR share at baa2f5d. Add section 12 for the final seven-reviewer
round. Move the record under docs/notes/, alongside the repo's other
closed decision records, and update ADR-0024's pointer to it.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
improve.md still required a retrofit before extending but lost the
procedure with retrofit.md. Restore the ordered cuts inline, and fix
the stale hook name and plugin-mode wording in skill-author's tests and
deployment-modes reference.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
The exception pointed at a Gotcha that does not exist. State the rule
(machine-parsed output or an interactive editor runs bare git, with the
reason inline) and point at rewrite-history.md for interactive rebase.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
ADR-0014 gains a dated correction: skill-size-check now sources the
boundary resolver from kyberforge (ef27c97), so restoring the external
hook contract needs it made self-contained first. ADR-0017's status
reflects its supersession, architecture.md and gates.md carry the
current duplication counts and reason, gates.md defines vacuous green
inline, and the gitleaks lesson is marked historical.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Removing the mattpocock-skills entry is a minor catalog change under
apm-workflow's marketplace policy, not a patch. Also describe Copilot
support as reached through apm rather than a native Copilot CLI
marketplace (ADR-0024), and rebuild marketplace.json with apm pack.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Both runners excluded */.claude/worktrees/* by absolute path, which
filtered out every test when the repo itself is a Claude worktree.
Search from inside the root so only nested worktrees are skipped.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Skills no longer carry a README.md, so the size advice in
skill-size-check and factory-audit's validator now names a references/
file instead.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
read_version required --- at byte 0 while skill-size-check accepts
leading blank lines, so a file one gate passed the other reported as
unversioned, and an unversioned merge-base side let an unbumped change
through. Match FRONTMATTER_RE, add case 39, and describe the main-tip
check and fail-closed cases in the hook entry.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
apm recognises its own settings.json hook entries only through the
.claude/apm-hooks.json sidecar. With the sidecar gitignored, apm install
in a fresh clone keeps the committed SessionStart entry as user-owned and
appends a duplicate, so apm audit --ci reports drift and the apm-audit-ci
pre-push hook fails. Reproduced on main and this branch with apm 0.28.0.
Commit the sidecar in apm's exact serialisation, exclude it from
pretty-format-json alongside settings.json, and record the correction
in ADR-0019.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Why: 4de5b6b removed check-release-needed and two test suites, but the
audit's hook and test figures still described the tree before it.
Implementation Notes: re-measured at 4b17703. Hook entries 27/9 -> 26/8
(line 52 and finding 1), gates.md now reads 10 reported / 8 authored,
the enforcement row is 10,000 test lines over 19 suites + 502 runner
lines + 1,924 in scripts/, and the withdrawn tests target is struck.
The walk-up gate figures (381 + 297) were re-checked and are unchanged.
Also records that the run-tests wall-time follow-up and finding 22 are
deliberately untracked.
Impact: docs only.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Why: the last loose end, finding 6's check-apm-agents-valid fold, was
never closed, and the document did not say it was finished.
Implementation Notes: finding 6 records that the hook stays at repo
level, because it validates this repo's own agent files, which a
consumer-shipped skill test cannot reach. A status banner at the top
and a closing note in §7 mark the audit complete, with only 22
deferred with bin.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Why: HUMANS.md pointed at docs/ai-constitution.md, which adaa978 moved
to core/ai-constitution.md. The wiki commit is pushed, so the pointer
can now follow it.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Why: the 2026-09-16 grill decided the two salvage options the verified
notes for findings 5 and 16 had left open.
Implementation Notes: finding 5's differential-suite speed-up is closed
as not proceeding, with the per-suite timings that decided it. Finding
16's resolver-sourcing option is recorded as done in ef27c97, with the
line delta and the output-identity check.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Why: scripts/skill-size-check.sh embedded a byte-identical 1,061-line copy
of the ADR-0020 boundary resolver only because it was also exported
through .pre-commit-hooks.yaml, whose consumers could not reach a file
inside the plugin. 4de5b6b retired that export, so the hook now runs only
in this repo and can source factory-audit's lib-boundary-resolver.sh like
validate.sh does. One copy removes the edit-one-paste-the-other hazard.
Implementation Notes:
- The hook's Python program is assembled from its own preamble, the
library's resolver and its own checks, read from quoted here-docs. The
assembled program matches the old one line for line except one comment,
and the hook's stdout, stderr and exit code are identical over every
corpus SKILL.md and the 26 differential-suite fixtures.
- The hook fails closed, naming the library, when it is missing or
defines no resolver.
- test-adr0020-contract.sh assertion 1 now pins the single copy: one
marker pair in the library, none in the hook, fail-closed on a missing
or gutted library, and a sentinel planted in a copied library that must
appear in the hook's output. 1a expects exactly one authority. 27 -> 29
passes.
- ADR-0020 and ADR-0025 carry dated amendments; gates.md and the
library, hook and mode-library comments no longer describe two copies.
- factory-audit is new on this branch, so the version-bump gate exempts
it; kyberforge is already at 2.0.0 against main's 1.6.2.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Why: the always-on governance.md told agents to read
docs/ai-constitution.md when a decision is not covered, a path that
exists only in this repo, so the fallback failed in every other project.
(Simplification audit finding 27, second defect.)
Implementation Notes:
- Move docs/ai-constitution.md to core/ai-constitution.md; the existing
core deploy step now ships it to ~/.claude/core/.
- governance.md line 4 and line 73 name ~/.claude/core/ai-constitution.md;
the HUMANS.md and CONTROLS.md pointers now say they live in the
holocron repo.
- Repoint path-qualified citations in AGENTS.md, architecture.md,
skill-implementation-workflow.md and CONTROLS.md. The vendored
write-skill example and the audit's historical notes are left as
records.
- The docs/wiki gitlink is not bumped here; the wiki commit awaits push.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Why: a full read after finding 15 closed found notes that still read as
open or predated later outcomes, and the fixed governance.md had never
reached ~/.claude.
Implementation Notes:
- Record that the fixed governance.md is deployed. It was copied alone
rather than through install.sh, which would have overwritten
machine-local keys in ~/.claude/settings.json.
- Close finding 2: check-scope-walkup-sync stays now that 14 left its
ports at four and 15 is refuted.
- Correct the §9 and §10 sentences that still assumed 15 would merge,
and tick the §8 external-consumers question.
- Mark every closed finding [x] regardless of outcome; only 22, deferred
with bin, stays unmarked. §7 states the convention.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Why: finding 15 proposed merging skill-author and agent-author on the
claim that they share most of contract.md and near-identical steps.
Measured, the pair shares about 150-180 distinct non-blank lines,
against the 2,934 the factory-audit merge removed. ADR-0020's exclusion
of the pair holds, so the merge does not proceed.
Implementation Notes: finding 15 is struck through and carries a dated
Refuted note with per-file counts and the command that reproduces them.
The §7 order and status notes and the §8 ADR-0012 bullet now show no
open finding, with 22 deferred with bin. ADR-0020's rejected
alternative records the measurement; ADR-0025 point 7 no longer calls
the finding open and unmeasured.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Why: a review of 4de5b6b and 3c5f6a6 found seven places that still
described the retired external hook contract as current, or that could
pass without checking anything.
Implementation Notes:
- gates.md: drop the "one caveat below" pointer; the caveat is gone.
- SIMPLIFICATION-AUDIT.md: the §7 note now says all but one entry is
closed and lists 36 (4de5b6b) as struck. Finding 2's count chain gets
a closing note: pre-push is 8 repo-authored hooks (10 reported).
- .pre-commit-config.yaml: the check-vale-style-sync comment points at
case 32 (one-plugin narrowing guard), not the deleted case 33.
- ADR-0014: the retirement pointer now covers the ADR-0025 amendment
above it too, naming case 33 and the exported hook IDs. ADR-0022 gets
a reciprocal amended-by note on its check-release-needed comparison.
Historical body text is unchanged.
- test-vale-wrap.sh case 32: property 3 fails when a class's corpus
regex matches no tracked file, instead of passing vacuously after a
layout move. The corpus regexes are now globals so a new Part D can
point them at a missing layout and require that failure.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Why: 4de5b6b retired the external pre-commit hook contract, so the audit
still listed 36 as open, with a deferred decision.
Implementation Notes: finding 36 is struck through and carries a dated
Done note (measured line counts, the case 33 cost and how its guard was
kept). The §7 status notes, the §8 consumer question and the §10
dispositions now leave 15 as the only open finding, with 22 deferred
with bin.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Why: .pre-commit-hooks.yaml and its release-tag gate served external
consumers that do not exist. No repo on the Gitea instance pins these
hooks, and the README names apm as the only supported install path. The
mechanism was also already failing: skill-size-check.sh changed after
v2.0.1 with no tag cut, and the gate cannot fire through Gitea's merge
button. (Simplification audit finding 36.)
Implementation Notes:
- Delete .pre-commit-hooks.yaml, scripts/check-release-needed.sh,
tests/test-check-release-needed.sh and tests/test-vale-hooks-consumer.sh,
and remove the check-release-needed pre-push hook. The repo: local
skill-size-check and vale-audit-prefilter-* hooks are unchanged.
- ADR-0014 is amended, not retired: its runtime decision to bundle Vale
inside factory-audit stands. The amendment keeps the entry[0]-only
constraint (LESSONS.md:101,105) in case the export returns. ADR-0025
gets a pointer.
- test-vale-wrap.sh: drop case 33 (the cross-manifest drift check) and
case 28's hook-scope half, which read the published manifest. Case 32
now also requires each hook to select every tracked file of its class,
which keeps case 33's one-plugin-narrowing guard, with a mutation test.
- test-skill-size-check.sh and test-adr0020-contract.sh now assert the
hook contract and verbose: true on .pre-commit-config.yaml only.
- gates.md: pre-push count goes from 9 to 8 authored hooks (11 to 10
reported), and the Release table, the External consumers section and
the two-manifest scope table are removed. README and script/test
comments no longer describe the export as live. The resolver comment
is edited identically in both copies.
- The v1.0.0/v2.0.0/v2.0.1 tags are left in place; they are inert.
ADR: 0014
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Why: the audit carried stale figures and working-tree citations after the
grill commits landed, and this review round's decisions needed recording.
Implementation Notes: line totals pinned to c07ca07, working-tree
citations replaced with commits, version-location count corrected, and a
new section lists this round's dispositions.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Why: the docs said to discard a refreshed apm.lock.yaml on a feature
branch because the refresh records main's commit, but the branch's own
lock records a (older) main commit too, and the SessionStart notice gave
the same "commit or discard" advice on every branch.
Implementation Notes:
- check-apm-current.sh picks fixed advice by branch: commit or discard
deliberately on the default branch (origin/HEAD, else main), discard and
reinstall on a feature branch; the branch name is never interpolated.
- README, AGENTS.md and ADR-0019 give the real reasons (no lock churn in
the branch diff, deployed tree matches the committed lock), the cost
(the session runs the older main) and that the next session start
refreshes again.
- ADR-0019's check-clean and stale-server claims restated to match apm's
source.
ADR: 0019
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Why: for a local-path marketplace entry, apm treats a root description: as
an override of the package's own apm.yml, reported only at verbose level,
so the "two copies stay identical" rule had no enforcement. Same fix as
2def060 made for version:.
Implementation Notes:
- All six root copies matched their package apm.yml before removal; the
compiled marketplace.json descriptions are unchanged.
- apm-workflow references now scope the "omit it" advice to local-path
entries: on a remote entry, version: is the semver range that selects
the tag, and version: or ref: is required.
Impact: ADR-0021 amended; the package apm.yml is the single source.
ADR: 0021
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Why: each of these plugins changed shipped .apm/ content on this branch
without a package version bump, which configure.md requires and no gate
catches. No skill, agent or hook was removed, so a patch bump fits.
Implementation Notes: marketplace.json regenerated with apm pack; the diff
is the five version strings only.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Why: two branches that both bump a skill 1.0.0 -> 1.0.1 with different
content merge without a conflict, and each passed the gate against its own
merge-base, so main could ship two changes under one version.
Implementation Notes:
- check-skill-version-bump requires the pushed version to exceed both the
merge-base and the main tip; failures name the baseline they missed.
- Presence is read from the tree, so a blob missing from a partial clone is
a read failure instead of a silently exempt "new" skill.
- A leading UTF-8 BOM no longer reads as a missing version.
- Version parts reject leading zeros in all three validators
(check-skill-version-bump, skill-size-check, factory-audit).
- New tests cover equal bumps, moved files, major/minor ordering, bad refs,
unreadable blobs, mode-only changes, symlinks and tag peeling.
Impact: ADR-0022 amended (reverses "not main's current tip"); gates.md
updated to match, including pre-commit 4.6.1's exact ref selection.
ADR: 0022
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Re-measures the figures the new hook and ADR edits moved, re-points
shifted gates.md and config citations, ticks the decided §8 questions,
and aligns §7, §10 and the finding 8/18/28/33 notes with the decisions.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
The root apm.yml packages[] entries no longer carry version:, and a
version there is a silent override that --check-versions does not
catch. configure.md and marketplace.md now name the package's own
apm.yml as the single source and drop version: from the examples.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
ADR-0022 and gates.md now state the merge-base baseline, the fail-closed
cases, the PyYAML requirement and the multi-ref push gap (shared with
check-release-needed); the new hook gets its own gates.md group. Both
ADR additions follow each file's amendment format. README and AGENTS.md
now say to discard a refreshed apm.lock.yaml on a feature branch, and
that an .apm/ edit is live only once it is on the remote's main.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
- Read changed paths NUL-delimited so non-ASCII paths are no longer
silently skipped.
- Fail closed when only local main resolves and the pushed commit is
the merge-base, instead of passing on an empty diff.
- Accept ASCII-only versions with at most nine digits per part.
- Check for python3/PyYAML up front, and report read failures as such
rather than as a missing version; name a missing SKILL.md.
- Document that pre-commit gates only the first ref of a multi-ref push.
Tests grow to 29 cases covering each fix plus annotated tags, CRLF
frontmatter, unrelated histories and pushing main.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Findings 11, 17, 23, 25 and 27 are declined. From the grill: 33 and 34
done, 8, 18, 20 and 28 closed, 22 deferred with bin. Open: 15 and 36.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
The session-start refresh resolves against main, so on a branch it
redeploys content the branch removed and rewrites the lock to main's
commit. Documented as a consequence rather than skipped in code, since
a skip would only freeze the session on an older main. Also records the
re-measured refresh time (~24-26 s). Simplification audit finding 34.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
The six packages[].version lines restated each plugin's own apm.yml
version and were unpoliced: on drift apm silently shipped the curator
value. apm reads the plugin's apm.yml when the entry is absent, and
apm pack --check-versions --check-clean still passes with the committed
marketplace.json unchanged. Simplification audit finding 33.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
check-skill-version-bump fails a push when a skill directory changed
against its merge-base with main (tests/ excluded) without a strictly
higher metadata.version than main. New, renamed and deleted skills are
exempt; every plugin is covered. Recorded as a dated section in
ADR-0022 and documented in gates.md.
Patch-bumps the 17 skills that changed on this branch without a bump,
so the branch passes its own gate. Simplification audit finding 33.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Why
A six-agent review of the two preceding commits found their code sound -- the
differential claim holds, the published hook contract is byte-unchanged -- but
their prose drifted from it in three ways: statements of fact the code
contradicts, markers in a convention this repo does not use, and figures that
went stale when the merge changed what they counted.
Implementation Notes
ADR-0025's edge-path table is rewritten around one stated doctrine: 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. Its old row 1 promised "one
generic matches-neither message" for three different inputs; there are three
distinct messages, and the missing-path case exited 1 until the preceding commit
fixed it. Rows are added for the preflight and CDPATH changes, because a table
claiming to enumerate every entry-point behaviour change reproduces its own
"an earlier revision of this ADR said they were behaviour-neutral" failure if it
omits any.
ADR-0025 also gains a Consequences supersession record in ADR-0016's form:
partially-superseded entries for 0008, 0014, 0020 and 0021, and explicit
"is not superseded" entries with reasoning for the rest. Twelve ADRs are amended
and it previously listed none.
ADR-0008 moves from an amendment note to partially superseded. Its contract
genuinely narrowed -- an agent .md outside an agents/ directory was audited
before the merge and is refused now -- and ADR-0020 already recorded that the
merge "reopens ADR-0008". Its detector description said "a path under
.apm/agents/", the phrasing ADR-0025 rejects as wider than the script and
circular; the shipped rule is a .md whose immediate parent is named agents/, at
any scope.
ADR-0020's amendment claimed the boundary resolver is sourced by
validate-provenance.sh. It is not, and never was; only validate.sh sources it,
once per mode branch. Three Home-column entries pointed at reference filenames
the merge renamed, one of which now resolves to two files because its row covers
skills and agents.
Five ADRs opened with "Skill renamed per ADR-0025", a form this repo does not
use, in the same commit that used the conventional "Amended by ADR-0025" twice.
They are normalized. "Renamed" was also wrong: the BREAKING-CHANGE trailer says
the skills were removed and their flows merged.
SIMPLIFICATION-AUDIT.md had 2026-09-15 notes attached to headlines that were
never updated, against its own convention of correcting in place with
strikethrough. Every figure here was re-derived at HEAD by command, and several
differed from the review's own numbers, so the notes record the basis rather
than the result alone.
LESSONS.md asserted the two review-time suite failures were the SIGPIPE race.
The commit that fixed that race explicitly declined to claim it -- the suite was
running while agents edited live config files -- so the hedge is restored.
Impact
No code, test or configuration change; documentation only. Suites stay 20/20
strict with 0 skipped and 374/374 bats. No gate parses ADR or gates.md content,
so nothing here is load-bearing for a hook.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01YR2CjVumUbEGWcMikcoXBD
Why
Two suites failed intermittently — tests/test-vale-wrap.sh case 21 and
tests/test-check-release-needed.sh cases 4 and 15 — on correct output, and never
when run alone. The cause is the `echo "$OUT" | grep -q P` idiom under
`set -o pipefail`: grep -q exits as soon as it has an answer, bash's echo can
hand a multi-line value to the pipe one line at a time, and a write after the
reader is gone kills echo with SIGPIPE. pipefail then reports the writer's
death, so output that DID match reads as "no match". Every observed failure had
lines after its match; case 15's match is on line 1 of 6, the widest window in
that file.
Forced with a pause before the writer's last line, the pipe form failed 50 of 50
runs; a here-string, a match on the last line, and the same pipe without
pipefail each passed 50 of 50. Unforced the rate is about 1 per 670 suite runs,
which is why it read as a flaky gate rather than a bug.
The failures at review time are consistent with this, but were not proven to be
it: the suite was running while agents edited live config files in place, and a
brief change to .vale.ini or .pre-commit-hooks.yaml would produce the same two
failures. The race is real and fixed either way.
Implementation Notes
`grep -q P <<< "$VAR"` has no separate writer process, so there is nothing to
race. It is not a retry or a sleep. 121 sites converted across 9 files, three of
them scripts rather than tests: new-agent.sh, new-skill.sh and
check-executables-allow-sync.sh. None ships via .pre-commit-hooks.yaml, so no
external consumer pins them, and all three are single-pipeline checks whose
verdict cannot change.
Left alone deliberately: 14 sites whose writer is a command, not a shell
builtin — they either absorb the writer's status with `|| true` or are python3
and awk, which write once at exit — and one file with no pipefail. `printf '%s'`
sites differ from a here-string only by a trailing newline, which no -q verdict
on a non-empty pattern depends on.
tests/test-no-pipefail-early-exit-grep.sh is a static guard against new
occurrences, discovered automatically by run-tests.sh. It only scans files that
set pipefail, joins continuation lines, skips comments, and flags only
echo/printf writers. Its first case proves the scanner can fail before its
second trusts a clean verdict on the tree.
A guard covers exactly the spellings its regex models, so the miss surface was
measured rather than assumed. Four were found and closed: pipefail declared as
`set -o errexit -o pipefail` (where the old pattern required pipefail to follow
the FIRST -o, and a file-level miss skips every site in that file); a writer
separated from grep by an intermediate stage; a pipeline wrapped on a trailing
`|` rather than a backslash; and readers spelled egrep, fgrep, /bin/grep,
`command grep` or with an env-var prefix. Segment characters exclude a bare `&`
so `echo ok && other | grep -q x`, whose writer is `other`, does not false-fire.
Widening surfaced 5 live sites invisible to the original scanner, all in
tests/test-apm-current-hook.sh, all `echo "$out" | json_field ... | grep -q`;
they are safe today only because json_field is python3, which reads to EOF and
writes once. Fixtures go 4 to 12 vulnerable spellings plus near-miss negatives.
Two `grep ... | head -1` sites (test-vale-wrap.sh) are the same race with a
different early-exiting reader, and are fixed by absorbing the writer. The
scanner deliberately does not model `head`, `sed -n 1p` or a bare `read`: most
legitimate uses in this tree are already absorbed with `|| true` and the scanner
cannot see absorption from pipeline text, so a high false-positive rate would be
how this guard gets weakened. Heredoc bodies are scanned as code; none in the
tree trips it today.
Impact
The bug predates the factory-audit merge: every converted site in
check-release-needed and case 21 dates to 4d018af and aa8cc22 (2026-08-09).
Test suites go 19 to 20. `run-tests.sh --strict` passes 20/20 with 0 skipped,
four consecutive runs.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01YR2CjVumUbEGWcMikcoXBD
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 <[email protected]>
Claude-Session: https://claude.ai/code/session_01YR2CjVumUbEGWcMikcoXBD
Why: three governance documents cited `docs/HUMANS.md`, which has never existed -- the human
practitioner rules live at `docs/wiki/HUMANS.md`. One of the three is
`core/instructions/governance.md`, which is `@`-imported into every session in every project,
so an agent following its "read it when making decisions not covered here" pointer hit a dead
path. That file was self-inconsistent: line 73 already cited the correct path while line 82 did
not.
Implementation notes: five occurrences corrected across three files --
`core/instructions/governance.md:82`, `docs/research/governance_principles/CONTROLS.md:5,101,106`,
and `docs/ai-constitution.md:238`. Text is otherwise untouched; this is a path correction only,
not a change to any governance rule. Marked the defect fixed in SIMPLIFICATION-AUDIT.md, which
recorded it in two places as outstanding.
Impact: no rule, gate or behaviour changes. The deployed copy at
`~/.claude/core/instructions/governance.md` no longer matches the repo and stays stale until
`scripts/install.sh` re-runs; it was byte-identical before this commit.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01YR2CjVumUbEGWcMikcoXBD
Why: this audit was written read-only and its scope estimates proved systematically optimistic.
Ten open findings with claimed yield were re-verified against the files by independent agents.
One premise of ten survived, and the headline figure was wrong in at most eight of the ten.
Implementation notes: per-finding verification notes on 11, 16, 20, 22, 24, 27, 28, 33, 34 and
36. Finding 5 marked not proceeding, on the same grounds as finding 3 -- its six suites are
split by failure class, not ADR section, and five of the six headers name the incident they
guard. Finding 18 re-scoped and folded into finding 22 under three exemptions (audit criteria,
assets/templates and sourced spec restatement, the last now carrying a decidable test rather
than resting on the presence of source_keys). Finding 32 closed with its premise corrected.
Section 8 questions updated where measurement answered them: ADR-0012 is moot, git/gitea
granularity fails an enforced gate at 4.9x, and the external-consumer question has its evidence
but awaits a decision. New section 10 records the wave, the recurring failure mode behind six
wrong findings, and where the remaining opportunity actually sits.
The wave's own notes were then re-verified by a second independent round, and this commit
carries those corrections. The notes had an error rate comparable to the findings they
corrected. Four errors changed a verdict. Finding 11's note anchored its search at column 0 and
so missed every source_keys carrier nested under metadata:, producing "172 carriers" (196),
"zero of 40 SKILL.md files carry source_keys" (28 of 39) and "check 2 is dead code" (live, with
bats coverage); its double-counting accusation was a misreading of the word "plus" and is
withdrawn. Finding 28's note claimed 2,740 lines "has never matched any commit" -- it is exact
at a3e721e, the unique commit of the 67 touching docs/adr/ that yields it, and where all of the
finding's headline figures reproduce simultaneously; the finding went stale, it was not
fabricated. Finding 20's note argued the gitea split was blocked a fortiori by ADR-0011, which
inverts that ADR's reasoning (its objection is to a boundary being crossed, not to bundle size)
-- withdrawn and replaced with the same objection aimed at the correct seam, in the note and in
section 8. Finding 18's "sourced spec restatement" exemption collided with finding 20's own
salvage recommendation in the same commit and now carries a test that separates them.
Bookkeeping corrected throughout: the dangling docs/HUMANS.md path is five occurrences across
three files, not four (the sentence enumerated five while stating four); finding 16's c8a7c9e
chronology was inverted, and its resolver core is 549 executable lines, not 357, making it 2.7x
the proposed budget rather than 1.8x; finding 24's Q1-Q5 coverage is 20 tests and ~67%, not 24
and ~76%; finding 22's estimate is ~150-180 lines with its components summing, and its
RED/GREEN rebuttal no longer depends on ignoring the two diagrams the finding most plausibly
named; finding 27's preamble is 43 words; finding 28's proposal is a wash (+5 to -1) rather
than a firm +5; finding 32's citation is architecture.md:22 and its net is 6 lines. Section 10's
table reconciled against every corrected note.
Impact: no code, gate or behaviour changes. Two defects are flagged for independent fixing -- the
deployed core/instructions/governance.md cites docs/HUMANS.md, which does not exist, in five
places across three files; and apm update on this branch resolves against main and would restore
the obsidian MCP server removed in c96ca9c, via the regenerated repo-root .mcp.json, which is
gitignored and so would not appear in git status.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01YR2CjVumUbEGWcMikcoXBD
Why: AGENTS.md's Structure bullet restated apm-install mechanics already owned by
docs/spec/architecture.md:24 and README.md:55, and docs/VISION.md carried stack, framework and
deployment choices for a product that lives in a separate repo.
Implementation notes: AGENTS.md keeps two actionable one-liners plus pointers to the README
layout table and architecture.md, preserving the session rule that .claude/skills/ and
.claude/agents/ are install output and must not be edited. VISION.md's Phase 1 Architecture
block becomes a one-line scope statement; the "Mobile/desktop (Phase 3)" line is dropped as an
intra-file duplicate of the Phase 3 section.
Impact: no behaviour change. README.md and docs/spec/architecture.md are untouched -- the
finding's premise was inflated, and architecture.md had already been differentiated in a way it
documents in the file itself.
Refs: SIMPLIFICATION-AUDIT.md finding 32
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01YR2CjVumUbEGWcMikcoXBD