A Claude Code workflow that drives a build from a ledger of machine-checkable quality gates. A PM agent picks the next gate, spawns one lane-owner teammate per tick, re-runs the gate's verification, commits on pass, and loops until every gate passes. Multiple teams/branches can build the same ledger in parallel, and the whole thing can run headlessly.
You describe the work as a ledger of quality gates in .gatesmith/gates.yaml. The
PM agent picks the next unblocked gate, spawns the one teammate that owns it,
re-runs the gate's verification command, enforces a lane fence, commits on pass,
and journals the verdict. A self-referential loop (vendored into the kit) fires the
PM tick repeatedly until every gate passes.
- Claude Code
gitjq(used by the vendored loop's Stop hook)snapdir(optional — only for git-free "snapdir mode";cargo install snapdir-cli)
The loop is built in — no external ralph-loop plugin needed.
-
Copy the kit into the repo you want to build:
git clone https://github.com/bermi/gatesmith ~/code/gatesmith # once cd ~/your-project ~/code/gatesmith/install.sh # drops .gatesmith/ + .claude/
-
Generate your gate ledger and lane templates: open the project in Claude Code and paste the filled-in kick-off prompt from SETUP_PROMPT.md — it has a template plus worked examples for an API, a CLI, and a data pipeline.
-
Run the loop (below).
Single-stream (no owners):
/gatesmith:loop # start the build loop in this session
/gatesmith:cancel _default # stop (human owns end-of-project)
Multi-team / multi-branch — one loop per owner, on separate worktrees, terminals, or machines:
# worktree / machine A
/gatesmith:loop team-a
# worktree / machine B
/gatesmith:loop team-b
Or drive several owners from a single session with the conductor:
/gatesmith:conduct # spawns one tick per owner with work, reconciles the ledger
Each tick is one /gatesmith [<owner>] [flags] invocation. The loop is a Stop hook
(.claude/gatesmith/stop-hook.sh) that re-feeds the owner tick until that owner's
gates are all green (===== OWNER COMPLETE: <owner> =====). Watch progress in
.gatesmith/journal.md and .gatesmith/state.md.
A gate may carry an optional owner (team/branch), distinct from owner_agent
(the lane). A /gatesmith <owner> tick only picks and mutates gates whose owner
matches its scope, but depends_on always resolves against the whole ledger —
so team-b's gate can depend on team-a's and only becomes pickable once that gate
is passed. The single gates.yaml is the shared source of truth: each owner writes
only its own rows, under a short-lived .gatesmith/locks/ledger.lock, via
read-modify-write. The conductor (/gatesmith:conduct) is the cross-machine
reconciler. Per-owner loop locks (.gatesmith/locks/<owner>.lock, TTL/heartbeat
liveness) stop two processes from looping the same owner.
Add --remote-control to any loop or tick to disable AskUserQuestion. Every human
question is written to .gatesmith/questions/<uuid>.md (with all options) and the PM
polls .gatesmith/answers/<uuid>.md each tick; the uuid is recorded on the gate and
in the journal. A gate awaiting an answer is skipped (it doesn't block other work).
Both dirs are gitignored. Drop an answer file (choice: + optional directive: pass|fail|retry|supersede) to unblock it.
--tick-cmd ./path runs a repo-relative command before (GATESMITH_TICK_PHASE=pre)
and after (=post) each tick. The path must start with ./, contain no .., and be
a tracked/executable file in the repo. Allowlist your command in .claude/settings.json
or unattended ticks stall on a permission prompt.
Tell the PM (interactively, or with a directive: supersede answer in remote mode)
to supersede a gate: it marks the old gate superseded, appends your replacement gate
as pending, and repoints any dependents to the new id — letting you move on without
losing the audit trail.
For builds where git isn't available or wanted, run with --snapdir-store file:///abs/store (or set SNAPDIR_STORE) to use snapdir — BLAKE3
content-addressed directory snapshots — as the state/sync backend instead of git:
/gatesmith:loop team-a --snapdir-store file:///abs/store
/gatesmith:conduct --snapdir-store file:///abs/store
In snapdir mode: the lane fence is a snapdir manifest diff (not git diff); RECORD
pushes a snapshot and records snapdir_id on the gate instead of committing a
git_sha; the ledger travels inside the snapshot, so state syncs without git; and the
conductor is the sole canonical pusher (cross-machine peers re-sync by pulling the
canonical id from the shared store). Pass --snapdir-id <id> to materialize a prior
snapshot at session start. Needs the snapdir binary (cargo install snapdir-cli, or
set SNAPDIR_BIN=/abs/path). A bundled snapdir skill (.claude/skills/snapdir/)
also teaches the model to inspect any snapshot id and to checkpoint/revert its own
state. Note: a snapshot captures the whole working dir — run from a dedicated project
dir, not $HOME.
Every /gatesmith tick runs this contract (full text in .gatesmith/PM_PROMPT.md):
- READ STATE — load
gates.yaml,state.md, recentjournal.md, git status; re-verify any frozen-interface SHA locks. - PICK NEXT GATE —
pending|failedgates whose deps are allpassed, sorted by (phase asc, failure_count desc, id asc). Head wins. - CHECK ESCALATION —
failure_count >= 3,human_checkpoint: true, or a frozen-interface change proposal → ask the human viaAskUserQuestionand exit. - SPAWN TEAMMATE — exactly one, from
.gatesmith/templates/<owner_agent>.md, with the gate's fields substituted in. - VERIFY — lane fence (
git diff --stat— every changed path must be in the teammate's lane), re-runverification_cmd, applypass_criteria, capture evidence. - RECORD — append to
journal.mdfirst, then mutategates.yaml, re-projectstate.md, and commit if the gate passed and the diff is in-lane. - EXIT — print a tick summary and the next likely gate.
The PM never writes production code and spawns at most one teammate per tick. That single-writer rule plus the lane fence is what keeps an unattended build from corrupting itself.
.gatesmith/gates.yaml is the single source of truth. Each gate:
| field | meaning |
|---|---|
id |
unique kebab-case identifier |
phase |
integer; lower phases run first |
owner_agent |
the lane that owns it → .gatesmith/templates/<owner_agent>.md |
owner |
optional team/branch that owns it (drives loop scoping); distinct from owner_agent |
depends_on |
gate ids that must be passed first (resolved across the whole ledger) |
status |
pending | failed | passed | superseded (PM mutates) |
failure_count |
retries; >= 3 escalates to the human |
verification_cmd |
shell command the PM re-runs from the repo root |
pass_criteria |
the DSL applied to the result (below) |
human_checkpoint |
true → PM asks the human before passing |
description |
one-line human summary |
Use any one, or combine with and::
exit_code: 0
file_exists: path/to/file
files_exist: [a, b, c]
regex_match: "pattern" # against stdout
json_path: ".metrics.score" # with:
op: ">=" # == != < <= > >=
value: 20
human_confirm: "question for the human"
and: [ {exit_code: 0}, {file_exists: foo} ]A gate that has never been shown to fail is not evidence. pass_criteria: exit_code: 0 tells you a
command succeeded; it tells you nothing about whether that command could have failed. An
instrument that has silently stopped measuring is indistinguishable from a clean result.
So a gate may declare sabotage controls: concrete mutations to the real source that must turn it
red. /gatesmith:sabotage runs them.
.claude/gatesmith/sabotage.sh --list # what is declared
.claude/gatesmith/sabotage.sh # run them all
/gatesmith:sabotage --audit # author the missing onesEach control is rsynced to a scratch copy, patched, built and run there, then deleted — the
working tree is never mutated. Four things are asserted, and they are deliberately separate:
- The patch landed — a unique marker counted with
grep -cin the file the build consumed. Neverdiff: an untracked new file produces no diff at all. - Something moved — the gate's own metrics, or its stdout, must differ from the clean run.
This is what catches an arithmetically inert mutation:
max(1.0, x)wherex >= 1applies, builds, counts its marker, and changes nothing. A marker count cannot see that. - It went red.
- For the right reason — the expected predicate must be in
failures(sabotaged)and absent fromfailures(clean). A gate already red for that reason cannot borrow its own defect as proof of sensitivity.
The statuses are the point. RED is the pass; INERT_MUTATION, RED_FOR_THE_WRONG_REASON,
PREDICATE_ALREADY_RED_WHEN_CLEAN, SABOTAGE_DID_NOT_APPLY and NO_CONTROL_DECLARED are five
distinct ways a control can be worthless while looking like one from a distance.
It works on a ledger that has never heard of any of this. With no evidence envelope the
differential falls back to the exit code and records strength: "exit-code-only" — weaker, but it
still catches the biggest failure mode, a gate that cannot fail at all. Gates that emit
.gatesmith/evidence/<id>.json with named failures[] get the full check. See
.gatesmith/controls/README.md for the envelope and the control format.
Start by auditing what you already have. /gatesmith:sabotage --audit walks the gates that
declare no control and asks the one useful question: can you make this fail on demand? The ones you
cannot are the real findings.
A gate exits 0 (PASS), 1 (FAIL) or 3 (UNTRACED — the instrument could not be shown to be
working). UNTRACED is never counted as a pass and never as a failure: "the instrument was broken"
and "the subject was fine" are the same reading, and a gate that cannot see must not report a
colour.
- Never weaken a threshold to go green. To change any criterion, first demonstrate that the
reference or baseline fails the same predicate, and record that demonstration — with the
measurement — in
.gatesmith/CORRECTIONS.md. - Whoever implements does not verify. A lane may mutate its gate's
status, not itspass_criteria. - When a control cannot be written as declared, say why in place. A deleted declaration is an erased hole.
- When a control refuses to go red, suspect the instrument before the code. A gate can be structurally blind to its own subject — integrating a region no part of the defect falls inside, so the most drastic version of that defect leaves every metric identical.
Rules an agent can edit around are suggestions. These are the mechanical halves.
.claude/gatesmith/ledger-fence.sh # a lane may move a gate's STATUS, never its BAR
.claude/gatesmith/baseline.sh --verify # the thing a gate compares against is pinned
.claude/gatesmith/fresh-checkout.sh # the suite must pass for someone who is not youledger-fence.sh — "never weaken a threshold" is reachable in Gatesmith: a /gatesmith <owner> tick may mutate the gates it owns, so the agent with the strongest incentive to move a bar
holds the pen. The fence blocks changes to verification_cmd, pass_criteria, thresholds and
controls; status, failure_count and failure_reason stay free, because that is the loop's own
bookkeeping. The escape hatch is evidence, not a flag: an entry in CORRECTIONS.md naming the gate.
baseline.sh — if the discipline says "demonstrate the reference fails the same predicate",
then the reference is load-bearing, and a load-bearing input nobody pinned will drift. The digest
covers the whole declared set, so a file left behind by an older sync changes the baseline's
identity even if nothing reads it. --pin refuses without a reason and appends what moved to
CORRECTIONS.md — re-pinning to make a run go through is the one thing the lock exists to prevent.
fresh-checkout.sh — a working-tree run can be green because the tree hands it a file the
repository does not have. The predicate is git ls-files --others --exclude-standard:
untracked and not ignored. An ignored input is a decision; one that is neither is an oversight.
--run "CMD" clones HEAD to a temp dir and runs the suite there, which is the only way to catch
this class at all — it is not the concurrency problem, and owning the tree does not help.
The ledger seeds three meta-gates for these: sabotage_controls_all_red, ledger_bar_not_moved
and inputs_are_tracked.
Teammates may edit only their own lane directory. Gatesmith enforces this after
the fact with git diff --stat, not with harness file-permission denies —
because spawned teammates inherit the same settings.json and hard denies would
block them from editing their own lane. If a teammate's diff strays out of lane,
the PM marks the gate failed and refuses to commit, leaving the diff for a human
to inspect.
template/ # copied into your project by install.sh
├── .gatesmith/
│ ├── PM_PROMPT.md # the authoritative PM contract (fill in {{LANES}} etc.)
│ ├── gates.yaml # the gate ledger (replace seed gates)
│ ├── state.md # derived snapshot (PM re-projects each tick)
│ ├── journal.md # append-only audit log
│ ├── .gitignore # ignores loops/ locks/ questions/ answers/
│ ├── evidence/ # captured verification output per tick
│ │ └── _sabotage/ # per-control results + _matrix.json summary
│ ├── controls/ # sabotage controls: one JSON per control
│ ├── CORRECTIONS.md # every change to what a gate MEANS, with its measurement
│ ├── baseline.lock.json # (optional) the pinned reference a gate compares against
│ ├── handoff/ # teammate handoff files
│ ├── loops/ locks/ # runtime: loop state + per-owner/ledger locks (gitignored)
│ ├── questions/ answers/ # runtime: remote-control Q&A handoff (gitignored)
│ ├── work/ # runtime: snapdir-mode per-owner checkouts (gitignored)
│ └── templates/_lane.md # generic lane template — copy per lane
└── .claude/
├── commands/
│ ├── gatesmith.md # the /gatesmith tick command
│ └── gatesmith/{loop,cancel,conduct,sabotage}.md # /gatesmith:loop|cancel|conduct|sabotage
├── gatesmith/ # vendored loop: stop-hook.sh, setup-loop.sh, cancel-loop.sh, lib.sh
│ # plus sabotage.sh, ledger-fence.sh, baseline.sh,
│ # fresh-checkout.sh
├── skills/snapdir/SKILL.md # snapdir inspect / checkpoint-revert guide
└── settings.json # Stop hook + allowlist (add your build/test commands)
SETUP_PROMPT.md # one-shot kick-off prompt to generate your ledger
install.sh # copies the template into $PWD, chmods scripts, merges the Stop hook
MIT