Commit Graph

34 Commits

Author SHA1 Message Date
88866b81a3 fix(kyberforge): announce every provenance skip and scope checks 7-8 to source indexes
The clean provenance bill was an artifact. Checks 7 and 8 assume `Research doc:`
names a source index whose H2s are slugs, but 30 of 121 corpus entries point at
topic content documents whose H2s are topics. Those 30 produced every new check-7
INFO — all false positives. Check 8 aimed at the same documents, which carry no
`Status:` line at all, would have emitted a large false-FAIL flood; the only thing
preventing it was an unannounced `rd_status != extracted` skip. So "0 new FAILs"
rested on exactly the fail-open class this branch exists to remove, and naively
fixing the skip would have turned the branch red.

Checks 7/8 now run only when the research doc's basename is `sources.md`, and every
other case emits a visible INFO naming the slug. The dangling-path INFO stays ahead
of the basename gate, because a path that does not resolve is rot whatever it is
named. `parse_status` accepts the bullet form and a trailing note after the
backticked value, so a status it cannot read no longer reads as "nothing to check".

Corpus: 36 INFOs of which 30 were false, to 56 of which none are. FAIL stays 0, and
no Status line flipped to `extracted` under the new parser, so no FAIL was
suppressed by luck.

Also closed, each a silent pass: a nonexistent directory, a directory with no
SKILL.md, and extra arguments now exit 2; a UTF-8 BOM no longer defeats frontmatter
parsing; the bare `except Exception: return False` that turned an unreadable file
into a clean pass is gone, with all reads pinned to UTF-8; check 3 walks nested
`references/` subdirectories; `FILL IN:` at end of line no longer escapes checks 1
and 6; duplicate `## slug` blocks and repeated `Research doc:` lines are announced
rather than half-read.

The agent-audit copy carried all of the above unfixed and is now ported, minus the
four fixes that are genuinely N/A at agent scope — it reads a plugin-root
`sources.md` and has no checks 7/8 and no `references/` tree. Its silent exit 0 for
a file outside plugin scope is preserved deliberately: that is a verdict about a
valid file, not a skip, and `check-scope-walkup-sync.sh` pins it. Every exit-2 gate
therefore decides from the argument alone, before the walk-up runs.

`validation-scripts.md` said flatly that silence from the validator is a pass, not
a skip. That sentence is what made a typo'd path dangerous, and both copies are
corrected here. The matching SKILL.md exit-code guidance lands with the audit
rubric change, which touches the same files.

Tests: skill-audit 45 to 65, agent-audit 24 to 43, every new case proven by mutation.

Refs: #111, #118, #121
2026-09-01 12:37:50 +00:00
9fe734573d fix(gates): close the /name fail-open and stop the path guard inventing targets
Two defects in the routing-target resolver, both latent in the corpus but hot for
anything written next.

The free-standing `/name` sweep sat inside `if boundary:`, so route notation in a
sentence carrying no boundary marker was never extracted at all — not an ERROR, not
a SUGGESTION, not an INFO. That contradicted ADR-0020's amendment and gates.md,
which both promise `/name` blocks unconditionally. The sweep now runs over every
sentence. `-> name` and backticked forms stay gated deliberately: an arrow also
writes a process chain and a code span cites tools, files and skills alike, so
ungating either fires on ordinary prose.

The path guard used `\b`, which still holds after a hyphen, so the engine
backtracked to a shorter hyphen-terminated prefix whenever the lookahead rejected
the full segment. `/api-docs/v2.md` in a boundary clause raised blocking ERRORs for
'api' and 'api-docs' — names no author wrote, with no corroboration escape.
`(?![\w-])` forbids the shortened prefix outright; MARKED_TARGET, which had no
trailing guard at all, gained one.

Zero arguments now exits 2 rather than 0, so a mis-scoped `files:` pattern is no
longer indistinguishable from a clean corpus. Both hook manifests pass filenames
and pre-commit skips a filename-passing hook when nothing matches, so the hook
never sees an empty argv — that contract is now asserted by a test rather than
left in prose.

Deleting the sweep entirely used to leave every suite green. It now kills eight
assertions. The suite also gains its first slash-path and URL fixtures, in both
directions.

Refs: #107, #110, #124
ADR: 0020
2026-09-01 12:37:12 +00:00
00daf285ec fix(kyberforge): tell an unparsable Contributing-files block from an explicit (none)
parse_contributing_files documented that callers depend on None vs [], because a
parse failure returning [] would silently disable the check. Only check 8
honoured it; checks 4/5 (skill-audit) and 3/4 (agent-audit) used a truthiness
test, so an unreadable block disabled them without a word.

Two live corpus entries were skipping this way. A sweep of all 32 sources.md
found 134 entries, exactly 2 parsing to None, both in gitea-files: one heading
carried an inline parenthetical that defeated both regexes, and one (none) was
written without its leading bullet. Also pins EMPTY_SOURCE_KEYS_RE to the two
indents parse_source_keys actually reads.

agent-audit had no INFO tier at all, so it gains one rather than reporting a
check that could not run as a FAIL.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EJJrm5YmacbwMdzZpXcoti
2026-08-31 19:46:05 +00:00
c232e69645 fix(gates): block a dangling /name route with no preceding verb
The header promised explicit route notation always blocks. It did not: /name
reached extraction only behind a ROUTE_VERB, so a target with no verb before it
was never extracted at all -- exit 0, no output. Taking the SUGGESTION's own
advice ('write it as /name and it will be checked properly') was the one edit
that blinded the gate.

Adds two notation sweeps gated on BOUNDARY_MARKER and routed through _add, plus
a path guard so file paths and URLs are not read as routes. Also excises the
matched pointer span before the REFERENCE_PAST sweep, so a reference file can no
longer exempt itself by its own filename, and guards the agent branch with the
isfile test the skills branch already had.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EJJrm5YmacbwMdzZpXcoti
2026-08-31 19:45:45 +00:00
27a76692b0 fix(kyberforge): stop the provenance checker skipping check 8 on unparsed input
484357a taught the Contributing parser the bullet form, but a block it still
could not parse returned the same empty result as an explicit "(none)", so the
checker read "no contributing files" and skipped check 8 rather than reporting
that it could not tell. Checks 7 and 8 were consequently dead across the whole
git plugin without anything failing.

The parser now distinguishes "declared none" from "could not parse", which
wakes both checks. Because the parser is duplicated between the skill-audit and
agent-audit copies, it is fenced with BEGIN/END markers and a test hashes the
two regions so the copies cannot drift apart again silently.

Addresses #111.
2026-08-31 08:01:18 +00:00
db5a426416 fix(gates): make the ADR-0020 boundary check parse what skills actually write
The routing-target check understood only a single-arrow clause naming a bare
skill, so most real boundary prose was silently skipped rather than verified.
Two of those silences were fail-open: an unrecognised token following a target
dropped that target from the check entirely, and a skill directory with no
SKILL.md still resolved as a valid routing target, so a broken route passed.

Multi-target arrow clauses now draw a SUGGESTION instead of being ignored,
hand-invocation phrasing is carved out so it is not read as a route, and a
dotted filename parses into a new `unparsed` status rather than disappearing.
Three test fixtures had been relying on the SKILL.md-less directory resolving
as a target; they are corrected alongside the check.

Addresses #107, #108, #110.
2026-08-31 08:01:07 +00:00
484357a3b9 fix(gates): parse the bullet form of Contributing files
validate-provenance.sh matched Contributing files only as a single inline
line beginning "- **Contributing files:**". Seven skills write it as a
bare "**Contributing files:**" heading above a bullet list, so
parse_contributing_files returned None and checks 4 (contributing file
exists) and 5 (bidirectional source_keys) silently verified nothing on
git-branches, git-remotes, git-submodules, git-workflow, git-worktrees,
gitea-files and gitea-releases.

Those are among the skills this branch changed most — git-branches alone
gained five reference files — and the retrofit's mandatory sources.md
collateral went in unchecked. Demonstrated rather than argued: planting a
nonexistent contributing path in git-remotes yields 0 findings under the
old parser and 1 FAIL under the new one.

Both forms are now accepted. The bullet form is parsed per bullet rather
than by splitting a joined value, because its per-file notes contain
commas that would otherwise be read as path separators. The return type
becomes a list of note-stripped paths, with "(none)" as an empty list and
an absent entry as None, so the two callers no longer re-split a string.

Applied to agent-audit's copy as well. No agent ships a sources.md today,
so it is latent there, but it is the same defect.

This is a third gate blind spot alongside #117 and #118, and was unfiled.
One real defect surfaced immediately and is fixed separately.

Refs #99
2026-08-30 20:51:56 +00:00
79c60715dc fix(gates): restore byte-identity of the shared ADR-0020 boundary resolver
915eb09 rewrote the bare-arrow carve-out comment in scripts/skill-size-check.sh
without pasting the block over the two other copies, so the resolver stood at
764 lines in the hook and 755 in both audit validators.
tests/test-adr0020-contract.sh exists to catch exactly this and did -- it was
the regression behind that suite's failure, and I reported the suite green
after 915eb09 without re-running it.

No behaviour changes: the drift was comment-only. Restoring identity keeps the
invariant the test enforces, which matters because the three copies are the
only thing making the hook and the two validators agree on what a boundary
target is.

Refs #99

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MWb5RQgCL1ye7cGp2RPb2u
2026-08-30 16:42:51 +00:00
75a13c82f6 fix(kyberforge): scope corroboration to a real sentence boundary
A prose-form routing target blocks a commit only when its own sentence names
another target that resolves. That makes the sentence splitter part of the
ADR-0020 contract rather than an implementation detail, and the naive
"period, space, capital" rule got it wrong in both directions:

- OVER-SPLIT: `e.g. "..."` is not a sentence end, but the quote looks like a
  start. The clause was cut in half and the corroborator stranded on the far
  side, so a genuinely dangling target silently demoted to SUGGESTION — a
  measurement taken and then discarded, the vacuous-green shape this gate
  exists to prevent. Seven such splits are live in the current corpus.
- UNDER-SPLIT: a sentence opening with a code span or a lowercase skill name
  was not seen as a start, so two sentences merged and a resolving target
  vouched for an unresolvable one it never stood beside — a hard FAIL with no
  escape hatch, which is the exact failure corroboration was added to prevent.

The splitter now excludes the five abbreviations that occur in routing prose
and admits a backtick or lowercase letter as a sentence opener. Applied
byte-identically to all three copies of the shared resolver.

Verified zero-delta against the corpus: 37 ERROR / 58 SUGGESTION / 2 dangling
before and after, findings byte-identical. The exposure this closes is to the
descriptions #99 is about to rewrite, not to the ones already measured — which
is why the deferral reason recorded on PR #103 ("can move the documented corpus
counts") does not hold and the fix lands here rather than after the retrofit.

Three regression tests, one per direction plus the backtick opener, each proven
non-vacuous by reverting the splitter alone and watching it go red.

Refs: #99
ADR: 0020

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-16 20:44:58 +00:00
f7cc27908c fix(kyberforge): close the vacuous-green and consumer-resolution defects
Review of the ADR-0020 gate found four ways it could exit 0 without measuring, and
one way it hard-failed a repo it had no business failing. On a gate shipping hot
with no baseline, a silent pass is the worst outcome available and a false block is
the second worst.

Consumer resolution was the blocker. _authoring_root() fell back to the nearest
.git, so it returned truthy in ANY git repo; _collect_authoring_root() then
contributed nothing and the deployed-tree branch was dead code in precisely the
consumer case it exists for. A consumer repo routing to an installed sibling got an
unblockable ERROR, and deleting .git "fixed" it. It now keys on which of the two
walk-up passes matched. A name-count delta was tried first and is wrong: a
single-plugin monorepo re-collects its own package and adds no new name, so the
delta reads zero and drags the deployed trees — including a global ~/.claude — back
into the universe. That reintroduces the install-dependence ADR-0020 forbids, one
layer down.

The three silent passes: an indented `---` inside a block scalar truncated the
frontmatter and reclassified the rest of the description as body; a non-string
description was str()-coerced, so `description: true` measured as the four-character
"True"; and an unterminated fence blanked the rest of the body, disabling the
ERROR-tier references/ check and the gotcha counts.

Two measurement defects came with them. The awk line/word counts discarded awk's
exit status, so an unreadable file passed both spec ceilings in total silence, and
awk NR/NF disagreed with the audit script's splitlines()/split() on Unicode
whitespace — the "fix one gate, get blocked by the other" bug, on the two axes the
differential test deliberately excluded. Both counts now run in the Python block
that already reads the file. A type error also no longer reports itself as a syntax
error.

Also: glob metacharacters in the checkout path silently disabled the resolver;
re.I was applied to some extraction patterns and not others; agent-audit missed
`tools:` written as a YAML block sequence, the shape Copilot files use; and a
nonexistent agent file raised a bare FileNotFoundError instead of a diagnostic.

The shared resolver block stays byte-identical across all three scripts. Corpus
output is unchanged — 26 description FAIL, 9 body FAIL, 2 dangling, 0 missing
references, 58 SUGGESTIONs — so no documented count moves.

Refs: #99
ADR: 0020

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015W3iwF9ncfRZddGBxsMCYi
2026-08-16 19:48:56 +00:00
b6e68e9a2b fix(kyberforge): close the vacuous-pass paths in the ADR-0020 gate scripts
Three ways the gates could report green having measured nothing. All three were
invisible to a passing test suite, because pre-commit prints nothing at all for a
hook that exits 0 — a gate that declines to check and a gate that checked and
passed produce the identical signal.

- A UTF-8 BOM, a leading blank line, a trailing space after a `---` marker or
  CRLF line endings defeated the `^---\n` frontmatter matcher. Every ADR-0020
  check was then skipped and the file passed: measured at the time, a
  550-character description with a 1,000-word body exited 0 behind a BOM.
  All four shapes are now tolerated, and frontmatter that genuinely cannot be
  parsed is a hard ERROR rather than a silent skip.
- An agent file with a valueless `description:` followed by another key let a
  line regex capture the *next* key, which looked non-empty, so the
  missing-or-empty branch never fired and every gate below it early-returned on
  the empty folded value — zero output, exit 0, on a blocking gate. The one
  field this contract is entirely about was the one field a gate could fail to
  notice was absent. Presence is now decided on the YAML-folded value and
  nowhere else, and a missing or empty description is a hard FAIL in all three
  validators.
- The hand-rolled frontmatter fallback disagreed with PyYAML across the FAIL
  boundary on folded scalars, so which reader happened to be available decided
  the verdict. A fallback that mis-parses a scalar shape reports a vacuous pass,
  which is worse than not running, so it is deleted: python3 and PyYAML are hard
  requirements that fail loudly with an install pointer.

Boundary-target resolution no longer derives its universe from its own location.
A `${BASH_SOURCE}`-relative repo root leaked this repo's 39-skill universe into
every consumer repo running the hook through pre-commit, so a consumer skill
routing to `skill-audit` resolved against a plugin it had never installed. The
interim form resolved through `.claude/` and `.agents/`, which are gitignored
`apm install` output — the same commit reported 2 dangling targets on a machine
that had run the install and 6 on a fresh clone. Resolution now walks up from the
file being checked to an authoring root (nearest ancestor holding
`plugins/*/.apm/{skills,agents}`, else the nearest `.git`, in two passes so a
nested `.git` cannot outrank a real monorepo root); the universe is every skill
and agent under `<root>/plugins/*/` plus the file's own apm package and that
package's declared `dependencies.apm`. Deployed trees are consulted only when no
authoring root exists at all — the consumer case. One commit now gets one verdict,
which a gate shipping hot with no baseline file has to.

Narrowed in the same pass: a routing target inferred from the prose boundary form
and corroborated by nothing else reports at SUGGESTION instead of blocking. A
blocking check with no escape hatch is the wrong trade when the inference from
prose is the weak part of it.

New deterministic checks, all previously untested or absent: every
`references/<file>.md` a body names must exist (ERROR — a broken pointer is not a
style opinion); a description with no boundary clause at all, a Gotchas section
over five entries, and a Gotchas section over 25% of the body are SUGGESTIONs.
Where no universe can be determined the target check prints `INFO ... DID NOT
RUN` rather than passing quietly. Each prose-scanning check needed its own
false-positive fix — a fenced example of a Gotchas section was being read as the
section itself — and those fixes are pinned rather than assumed.

The resolver is one block copied verbatim into all three scripts between
BEGIN/END markers, because a cache-installed plugin's scripts cannot read outside
their own plugin directory. Nothing asserted the copies were still identical; a
one-line edit to a single copy passed every constant-agreement assertion, since
constants are not what drifts.

Tests land here rather than in a later commit. The existing suites assert the old
behaviour and go red against these scripts, so splitting them would leave a commit
whose own `run-tests` pre-push gate fails in isolation.

Refs: ADR-0020
2026-08-16 16:39:29 +00:00
4a5c3c0cff feat(kyberforge): enforce the ADR-0020 context contract for skills and agents
Skill name+description pairs are preloaded into every session, costing
~6,200 tokens across 39 skills before any skill is invoked. The authoring
rules mandated that growth: skill-author:104 and description-quality.md:21
both required padding, while skill-author:102 (the deflating rule) had no
FAIL condition behind it.

Gates (blocking, no baseline file):
- description 250 chars SUGGESTION / 400 FAIL, measured on the folded
  YAML value
- body-only 600 words SUGGESTION / 900 FAIL, independent of the unchanged
  whole-file 2770-word / 500-line spec backstop
- every boundary-clause routing target must resolve to a real skill or
  agent; catches skill-improve, neuledge-context and gitea-labels
- agents take the description gates but deliberately no body gate; a test
  pins that absence

Vale: DescriptionOpener widened to ^This\b, new CompositionNote rule
banning architecture notes from descriptions. 10 hits, 0 false positives.

Kyberforge's own four skills retrofitted: descriptions 3,364 -> 938 chars
(-72%), bodies 8,306 -> 2,487 words (-70%), all via the apm-workflow
dispatch pattern. Fixes the skill-improve dangling route and the
agent-author misroute to manual review.

Also fixes a pre-existing false positive where any line-initial 'read '
was flagged as interactive input, which had already caused two scripts to
be rewritten around it.

Refs: ADR-0020
2026-08-14 21:13:13 +00:00
c16ec2d45a chore(apm): regenerate content mirrors
Output of scripts/sync-plugin-content.sh --all against this round's .apm/
source edits. No file here is hand-edited.

Carries the agent-author and agent-audit documentation changes into the flat
mirrors. No compiled manifest changed: nothing in this round touched apm.yml,
so apm pack and sync-marketplace-mirror.sh both produced byte-identical output.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X7GvKuJfy2WrdBmUttV4DT
2026-08-14 12:30:38 +00:00
874bf06b18 chore(apm): regenerate compiled manifests and content mirrors
Output of apm pack, sync-plugin-content.sh --all and
sync-marketplace-mirror.sh against this round's source changes. No file here
is hand-edited.

Carries the version bumps and marketplace owner.email into the compiled
manifests, the disallowedTools frontmatter and doc corrections into the flat
mirrors, and changes plugins/bin/.github/plugin/plugin.json's mcpServers from
the inlined server object to the ".mcp.json" pointer. That last file also
returns to 0644: the previous re-injection wrote it through mktemp and carried
0600 across, which no gate could see because the mode check did not cover
.github/plugin/ and git tracks only the exec bit.

.agents/plugins/marketplace.json is unchanged and that is correct -- apm's
codex profile carries neither version nor owner keys, so nothing in this round
reaches it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X7GvKuJfy2WrdBmUttV4DT
2026-08-14 11:05:20 +00:00
38f1ba4e03 fix(kyberforge): bridge apm content to Claude Code's flat plugin discovery
Claude Code's (and Copilot's) native plugin installer has zero awareness of
.apm/ nesting -- it convention-scans only flat skills/, agents/, commands/,
hooks.json at each plugin's root. Confirmed via strings on the installed
claude binary and live installs of git@holocron/gitea@holocron/kyberforge@
holocron, all reporting Skills(0) Agents(0) Hooks(0) post ADR-0015's apm
conversion. Root cause (apm_cli/core/plugin_manifest.py): apm's plugin.json
compiler deliberately strips skills/agents/commands keys, assuming the host
already auto-discovers those convention directories -- it has no model of
.apm/ being host-visible at all. Separately, apm's own bundle exporter
(apm_cli/bundle/plugin_exporter.py, behind `apm pack --format plugin`)
implements the correct .apm/ -> flat mapping, but only ever targeted
build/<name>-<version>/, a path nothing in marketplace.json's source: points
at.

scripts/sync-plugin-content.sh wraps that bundle exporter and copies its
agents/, skills/, commands/, instructions/, extensions/, and merged
hooks.json back into each plugin's own root as a second tracked
compiled-output category -- same governance status as
.claude-plugin/plugin.json: generated from .apm/, never hand-edited. tests/
subdirectories are excluded from the mirror (dev fixtures, not host-visible
runtime content; several hardcode a relative repo-root walk-up sized for the
.apm/-nested depth, which breaks when duplicated one level shallower).
Applied for real across all 6 plugins and verified two ways: `claude plugin
validate --strict` passes on every real plugin directory, and a live
`claude --plugin-dir <path> -p "list skills/agents"` behavioral test
confirms content is now actually discovered.

Also, from the same issue #90 review round:
- scripts/check-manifests.sh pointed at each plugin's root-level plugin.json
  (checking skills/hooks/mcpServers/agents pointer fields) -- that file was a
  stale near-duplicate of .claude-plugin/plugin.json nothing else read or
  wrote, now deleted across all 6 plugins. check-manifests.sh is rewritten to
  validate .claude-plugin/plugin.json instead, and drops the pointer-field
  checks entirely (nothing to check -- those fields are correctly absent by
  design). Content-presence drift is now check-plugin-content-sync's job, a
  new pre-push hook wired in .pre-commit-config.yaml.

docs/adr/0017 records the root cause and decision in full, including two
rejected alternatives (patching plugin.json's path fields directly -- apm's
compiler strips them on every run; pointing marketplace.json at apm pack's
build/ output -- a version-suffixed non-source directory nothing can install
from without an extra build step). ADR-0015 and CONTEXT.md are updated to
point at it.

Refs: #90
2026-08-13 16:59:03 +00:00
5e232503c4 feat(kyberforge): execute plugin-to-apm marketplace conversion
Why:
ADR-0015 established that Microsoft APM (apm.yml + .apm/) should replace
this repo's hand-authored plugin.json/marketplace.json model, with those
files becoming compiled output of `apm pack` instead of files edited by
hand via the (now-retired) plugin-author/marketplace-author skills.
Issue #90 was the deferred execution of that decision, gated on #88
(apm tooling) and #89 (apm-native agent-author/skill-author routing).

Implementation notes:
- All six plugins (bin, core, git, gitea, kyberforge, lint) now carry
  apm.yml + .apm/{skills,agents,hooks} as their authoring source. Skills
  moved with a plain git mv (content-identical across targets). Agents
  were re-authored, not moved: per ADR-0016, .apm/agents/*.agent.md
  compiles verbatim to both Claude and Copilot, so plugin-scope agents
  now carry only name/description/model/source_keys -- no tools: field,
  no Claude-only knobs (isolation, maxTurns, effort, memory,
  permissionMode).
- Root apm.yml registers all 7 marketplace packages (6 local plus
  mattpocock-skills as a remote entry) under versioning: per_package,
  matching this repo's existing independent-plugin-versioning practice.
- .claude-plugin/marketplace.json and every plugin's plugin.json are now
  apm-pack-compiled output, verified against the prior hand-maintained
  content: same names/descriptions/versions/licenses/authors, only
  cosmetic serialization differences (JSON key order, owner email vs.
  url, Unicode escaping).
- plugin-author and marketplace-author are retired now that apm-based
  authoring fully replaces their job; kyberforge bumped 1.3.1 -> 1.4.0
  for that removal, and the root marketplace catalog bumped
  0.3.1 -> 0.3.2 to match, per the version-bump convention now
  documented in apm-workflow's reference docs instead of a dedicated
  script (apm has no native version-bump automation).
- Fixed hardcoded pre-.apm/ path assumptions across
  .pre-commit-config.yaml, .pre-commit-hooks.yaml,
  scripts/check-scope-walkup-sync.sh, scripts/sync-vale-styles.sh,
  scripts/check-vale-style-sync.sh, six plugins' root plugin.json
  (stale skills/hooks/agents pointer fields that check-manifests.sh
  validates), and several tests/*.bats and tests/*.sh fixtures --
  including a bats REPO_ROOT relative-path depth bug (10 files, one
  extra .apm/ directory level to walk up) and a vale probe-path
  isolation regression introduced mid-fix.
- Corrected empirically-wrong assumptions surfaced this session in
  apm-workflow/apm-install's own reference docs: `apm marketplace
  package add` does not accept local paths (only owner/repo remote
  shorthand -- local packages are registered by editing apm.yml's
  marketplace.packages[] directly); `apm compile` is a consumer-side
  AGENTS.md/CLAUDE.md generator, not the plugin.json producer, and
  hard-fails on skill/agent-only packages without --clean; `apm plugin
  init <name>` nests a stray subdirectory when run with a positional
  name arg from inside a same-named directory; no native Copilot
  marketplace output profile exists; .mcp.json is merged into the
  compiled plugin.json content-aware and target-scoped, with no
  dependencies.mcp entry needed for simple passthrough; pipx is the
  correct pip fallback on externally-managed Python environments.
- Renamed agent-author's copilot.agent.md template asset to
  copilot.agent.md.template so apm compile's recursive *.agent.md glob
  stops misparsing the placeholder template as a real agent primitive.

Impact:
plugin.json and marketplace.json are compiled artifacts from here on --
editing them by hand is no longer the workflow; edit apm.yml/.apm/ and
run apm pack. CONTEXT.md's Plugin/Plugin marketplace glossary entries
reflect this. ADR-0001 is marked superseded, ADR-0006 moot, and
ADR-0010 updated for the new .apm/agents/ path (project/user scope
unaffected, per ADR-0016). Full local verification: claude plugin
validate --strict on all 6 plugins, apm audit --ci, apm marketplace
check, check-manifests.sh, and the full test suite (165/165 bats,
13/13 shell scripts) all pass clean.

Fixes: #90
Refs: #88, #89
ADR: 0015
ADR: 0016

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ub96PyaSRD9BHPktotj1pC
2026-08-12 18:21:24 +00:00
044b2d3f08 fix(kyberforge): fix HOME/git scope-walkup false-FAILs in agent-audit
validate.sh's detect_scope() and validate-provenance.sh's
find_plugin_root() disagreed with new-agent.sh's already-correct,
documented walk-up semantics on three points, each causing validate.sh
to false-FAIL a legitimately-scaffolded project-scope agent pair:

- a marker-less directory walked up into $HOME (no .git/apm.yml of its
  own) was classified as user scope instead of project scope
- the .git-boundary branch returned the walked-to .git location instead
  of the conventional scope root, breaking any <root> that is a
  subdirectory of a larger git-tracked tree (monorepo package dirs)
- the new conventional-root arithmetic introduced to fix the above two
  cases had no guard against non-conventional/hand-placed file paths,
  which could point it at the wrong ancestor

Also adds scripts/check-scope-walkup-sync.sh, a behavioral drift-guard
(per ADR-0014's no-cross-skill-path precedent) that cross-checks the
four independently hand-ported walk-up implementations (validate.sh,
validate-provenance.sh, new-agent.sh, new-skill.sh) against real
fixture scaffolds, wired into .pre-commit-config.yaml at pre-push so
future drift between the ports is caught automatically.

Verified via bash tests/run-tests.sh (13/13) and targeted before/after
reproduction of each bug this closes.
2026-08-12 11:35:23 +00:00
6f6b70781d fix(kyberforge): fix scope walk-up and manifest-parsing bugs from PR #93 review
A fresh /code-review of the APM-native authoring retarget (PR #93) found
several correctness bugs beyond the ones already fixed on this branch:

- new-agent.sh silently walked a marker-less subdirectory under $HOME up
  to user scope, contradicting its own usage text ("user scope is checked
  directly, no walk-up") and risking scaffolding into shared global
  ~/.claude or ~/.copilot directories instead of the intended local path.
- The hand-copied apm.yml type: manifest detector in new-agent.sh and
  new-skill.sh accepted mismatched quotes (e.g. `type: "skill'`) that
  validate.sh's regex correctly rejects, and silently dropped a final
  apm.yml line lacking a trailing newline — causing the scaffolder and
  validator to disagree on scope for identical input.
- Plugin-scope agent frontmatter could still contain the apm-agent.md
  template's HTML comments at ship time with no audit signal, yet
  apm compile copies frontmatter verbatim and <!-- --> breaks YAML
  parsing on both downstream harnesses.
- ADR-0016 asserted agent-audit already implements a SUGGESTION heuristic
  for tool-restriction-needing plugin-scope agents; it doesn't.
- agent-audit/README.md still described the old plugin-pair model this
  PR replaced with a single-file allowlist model.
- validate.sh's project/user-scope CC-only/Copilot-only field checks and
  counterpart-missing check lost their only test coverage when the old
  plugin-pair fixture was deleted.

Also replaces an echo-into-sed two-value parse (4 forks per call) with a
single space-separated echo + read in both scaffolders.

Regression tests added for every fix above, including one for a bug this
pass introduced and the test suite caught: an initial two-line
echo + `read` attempt silently dropped the second value, since `read`
consumes only one line regardless of embedded newlines.

Full suite: 158 bats tests, 39 shell-script tests, 12/12 summary
categories, 0 failures.

Refs: #89, #93
2026-08-11 21:49:38 +00:00
f037d49b5c fix(kyberforge): fix apm-scope validation gaps in agent/skill authoring scripts
Post-implementation review of PR #93 (issue #89's apm.yml-native retargeting
of skill-author/skill-audit/agent-author/agent-audit) found four confirmed
defects across the four scripts' apm.yml `type:` walk-up logic:

- field-inventory.md's apm-agent-allowlist was missing `source_keys`,
  contradicting agent-author/SKILL.md's own instruction (Step 5 checklist)
  to allow it at plugin/APM scope — a correctly-authored file with
  source_keys failed validate.sh.
- validate.sh's APM_TYPE_RE and validate-provenance.sh's TYPE_RE disagreed:
  the former tolerated a quoted `type: "skill"` value, the latter didn't,
  despite agent-audit/SKILL.md explicitly documenting that
  validate-provenance.sh walks up "the same way validate.sh does". Both
  also used `\b` word-boundary matching, which false-matches a malformed
  value like `type: prompts-only` on the `prompts` prefix. Unified both
  regexes to be quote-tolerant and require an exact value.
- All four scripts' `.git` project-boundary check used isdir()/[[ -d ]],
  which misses git worktrees where `.git` is a regular file (`gitdir: ...`)
  rather than a directory. Switched to exists()/[[ -e ]].
- new-agent.sh and new-skill.sh had the same quote-intolerance as above via
  inline `grep -qE` calls (new-skill.sh's also had the `\b` false-match
  bug); replaced both with a shared-shape `is_apm_package_manifest` bash
  helper matching the Python regex's semantics.

Four other findings from the same review turned out not to be bugs: a
bare `plugin.json` no longer signaling plugin scope is documented,
intentional behavior (agent-audit/SKILL.md:30, agent-author/SKILL.md:87),
deferred to issue #90's real plugin.json-to-apm.yml conversion — not
something this fix should reverse.

Verified via direct reproduction of each defect plus the full test suite:
147/147 bats tests, 39/39 shell-script tests, 12/12 summary categories.

Refs: #89
2026-08-11 19:13:26 +00:00
ffebdc6584 fix(kyberforge): stop dotfiles-repo $HOME from shadowing user scope in validate.sh
detect_scope() had the same bug class fixed in new-agent.sh (099bdec):
it checked for a .git directory before checking whether it had reached
$HOME, so a dotfiles-managed home directory (yadm, chezmoi bare-repo,
etc.) made validate.sh misresolve to project scope, deriving the
counterpart as ~/.github/agents/<name>.agent.md instead of the correct
~/.copilot/agents/<name>.agent.md and failing with a false "counterpart
file not found". Check the $HOME boundary before the .git check, same
fix shape as 099bdec.

Found via post-implementation review of issue #89.
2026-08-11 18:34:27 +00:00
dc2a41034e fix(kyberforge): remove dead plugin-scope branch from validate.sh
check_file()'s is_plugin_scope param and its plugin-silently-ignored
field check were unreachable dead code left over from the issue #89
restructure: plugin/APM scope now exits via check_apm_agent_file()
before check_file() is ever called, so is_plugin was always False.
Remove the param, its branch, the unused plugin_ignored_fields parse,
the now-stale field-inventory.md section, and the SKILL.md mention.

Found via post-implementation review of issue #89.
2026-08-11 18:21:51 +00:00
675ba40238 feat(kyberforge): restructure agent-audit for plugin-scope apm agents
Validates the new single-file .apm/agents/<name>.agent.md shape agent-author
now produces at plugin/APM scope: frontmatter allowlist (name/description/
model only, from a new apm-agent-allowlist entry in field-inventory.md),
no counterpart derivation, and Pair Consistency dropped from that scope's
report entirely (nothing to pair by design). Adds a plugin/APM-scope-only
SUGGESTION when an agent's description/body implies a tool restriction or
Claude-only behavior the vendor-neutral frontmatter can no longer express
(ADR-0016).

Scope detection in both validate.sh and validate-provenance.sh switches
from a flat plugin.json/.claude-plugin/plugin.json check to a walk-up for
the nearest ancestor apm.yml with a top-level type: field, skipping
type:-less marketplace-only manifests — full switch, no dual-mode fallback
to the old plugin.json signal. validate-provenance.sh's walk-up was fixed
to match validate.sh's (it still used the old plugin.json check, and its
counterpart-merge logic was rewritten to read a single file's source_keys
instead of merging a CC+Copilot pair, since plugin/APM scope has no
counterpart). Project/user scope validation is unchanged in both scripts.

Refs: #89
2026-08-11 18:05:33 +00:00
680aa4f43c refactor(kyberforge): consolidate vale-wrap.sh's config parsing and subprocess spawns
Two efficiency findings from a code-review pass:
- The separated (--config X) and joined (--config=X) argument branches
  duplicated ~20 lines of absolutize-if-relative path logic. Extracted
  into abs_config_value(), used by both branches; the redundant
  --config=/* special case falls out since the helper already passes
  absolute paths through unchanged.
- The common single-file path spawned python3 twice per file (once for
  abspath resolution, once for flatten()). flatten() now optionally
  takes a tmpdir arg and does both in one process. The per-file loop
  under a directory argument is unchanged — that path wasn't flagged.

agent-audit's copy is canonical; skill-audit's copy was regenerated via
scripts/sync-vale-styles.sh, not hand-edited, to guarantee byte parity.
No hardening (bash 3.2 compat, surrogateescape, symlink guards) touched.
Verified: tests/test-vale-wrap.sh 39/39, check-vale-style-sync.sh clean,
full suite 12/12.
2026-08-09 20:14:23 +00:00
ad1e5aaa9b fix(kyberforge): carry apostrophes verbatim through a |- literal block
The flattener's last-resort branch rewrote ASCII ' to U+2019, justified as the
one combination no YAML scalar can carry verbatim. That claim was false: a |-
literal block with a single indented content line carries ', ", \ and ": "
verbatim and keeps text.frontmatter.description matching — as the wrapper's own
docstring already said of literal blocks. The rewrite fired on 12 of 54
in-scope files, silently disabling every rule whose token contains an
apostrophe. Case 20 pinned only that the scope stayed alive, so it passed
either way.

The emission site now splits the emitted scalar on its first newline so a
carried-over trailing comment stays on the "description: |-" header line rather
than becoming part of the value, and pads by span_lines - 1 - newlines. The pad
stays non-negative because the branch is only reachable when the original span
is at least two lines. Verified across all 73 in-scope files: no line-count
changes, and exactly the 12 expected files take the new branch.

One reported position moves: an alert on a description that is itself flagged
shifts from the key line to the block's content line, both inside the original
span. YAML cannot put a literal block's content on the key's own line, so this
is unavoidable; no line at or after the end of any description span moves.

Also: --output no longer absolutises the built-in style names line, JSON and
CLI, which a same-named file or directory in cwd turned into a template path
(exit 2, E100 Runtime error). And case 19's empty-baseline guard no longer
lets five dependent comparisons print vacuous passes — while fixing it the
guard turned out to be unreachable, since under pipefail an alert-free report
aborted the script at the assignment.

Refs: #85
ADR: 0014
2026-08-09 17:23:24 +00:00
aa8cc22695 fix(lint): flatten every multi-line description form in vale-wrap.sh
Vale locates a frontmatter description by matching the parsed YAML value back
against the source text, so any scalar whose value is not spelled out verbatim
loses the `text.frontmatter.description` scope entirely. The wrapper only
flattened `>` folded scalars, so plain, double-quoted and single-quoted
multi-line descriptions silently reported zero alerts and exit 0 — a clean pass
indistinguishable from a real one, in a prefilter whose callers are instructed
not to re-derive its verdict by judgment.

Implementation notes:

- Classify the scalar kind after `^description:[ \t]*` and reuse one shared
  continuation-line generator for every form; `|` literal blocks keep their
  line breaks, stay verbatim-matchable, and are still left untouched.
- Emit the flattened value in whichever scalar form needs no escape at all
  (plain, then single-quoted, then double-quoted), because any escape breaks
  the verbatim match. The old blanket `'` -> U+2019 substitution silently made
  apostrophe-bearing rule tokens unmatchable across 63% of the corpus; it now
  survives only for the one combination no YAML scalar can carry verbatim.
- Terminate continuations at a line flush with the key, not only on a shallower
  indent — a `description:` followed by a flush-left line previously swallowed
  the rest of the frontmatter.
- Route vale's value-taking flags explicitly instead of inferring targets by
  file existence, and absolutize relative `--output`/`--path` values the way
  `--config` already was, since the run `cd`s into the scratch mirror.
- Fail loudly on a nonexistent path instead of inheriting bare vale's fallback
  to stdin, which rendered a typo'd path as `0 errors ... in stdin`, exit 0 —
  a form the callers' `0 files` NOT-RUN guard cannot match.
- Follow symlinks when walking a directory argument, matching bare vale.

Refs: #85
2026-08-09 15:44:04 +00:00
16c038b178 fix(lint): guard empty array expansions in vale-wrap.sh for bash 3.2
Under set -u, "${arr[@]}" on an empty array aborts on bash before 4.4,
which is what macOS ships as /bin/bash. Three expansion sites now use
${arr[@]+"${arr[@]}"} consistently.

The hazard is not currently reachable: verified on a bash 3.2.57 built
from source that all seven invocation shapes succeed against the
previous code, including zero args, flags-only and an empty directory.
vale_args is provably non-empty at every site because the default
--config branch always appends first. The guard is kept because that
invariant is non-local and untested, so an edit to the default-config
branch would reintroduce a macOS-only crash silently.

Test fidelity is deliberately mixed. Case 16 is static and is the only
one that fails against the previous code, since no bash 5 host can
reproduce the abort at runtime. Case 17 runs the emptiest invocations
under the oldest bash it can find and names that shell in its output
so it cannot overclaim. Case 18 guards against the tempting wrong fix
of dropping the quotes, which also silences the abort but word-splits
a path containing a space.

No other bash 4.x construct is present; swept for mapfile, declare -A,
case modification, negative indices, globstar, wait -n and namerefs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MCQ648fLSFXPHGZdQ8gn58
2026-08-09 13:32:30 +00:00
8c570e9659 fix(lint): make the Vale prefilter work for external consumers
pre-commit prefixes only entry[0] with the hook-repo clone path
(cmd = (prefix.path(cmd[0]), *cmd[1:])), so the --config argument in
.pre-commit-hooks.yaml resolved against the *consuming* repo's root
and hard-failed every external run with E100. Two of the three hooks
ADR-0014 promises were unusable.

vale-wrap.sh now self-locates its config from BASH_SOURCE when no
--config is supplied; an explicit --config still wins in all three
argv forms and stays cwd-relative. Both manifests drop the argument
and are kept byte-identical: the local repo: local config resolved
--config correctly only because the consuming repo *was* this repo,
and that divergence is why three review rounds missed the defect.

Also in the wrapper:
- replace GNU-only `realpath -m` with a portable abspath helper; -m is
  load-bearing (dest does not exist yet), so BSD realpath aborted the
  script under set -e on macOS
- walk directory arguments instead of passing them through unflattened,
  which reported a clean 0-error run for files that fail when named
  explicitly
- read/write with errors='surrogateescape' so one non-UTF-8 .md under a
  directory argument cannot abort the hook

New test-vale-hooks-consumer.sh builds the hook repo from the working
tree and points a file:// consumer at it, covering the manifest as a
hook repo for the first time.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MCQ648fLSFXPHGZdQ8gn58
2026-08-09 13:06:23 +00:00
1164f3abad fix(lint): make Vale prefilter portable via the plugin
skill-audit/agent-audit's Step 1 resolved vale-wrap.sh/.vale.ini via
`git rev-parse --show-toplevel`, which returns whichever repo the skill
happens to run in. Inside ai-development that works; in any external repo
that installs kyberforge@holocron as a plugin, it resolves to that repo's
own root, which has no .vale.ini — the prefilter silently fell back to
full LLM judgment. ADR-0013 named this as a deliberately deferred gap.

Vale's config/styles/wrapper now ship inside the plugin itself: a
canonical copy in agent-audit/assets/vale/ (Kyberforge + KyberforgeCopilot,
the superset agent-audit needs) and a smaller duplicate in
skill-audit/assets/vale/ (Kyberforge only) — per the no-cross-skill-path
rule already established for plugin cache-installs. Both skills resolve
these relative to their own directory, same as scripts/validate.sh
already does.

A new root .pre-commit-hooks.yaml exposes both copies plus
skill-size-check so any external repo can enforce the same rules via
`repo: <this-repo-url>, rev: <tag>` in its own pre-commit config,
independent of Claude Code entirely — the same mechanism covers CI. This
repo's own pre-commit hook now consumes the identical plugin-bundled
copies via repo: local (not a third root copy, and not a pinned
self-reference, which would lint working-tree edits against the last
tagged release instead of the change being made). Split into
vale-audit-prefilter-skill/-agent hooks after confirming, by diffing the
full corpus against both old and new config before deleting the old
files, that one combined hook pointed at only one copy silently 0-file-
skips the other file type.

scripts/check-vale-style-sync.sh guards the two copies against drift,
wired at pre-push alongside check-manifests.

ADR: 0014
2026-08-09 10:04:19 +00:00
642e4fd142 fix(kyberforge): broaden audit self-triggers, plug factory gaps
skill-audit/agent-audit now proactively trigger after a skill/agent
file is hand-edited outside skill-author/agent-author, not just on
explicit request — closing a gap from this session where a fork's
direct edits to agent-author/agent-audit shipped without their own
inline audit until forge was invoked to check afterward.

Also: plugin-author gains a gotcha on claude plugin validate --strict
auto-discovering every .md under agents/ regardless of manifest
declarations (ADR-0010); agent-audit's dimension count and
validate-provenance.sh's --help now match actual behavior.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-05 09:34:59 +00:00
996d9be428 fix(agents): relocate provenance sources.md outside agents/ dir
`claude plugin validate --strict` auto-discovers every .md under a
plugin's agents/ directory as an agent requiring frontmatter, so the
provenance file there always needs fake agent frontmatter to pass
validation. Confirmed empirically that an explicit `agents` manifest
array can't suppress this discovery. Move the file to <plugin-root>/
sources.md instead, and update agent-author/agent-audit accordingly.

Adds ADR-0010, partially superseding ADR-0005's `agents/sources.md`
convention. Fixes #63.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-05 09:01:06 +00:00
e58234eaf9 fix(kyberforge): address agent-audit audit findings in validate-provenance.sh
## Why

Two issues were found in `validate-provenance.sh` by `/skill-audit`:

1. `find_plugin_root` only walked up looking for bare `plugin.json`, missing
   the `.claude-plugin/plugin.json` layout used by kyberforge plugins — matching
   the logic already present in `validate.sh`.

2. Check numbers in `--help` and inline comments had a gap (0,1,2,4,5,6) from
   a previously removed check, making the numbering confusing to readers.

## Implementation Notes

Check numbers renumbered sequentially 0–5 in both the `--help` output block
and the inline `# --- Check N:` comments. The `find_plugin_root` condition now
mirrors the `detect_scope` function in `validate.sh`.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0147vXtL5sP6vorDdqXGJJU9
2026-07-04 11:33:03 +00:00
8463c87dfc feat(kyberforge): improve agent-audit skill with platform-accurate checks and description quality reference
- Fix validate.sh: remove incorrect name==stem check for CC files (CC docs say filename need not match name field); keep check for Copilot CLI only
- Fix validate.sh: plugin scope detection now checks both plugin.json and .claude-plugin/plugin.json
- Fix validate.sh: Copilot cloud/IDE agents (.github/copilot/agents/) have name as optional; path-based guard added
- Add validate.sh checks: Copilot body length >30,000 chars (SUGGESTION), Copilot-only fields in CC files (FAIL), subagent-unavailable tools in tools field (SUGGESTION)
- Add references/description-quality.md as conditional escape hatch for borderline description findings
- SKILL.md: name five audit dimensions in description; sharpen indirect-trigger phrasing
- SKILL.md: label pair-mandate as kyberforge project convention, not platform requirement
- SKILL.md: scope redundant name-match/body-empty checks to manual fallback only
- SKILL.md: add conditional reference to description-quality.md; update provider-safety description for new check categories; fix plugin scope gotcha to mention .claude-plugin/plugin.json
- SKILL.md: add INFO tier to result block template
- Add source_keys frontmatter to references/README.md; update sources.md to add description-quality.md to contributing files

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0147vXtL5sP6vorDdqXGJJU9
2026-07-04 11:10:32 +00:00
1333d2c1b1 feat(kyberforge): add agent-audit provenance chain validation (closes #60)
## Why

agent-author produces agents/sources.md at plugin scope to record which
research sources informed which agent files. agent-audit had no way to
validate this chain, leaving stale or missing provenance undetected.

## Implementation Notes

Validation is per-pair (the given agent file + its counterpart) rather
than plugin-wide, keeping the scope consistent with validate.sh. The
script exits 0 silently for non-plugin-scope agents.

source_keys is top-level in both CC .md and Copilot .agent.md files
(not under metadata:) to avoid conflict with Copilot's own metadata
field semantics. Checks 0, 1, 2, 4, 5, 6 mirror the skill provenance
set; upstream research-doc cross-reference checks (7, 8) are deferred.

agent-author Steps 2, 3, and 4 updated to formally specify the
agents/sources.md format and instruct authors to add source_keys to
both files when research sources are in context.

Refs: #60

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0147vXtL5sP6vorDdqXGJJU9
2026-07-04 10:37:44 +00:00
92c13b997f feat(kyberforge): add agent-audit skill (closes #11)
## Why

agent-author produces paired agent definition files (Claude Code .md +
Copilot .agent.md) but had no companion audit skill to validate them.
agent-audit fills that gap, giving the same structured PASS/FAIL report
that skill-audit provides for SKILL.md files.

## Implementation Notes

- validate.sh uses scope detection (walk up for plugin.json / .git) to
  locate the counterpart file and determine whether plugin-silently-ignored
  fields (hooks, mcpServers, permissionMode) should be flagged
- CC-only and silently-ignored field lists are read from
  references/field-inventory.md at runtime rather than hardcoded —
  provenance back to the research corpus; see ADR-0019
- Single-file invocation (pass either file, counterpart derived) chosen
  over directory or name+root — see ADR-0018
- 12 bats tests cover provider detection, scope detection, all FAIL paths,
  and clean-pair pass

## Impact

- kyberforge bumped to v1.1.2
- agent-author close step should be updated to reference agent-audit (#11)
- Provenance/sources chain check deferred to #60

ADR: docs/adr/0018-agent-audit-single-file-invocation.md
ADR: docs/adr/0019-agent-audit-field-inventory-reference.md
Refs: #11
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0147vXtL5sP6vorDdqXGJJU9
2026-07-04 10:06:37 +00:00