10 Commits

Author SHA1 Message Date
ea119d83b0 fix(gates): close six PR #135 review findings in gates and their tests
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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NwD8Egs5r4ndqeFLmhusX2
2026-09-19 21:08:10 +00:00
7380bed6da fix(tests): make runner worktree exclusions relative to the search root
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) <noreply@anthropic.com>
2026-09-16 15:27:25 +00:00
ffcbed6c41 fix(tests): replace pipefail-racy echo | grep -q with here-strings
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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YR2CjVumUbEGWcMikcoXBD
2026-09-16 09:14:01 +00:00
718c79af70 chore: drop the flat content mirror and native install support (ADR-0024)
apm becomes the only supported install path. The flat mirror at each plugin
root existed solely so Claude Code's native `claude plugin install` could
convention-scan plugin content (ADR-0017). With no native consumers, it cost
~20,000 tracked lines plus ~2,100 lines of sync tooling and ~88s of every
push to guard content apm never reads — and its only automated gate,
`claude plugin validate --strict`, passes on a plugin with zero content, so
it could not detect the defect ADR-0017 was created to fix.

Removes the mirror (213 files), the six per-plugin manifest pairs,
sync-plugin-content.sh, its 1,289-line test, the orphaned
marketplace-plugins.sh, and the check-plugin-content-sync and
validate-plugins pre-push hooks. The root `marketplace:` block and
.claude-plugin/ catalogue stay: apm's own marketplace consumers read that
same file, so `<name>@holocron` short names keep working.

tests/run-bats.sh now excludes .claude/skills/. apm installs from .apm/,
which carries the tests/ dirs the mirror stripped, so deployed .bats files
would otherwise be discovered and double-run.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YR2CjVumUbEGWcMikcoXBD
2026-09-14 16:59:42 +00:00
d42f6368fe fix(docs): correct claims the first fix round asserted without measuring
Why: two blind verifiers re-ran the five preceding commits and found four
defects of the same class this branch exists to close -- a confidently stated
measured claim that does not survive re-measurement -- this time inside the
fixes themselves.

- AGENTS.md:41 still carried both phrasings c68e864 reports having corrected.
  `grep -rn repo-defined` returned exactly one hit repo-wide: that line, in the
  file every session preloads. 4d336bb edited the line directly above it.
- ADR-0021 asserted twice, in the section justifying that no gate is added,
  that the ADR-0020 validators "never open an apm.yml". All three open and
  yaml.safe_load it (skill-size-check.sh:342, both validate.sh). The conclusion
  survives -- none reads the description: key, and their globs are SKILL.md and
  *.agent.md only -- but the stated mechanism is falsified by one grep.
- architecture.md said the ADR directory holds 20 numbered ADRs; c7ba3d2 made
  it 21, and c68e864 audited that file for exactly this class of stale count.
  The number is dropped rather than corrected: `ls docs/adr/` is already the
  index, so a count in prose is a second thing to maintain.
- gates.md's new three-verdict table said `-> name` promotes an unresolved
  target to ERROR. Reproduced with fixtures: NAME_HYPH (skill-size-check.sh:543)
  requires a hyphen, so `-> gitea-prs` is checked and `-> triage` is not
  extracted at all, and the unicode arrow is never recognised. The SUGGESTION
  text advises that spelling, so taking its advice can silence the finding. The
  gap is now documented as a defect; nothing covers it, since the one arrow case
  in test-adr0020-targets.sh happens to use a hyphenated target.

Implementation notes:
- AGENTS.md:48's coverage claim is shrunk rather than chased. Restoring six
  glossary entries did not make it true: 12 more sampled terms are undefined,
  three of them (trigger/capability/boundary clause) used inside CONTEXT.md
  itself. It now says CONTEXT.md is the glossary and is not exhaustive.
- CONTEXT.md's output profile and near-miss entries are corrected against their
  sources. The first stated a false exclusion -- .github/plugin/plugin.json IS
  apm-generated; only the marketplace mirror has no profile. The second
  inverted its source's referent: description-quality.md defines a near-miss as
  a query, not a sibling skill.
- The strict-mode message named jq, which no suite guards on (`command -v jq`
  appears nowhere in tests/), while omitting python3/PyYAML, which three do.
- README's git and gitea bullets now name git-workflow and gitea-workflow.
  ADR-0021 leaves README the only inventory and architecture.md now points at
  it, so the two bullets that were short had to be completed.
- ADR-0018's 2026-08-14 correction is marked superseded in place. It asserted
  machine state in the present tense that its own 2026-08-17 note retracts.
- ADR-0021's remaining errors: six files -> four (measured from de84d1b), the
  wiki description's length 114 -> 96 chars, the codex self-contradiction, the
  cost argument overstating bumps already owed for any skill addition, and two
  claims about files this branch went on to edit.
- The "15 of 17 suites" figure is restored where I had removed it: it is a dated
  record of one incident, not a live count, and four sites now describe it the
  same way.

Impact: 16/16 pre-push hooks pass, suite 24 passed 0 skipped 0 failed. No
behaviour change; every edit is prose or a comment.

Refs: #105

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TmFqzpuExLJv3m114XVE9w
2026-08-17 13:18:55 +00:00
36596598ef fix(tests): point the strict-mode dependency citation at README.md
Why: this branch moved the prerequisites list out of AGENTS.md into README.md
but left three references behind. The worst is run-tests.sh's --strict failure
message, which a developer is handed at the exact moment a push gate fails and
they need the dependency list: it named AGENTS.md, which no longer documents
vale, apm or jq anywhere. 56cc173 re-anchored this same class of citation in
.pre-commit-config.yaml, scripts/check-vale-style-sync.sh,
scripts/sync-marketplace-mirror.sh and tests/test-check-vale-style-sync.sh --
tests/run-tests.sh was missed, and it is the only one a human actually reads.

Implementation notes:
- run-tests.sh:262 (user-visible), run-tests.sh:16 (header rationale) and
  test-run-tests.sh:395 all now cite README.md's Prerequisites table, verified
  to name vale, apm, jq, python3/PyYAML and the claude CLI.
- Every assertion on this message greps for "a skip is a SETUP ERROR", which
  sits before the changed clause, so no test needed updating.
- test-vale-wrap.sh's bash 3.2 citation is corrected in the same pass: it
  credited LESSONS.md and the script headers, but LESSONS.md's only mention of
  3.2 is an aside inside an unrelated lesson, not a target declaration. It now
  cites scripts/lib/batch-run.sh and providers/claude-code/statusline-command.sh,
  which do declare it. The citation 56cc173 replaced was already false, so this
  finishes a half-fix rather than correcting a regression.

Impact: forced the strict-failure path with vale removed from PATH and confirmed
the corrected message printing -- 21 passed, 3 skipped, exit 1. Full suite with
vale present: 24 passed, 0 skipped, 0 failed.

Refs: #105

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TmFqzpuExLJv3m114XVE9w
2026-08-17 12:28:11 +00:00
d02765d595 fix(ci): close the RUN_TESTS_STRICT leak at its source, not at each caller
7607522 fixed the symptom in the wrong place. It made `test-run-tests.sh`'s
`run_fake()` spawn fixtures via `env -u RUN_TESTS_STRICT`, which stops that one
suite inheriting strictness — and leaves every future suite to defend itself the
same way. The variable's only job is done the moment `run-tests.sh` latches it
into the `STRICT` shell local, so it is unset there now and the leak is gone for
every child. The `env -u` stays as this suite's own defence in depth rather than
as the fix.

Two corrections to that commit's account of the bug, both overstated and both
cheap to have checked:

- The blast radius was two assertions, cases 10c and 10g, not six. Nothing else
  in the repo reads `RUN_TESTS_STRICT`.
- The pre-push gate was never red. It invokes `bash tests/run-tests.sh --strict`,
  and the flag sets a shell local that is never exported, so the flag spelling
  never leaked at all. Only the env-var spelling did.

That asymmetry between the two documented spellings is the real finding, and
nothing asserted against it. Case 10b compared the parent's verdict, which is the
half that already matched; the halves that differed were the environments the two
spellings handed every dispatched suite. New case 10i asks a child directly —
`${VAR+set}`, so an exported empty value still counts as a leak — and asserts the
two observations equal each other rather than a hardcoded expectation, so they
cannot drift apart in a direction the case did not anticipate.
2026-08-16 16:41:08 +00:00
76075223c7 fix(ci): unbreak the pre-push gate — strict-mode leak and apm-owned settings drift
Two pre-existing failures, both red at HEAD before ADR-0020 work began,
both invisible in an ordinary local run.

RUN_TESTS_STRICT leaked from the environment into test-run-tests.sh's
fixture children. The meta-test is itself a suite the runner discovers,
so under the gate's own invocation the variable propagated outer runner
-> batch_run -> the fixture's copy of run-tests.sh, flipping it strict.
Case 10c (a deliberate control asserting a skip is tolerated WITHOUT
strict) then failed. Six further cases were silently running strict too
and asserting against the wrong stream — case 9 was matching the stderr
strict block rather than the stdout skip list it was written to check.
run_fake now spawns via 'env -u RUN_TESTS_STRICT', so fixture strictness
is a property of the case, never of how the file was launched. No
assertion weakened; run-tests.sh itself is untouched.

pretty-format-json --autofix was re-sorting apm's output on the way into
every commit. .claude/settings.json is apm-owned (ADR-0018/0019) and its
exclude list named fifteen generated manifests but not this file, so
since 2e395a4 it has been committed in a key order apm would never write
— permanent drift on a file with an empty git diff. Content was always
byte-identical; only JSON key order differed. The exclude ships in the
same commit as the corrected file because otherwise the hook re-breaks
it during staging.

apm.lock.yaml: generated_at churn, plus lint's exec_status corrected from
'deployed' to 'gated_pending_approval' — executables.allow grants only
kyberforge#1.5.0, so lint's hooks/bin are genuinely gated.

New coverage: an ambient RUN_TESTS_STRICT must not reach a fixture that
did not ask for it, and under --strict the skip report goes to stderr
only with the stdout list suppressed. Neither was pinned.
2026-08-14 21:54:00 +00:00
aa15fc850c test: fail the gate when a suite is skipped or never reports
Three gaps left by the previous round, all the same shape: a gate reporting
success having verified less than it appears to.

run_bats() hard-failed on a missing or non-executable runner but never checked
that the runner produced anything. An empty, executable run-bats.sh exits 0,
and the dispatcher printed a green summary with 166 bats tests silently absent.
It now requires an "N tests, M failures" line with a non-zero count.

run-tests.sh's skip listing is swallowed by pre-commit on a pass, so on a
machine without vale three suites exited 77 and the pre-push gate went green
having run 14 of 17. The hook now invokes it as --strict, where a skip fails
and the error names each suite and the reason it skipped. An ad-hoc local run
still skips gracefully -- at pre-push a skip means a documented dependency is
missing, which is a setup error, not a legitimate state. Deliberately not wired
to the vale downgrade's env var: one flag must not disarm two gates.

BATS_FILE_FLOOR is replaced by an expectation derived from git ls-files. A
floor of 8 against a real count of 10 let two files and eleven tests disappear
green, and the number needed an edit whenever a plugin was added. The derived
set needs no number, and catches an addition as well as a removal -- a .bats
file staged into the index and deleted from disk is now demanded back.

The vale opt-out announced its downgrade to nobody: pre-commit prints nothing
for a passing hook, so the summary line AGENTS.md tells the reader to check was
unreachable in exactly the situation it exists for. The hook is now verbose.

Also corrects the PROBES_CHECKED guard, whose commit message described a state
that cannot occur -- the .vale.ini loop errs first. Its two reachable triggers,
a gutted probe heredoc and a probe row naming a missing directory, had no test;
they do now, each asserting the guard is the sole cause.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X7GvKuJfy2WrdBmUttV4DT
2026-08-14 12:29:42 +00:00
52bbd62286 test: stop the runners reporting green on suites that never ran
run_bats() was an if with no else, so a missing or non-executable run-bats.sh
made all 166 bats tests disappear with no diagnostic while the run printed a
green summary and exited 0. --bats-only became a total no-op. This is the same
defect the PR fixed one level down, left open in the dispatcher that pre-push
actually invokes -- and nothing tested run-tests.sh at all.

run-bats.sh's aggregation was asserted by nothing. Three separate mutations to
its failure-detection line all survived the existing suite, because real bats
emits both a nonzero exit and "not ok" lines, so each signal masked the other.
The new cases produce each signal without the other; all three mutants now die.

Also in this pass:
- zero discovered .bats files exited 0, so a widened path exclusion retired the
  suite silently. Replaced with a file-count floor: a collapse to one or two
  files is the same failure as a collapse to zero
- an existing-but-empty status file counted as a pass, because [[ "" -eq 0 ]]
  is arithmetic-true and the || echo 1 fallback only covered a missing file.
  The repro is deterministic: the stub truncates its own status file, then
  kill -9s its parent so the real exit-code write never happens

Adds tests/test-run-tests.sh, 9 cases pinning the exit 0 / 1 / 77 three-way
split against a single summary line.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X7GvKuJfy2WrdBmUttV4DT
2026-08-14 11:04:22 +00:00