Ship a portable verify.sh merge gate and a verified-change skill that enforces test-first, mutation-checked changes, then dogfood both into the agentics repo so no change here is proposed for merge without local proof.
CI on this account is frequently billing-blocked, so a red check proves nothing and a green one never arrives. This builds a merge gate that runs entirely on the local machine and a skill that refuses to call a change done until that gate is green and the evidence is written down. We will know it worked when bash scripts/verify.sh exits 0 here and the same script, unmodified, correctly skips absent stages in a bare project.
Read and implement all steps in the plan at docs/plans/add-local-merge-gate.md — Prove merge readiness locally, without GitHub Actions. Verify against the plan's Tests, Verification, and Acceptance Criteria before reporting done. If everything passed, mark completion in docs/plans/add-local-merge-gate.md — tick each step's [x] marker and each criterion's - [x], set status: completed — and re-render the HTML from the spec. If any check failed, leave status: in-progress and say which.
More ways to run this plan — goal & workflow prompts, file path
Achieve this goal: Prove merge readiness locally, without GitHub Actions. The plan at docs/plans/add-local-merge-gate.md describes one approach — use it as reference, but optimize for the outcome. Verify against the plan's Tests, Verification, and Acceptance Criteria before reporting done. If everything passed, mark completion in docs/plans/add-local-merge-gate.md — tick each step's [x] marker and each criterion's - [x], set status: completed — and re-render the HTML from the spec. If any check failed, leave status: in-progress and say which.
add-local-merge-gate.html
docs/plans/add-local-merge-gate.html
docs/plans/add-local-merge-gate.md
Context
The story behind this plan — what prompted the work and why it matters now.
GitHub Actions is frequently billing-blocked on this account. A quota-blocked
run fails every job in seconds with no test output, which means red CI is not
evidence of a defect and green CI is not evidence of correctness — it is
evidence of nothing at all. Merge readiness has to be provable on the machine.
The repo already has most of a local gate and does not know it.tests/run-all.sh auto-discovers every tests/**/test-*.sh, test-*.mjs, and*.test.mjs and runs them, so new tests need no wiring. What is missing is a
single entry point that also covers the non-test gates (marketplace validation,
the plugin version guard, README table freshness), and a discipline that makes
running it non-optional.
The second half of the problem is test quality, not test presence. A test
written after the implementation can pass for the wrong reason and nobody
finds out until production. The mutation check closes that: deliberately break
the implementation, confirm the new test goes red, restore. A test that stays
green through a mutation is not a test.
This repo has no package.json, no tsconfig, no ESLint config, and no
Playwright. A gate hardcoding typecheck/lint/unit/e2e would be three no-ops
and one real stage here, so the script auto-detects instead — which is also
what makes one file serve every project rather than needing a generator.
code-testing-agent already ships tdd-loop, whose RED phase depends on the
production code not existing yet. That shape does not fit a change to code
that already works, which is the gap verified-change fills.
Decisions
Choices already settled — read these before re-opening any of them.
- The skill lives in
code-testing-agent, not a new plugin and notgit-agent— the verified-change loop is a testing discipline, and that plugin already ownstdd-loop,tdd-fix, andrunning-tests. verified-changesits besidetdd-looprather than replacing or extending it —tdd-loopis for new features where red is free;verified-changeis for changes to working code where red must be manufactured by mutation.- One auto-detecting
verify.shserves every project, so there is no generator — "generate the gate" is a file copy, and the detection happens at run time. - The plugin skill is the source of truth and agentics also carries a project-local copy at
.claude/skills/verified-change/SKILL.md, because worktrees pin older plugin versions and Desktop loads frozen claude.ai snapshots; a parity test prevents the copy from rotting. - The mutation check restores from a scratchpad copy, never
git stashand nevergit checkout --, so it survives a dirty tree without destroying uncommitted work; restoration is proven withcmp -sagainst that copy — notgit diff --quiet, which ignores untracked files and would therefore pass on an unrestored mutation of a file this plan has not committed yet. scripts/verify.shand the plugin'sassets/verify.share a deliberate byte-identical duplicate, held together by a parity test rather than by generation, for the same reason theSKILL.mdcopy exists: the consuming environments cannot reach the plugin root reliably. Every future gate change is therefore a two-file edit, and the parity test is what makes forgetting the second one loud.verified-changeemits the VERIFICATION markdown and hands it off;git-agent:pr-agentis not modified, so only one plugin version bumps.- Steps are grouped RED/GREEN/VERIFY because the work touches source and
tests/run-all.shexists to run red against. There is no SHIP phase — "apply this to my current branch" asked for the work to land in the tree, not for a commit and PR.
Files that change
Every file this plan touches, and what happens to each one.
tests/fixtures/verify-gate-bare/new project with no toolchain, exercises every SKIP pathtests/fixtures/verify-gate-failing/new project whose unit stage exits 1, exercises fail-fasttests/test-verify-gate.shnew objective test for skip behaviour, ordering, and exit codestests/plugins/test-verified-change-skill.shnew skill structure, budgets, guard, and copy paritykit/plugins/code-testing-agent/skills/verified-change/assets/verify.shnew the portable gate templatekit/plugins/code-testing-agent/skills/verified-change/SKILL.mdnew the six-step verified-change loop- kit/plugins/code-testing-agent/skills/verified-change/references/
mutation-check.mdnew mutation catalogue and safe restore protocolverification-section.mdnew one filled VERIFICATION example
kit/plugins/code-testing-agent/bin/install-verify-gatenew executable wrapper that copies assets/verify.sh into a target repo, since${CLAUDE_PLUGIN_ROOT}in a documented Bash command is unrunnabletests/fixtures/README.mdmodified document the two new fixtures and their deliberate departure from the structure-only fixture policytests/plugins/test-exitplanmode-guard.shmodified register verified-change in the hardcoded guard list- kit/plugins/code-testing-agent/
README.mdmodified document the new skillCHANGELOG.mdmodified 3.6.0 entry
.claude-plugin/marketplace.jsonmodified bump code-testing-agent to 3.6.0README.mdgenerated regenerated Plugin Reference Tablescripts/verify.shnew agentics' dogfood copy of the gate.claude/skills/verified-change/new project-local copy of SKILL.md and both references files, parity-tested as a directoryCLAUDE.mdmodified the merge-gate rule
Steps
The step-by-step work, in order — each step says what to do, why it matters, and how to check it worked.
RED
tests/fixtures/verify-gate-bare/ holding a project with no toolchain at all — a tracked .gitkeep and a one-line README.md, since git does not commit an empty directory and a fresh clone would otherwise fail the objective test — and tests/fixtures/verify-gate-failing/ holding one whose unit stage is a package.json test script that exits 1 with no dependencies to install; name no file in either fixture test-*.sh, test-*.mjs, or *.test.mjs, because tests/run-all.sh discovers with a bare find tests -name 'test-*.sh' that has no tests/fixtures/ exclusion and would run the deliberately-failing fixture as a real suite member
ls -a tests/fixtures/verify-gate-bare tests/fixtures/verify-gate-failing lists both with their tracked placeholder files, find tests/fixtures/verify-gate-bare tests/fixtures/verify-gate-failing \( -name 'test-*.sh' -o -name 'test-*.mjs' -o -name '*.test.mjs' \) prints nothing, bash tests/run-all.sh reports the same failed count as before the fixtures existed, and neither contains a tsconfig.json, an ESLint config, or a Playwright config. Also add both fixtures to tests/fixtures/README.md, noting they deliberately depart from the "fixtures model structure, not working behavior" line in .claude/rules/testing.md because a gate can only be exercised against a runnable project.tests/test-verify-gate.sh asserting that running the gate in the bare fixture prints a SKIP (not configured) line for each of typecheck, lint, unit, and e2e in that printed order, and exits 0; that running it in the failing fixture exits non-zero naming the failed stage; and that no stage after the failed one runs — invoking the gate in every case as (cd "$FIXTURE" && bash "$ROOT/scripts/verify.sh") so detection is exercised against the fixture's working directory rather than the repo root
bash tests/test-verify-gate.sh exits non-zero with a missing-file error naming scripts/verify.sh, which is red for the right reason.tests/plugins/test-verified-change-skill.sh asserting the skill's frontmatter carries name and a description within the 200-char budget whose first sentence is within 80, that the file contains the plan-mode guard line verbatim, that .claude/skills/verified-change/SKILL.md is byte-identical to the plugin copy, that scripts/verify.sh is byte-identical to the plugin's assets/verify.sh, and that CLAUDE.md contains the merge-gate rule; also register verified-change in the hardcoded skill list inside tests/plugins/test-exitplanmode-guard.sh so the guard is enforced by the same repo-wide test that enforces it for every other mutating skill
tests/plugins/test-description-budget.sh already sweeps every shipped SKILL.md, so do not treat them as this test's reason to existbash tests/plugins/test-verified-change-skill.sh exits non-zero listing every absent file, not erroring on a bad path, and bash tests/plugins/test-exitplanmode-guard.sh exits non-zero naming the missing verified-change guard.GREEN
kit/plugins/code-testing-agent/skills/verified-change/assets/verify.sh running typecheck, lint, unit, then e2e in that order, each stage detecting its own tooling and printing either a result or SKIP (not configured), exiting non-zero at the first real failure without running later stages, and additionally running tests/run-all.sh, node scripts/check-plugin-versions.mjs, and node scripts/build-readme-table.mjs --check when it detects a .claude-plugin/marketplace.json. Resolve every detection against $PWD, never against $(dirname "$0") — the repo's own scripts set ROOT from $0 (tests/run-all.sh line 1), and a gate that did the same would, while running inside tests/fixtures/verify-gate-bare/, still find agentics' .claude-plugin/marketplace.json, re-enter tests/run-all.sh, which re-runs tests/test-verify-gate.sh, which re-invokes the gate: unbounded recursion on the objective test. Belt-and-braces, export a VERIFY_GATE_ACTIVE marker on entry and hard-exit non-zero with a named re-entry error if it is already set. Have the lint stage also detect shellcheck (the deliverable is itself shell), run detected tooling by its plain name without prepending node_modules/.bin to PATH, and open the script with set -euo pipefail and quoted expansions throughout
bash tests/test-verify-gate.sh still fails, but now on assertion content rather than on the missing file; (cd tests/fixtures/verify-gate-bare && bash "$PWD/../../../scripts/verify.sh") terminates rather than recursing, and shellcheck scripts/verify.sh reports no errors once the file exists.scripts/verify.sh, mark it executable, and add kit/plugins/code-testing-agent/bin/install-verify-gate — an executable wrapper that copies the plugin's assets/verify.sh into the target repo's scripts/, refusing to overwrite an existing file without --force
${...} expansion before permission rules are consulted, so a documented cp "${CLAUDE_PLUGIN_ROOT}/skills/verified-change/assets/verify.sh" scripts/ can never run for anyone; seven plugins in this marketplace already ship a bin/ wrapper for exactly this reasonbash scripts/verify.sh runs to completion and its output contains at least one SKIP (not configured) line and at least one real stage result; install-verify-gate run from a scratch directory produces a byte-identical scripts/verify.sh there and exits non-zero without --force when one already exists.kit/plugins/code-testing-agent/skills/verified-change/SKILL.md with the six-step loop — write the test, mutation-check it, restore, implement, iterate on scripts/verify.sh without pausing to ask up to a hard bound of 8 attempts, then stop and report the failing assertion, the last diff tried, and what was ruled out rather than reporting success, and browser-verify UI changes at 390px and 1280px in both light and dark themes with an axe run against the real rendered page (never a Storybook iframe), recording measured evidence — computed styles, element boxes, the axe violation count — because a screenshot alone is not evidence — carrying the plan-mode guard verbatim, a When not to use line pointing at tdd-loop for new features, and an allowed-tools list covering every tool it calls
bash tests/plugins/test-verified-change-skill.sh advances past the frontmatter and guard assertions to the parity assertions.references/mutation-check.md giving a mutation catalogue by change type and the safe break-and-restore protocol — copy the file to the scratchpad, mutate in place, run the scoped test, restore from the copy, then prove restoration with cmp -s "$SCRATCH/<file>" "<file>" and hard-stop if it fails, install a shell trap that restores on any exit path including interrupt, and delete the scratchpad copy after a proven restore
git diff --quiet <path> is a vacuous proof for the file this plan mutates, because scripts/verify.sh is new and untracked until it is committed, and git diff ignores untracked files entirely, so an unrestored mutation would report clean; cmp is true whether the file is tracked or not, and leaving working-tree source in the scratchpad after the loop puts it outside both the repo and its .gitignorecmp -s proof, the trap, the scratchpad cleanup, and an explicit STOP path, notes the untracked-file caveat that rules git diff --quiet out as the primary check, and matches the verification-gate pattern in .claude/rules/plugin-patterns.md.references/verification-section.md containing one filled VERIFICATION section — every gate with its real result, the mutation applied with its observed failure output, and the screenshot paths — as a worked example rather than a bracket schema
.claude/rules/plugin-patterns.md requires a filled instance for any skill emitting structured output<placeholder> brackets and shows a real command with a real exit status.SKILL.md and both references/ files — to .claude/skills/verified-change/, and extend the step-3 parity test to cover the references as well as SKILL.md
SKILL.md carries the same relative references/mutation-check.md and references/verification-section.md links, and copying SKILL.md alone leaves both dangling in exactly the environments this copy exists to servediff -r kit/plugins/code-testing-agent/skills/verified-change .claude/skills/verified-change prints nothing, and every references/ path named in the project-local SKILL.md resolves to a file beside it.code-testing-agent from 3.5.2 to 3.6.0 in .claude-plugin/marketplace.json, add the skill to the plugin README and CHANGELOG, and regenerate the root README table with node scripts/build-readme-table.mjs
kit/plugins/<name>/ fails the version guard without a bump, and the root table is generated output that must never be hand-editednode scripts/build-readme-table.mjs --check exits 0 and BASE_REF=main node scripts/check-plugin-versions.mjs reports the bump.CLAUDE.md stating that merging is never proposed until bash scripts/verify.sh is green locally and the PR body's VERIFICATION section is filled in, and that a billing-blocked CI run is not evidence either way
CLAUDE.md is honoured, not enforced; the pre-push hook and the git-agent:merge refusal in Next Steps are what would enforce itbash tests/plugins/test-verified-change-skill.sh exits 0 with every assertion passing.VERIFY
bash tests/run-all.sh
bash scripts/verify.sh in agentics and read its full output
SKIP (not configured) for typecheck, lint, and e2e, and prints real results for the unit stage and the three marketplace stages.git fetch origin && BASE_REF=main node scripts/check-plugin-versions.mjs
origin/main directly, so a stale remote-tracking ref compares against the wrong base and passes a bump that CI will rejectcode-testing-agent 3.5.2 -> 3.6.0.scripts/verify.sh so a failing stage returns 0, confirming tests/test-verify-gate.sh goes red, then restoring from the scratchpad copy under a trap that also restores on interrupt
bash tests/test-verify-gate.sh exits non-zero naming the fail-fast assertion, and after restore cmp -s "$SCRATCH/verify.sh" scripts/verify.sh exits 0 and the test passes again. Do not substitute git diff --quiet scripts/verify.sh here: the file is untracked until this branch is committed, so that check would pass on an unrestored mutation and the dogfood would prove nothing.Tests
The tests that prove the change does what it promises.
SKIP (not configured) for typecheck, lint, unit, and e2e in that order and exits 0, the failing fixture exits non-zero naming the failed stage, no stage after the failure runs, and a gate invoked from inside a fixture terminates instead of re-entering tests/run-all.sh; Run: bash tests/test-verify-gate.shDefinition of done
The plan counts as done when every statement below is true — check each one off as you verify it.
Final check
One last pass to confirm the whole change works end to end.
Run bash scripts/verify.sh from the repo root. It must exit 0, printSKIP (not configured) for typecheck, lint, and Playwright e2e, and print
real pass output for the unit stage (tests/run-all.sh) and the three
marketplace stages. Then run bash tests/run-all.sh directly and confirm the
final line reports 0 failed with tests/test-verify-gate.sh andtests/plugins/test-verified-change-skill.sh both in the PASS list.
Prove the gate is not vacuous. Copy scripts/verify.sh to the scratchpad,
edit the failing-stage branch to return 0 instead of propagating the failure,
and run bash tests/test-verify-gate.sh — it must exit non-zero naming the
fail-fast assertion. Restore from the scratchpad copy and confirmcmp -s "$SCRATCH/verify.sh" scripts/verify.sh exits 0 and the test passes
again. Use cmp, not git diff --quiet: the gate is untracked until this
branch is committed and git diff ignores untracked files, so that check
would report clean over an unrestored mutation. A gate whose test stays green
through the mutation has not been verified; it has only been run.
Prove the gate does not re-enter itself. From the repo root run(cd tests/fixtures/verify-gate-bare && bash "$OLDPWD/scripts/verify.sh") and
confirm it terminates with four SKIP lines rather than picking up agentics'.claude-plugin/marketplace.json and recursing back throughtests/run-all.sh into tests/test-verify-gate.sh.
Revert path if the gate proves wrong: delete scripts/verify.sh, the twotests/fixtures/verify-gate-* directories, tests/test-verify-gate.sh,tests/plugins/test-verified-change-skill.sh, and .claude/skills/verified-change/;
drop the CLAUDE.md rule; revert the marketplace bump to 3.5.2 and
regenerate the README table. Nothing in this plan changes existing runtime
behaviour, so the revert is a delete plus one regeneration.
Finally, confirm the marketplace stays consistent: git fetch origin && exits 0 naming
BASE_REF=main node scripts/check-plugin-versions.mjscode-testing-agent 3.5.2 -> 3.6.0, and node scripts/build-readme-table.mjs exits 0.
--check
Wrapping up
Three gates that must all pass before this plan is marked completed.
Completion Report
- Marketplace stage count
tests/run-all.shis the auto-detected unit stage, so the marketplace block contributes three stages (marketplace validation, version guard, README freshness) rather than re-running the suite a second time as step 4's wording implied; step 13's "unit stage and the three marketplace stages" is what shipped.- shellcheck detection
- gated on a
.shellcheckrc, not on the binary being installed, so the gate does not turn red for everyone the day someone runsbrew install shellcheck; this repo has no.shellcheckrc, so lint reports SKIP. bin/install-verify-gateshape- it is a pure
execwrapper overassets/install-verify-gate.sh, because check 3 oftests/plugins/test-no-shell-expansion.shparses every wrapper for anexec <interp> "$(dirname "$0")/…"line and fails a wrapper that carries logic of its own. - Re-entry marker escape
tests/test-verify-gate.shinvokes the gate throughenv -u VERIFY_GATE_ACTIVE, because that test itself runs under the gate whenever the unit stage istests/run-all.sh; without the escape the marker would refuse the very invocation the test exists to make. It is the one site allowed to clear it, and check 2 asserts the marker still refuses everywhere else.- Project-local copy scope
.claude/skills/verified-change/is a whole-directory copy includingassets/, since the acceptance criterion requiresdiff -ragainst the plugin directory to print nothing; that makes three copies of the gate, all held in lockstep by the parity test.- Version-guard verification method
node scripts/check-plugin-versions.mjscomparesorigin/main...HEAD, which is empty on an uncommitted branch and therefore passes vacuously; the 3.5.2 to 3.6.0 bump was verified by calling the script's exportedfindViolationsagainst the real working-tree change set. The CLI form becomes meaningful once the work is committed.- Silent-failure bug found by the dogfood
- proving the parity test fails on a forked gate showed it exiting non-zero with no message, because
diff | head | sedunderset -ekilled the script before the report. Fixed with|| true; the proof is what caught it.