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} ]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
│ ├── 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}.md # /gatesmith:loop|cancel|conduct
├── gatesmith/ # vendored loop: stop-hook.sh, setup-loop.sh, cancel-loop.sh, lib.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