Components
The gaffer and the loop
The gaffer is the parent agent for one factory, and its loop is the beat. Each beat reads the RFCs you approved, breaks them into tasks, dispatches a worker per task into its own tmux session, checks what came back, and reports what needs you.
The loop runs the way a Kubernetes controller does: watching is code, deciding
is the model. Every five minutes a deterministic sensor
(scripts/factory-sense.sh)
reads the world — the plans branch against the watermark, the inbox, the
floor, the board — for the price of a git fetch and a few API calls. Only
when something observably moved does a beat run, handed the reasons it fired;
a quiet tick costs zero model invocations. A resync backstop runs a full beat
hourly regardless, so anything the sensor cannot see costs latency, never
correctness. Five minutes is therefore the wait between approving something
and hearing about it, and an idle factory is genuinely idle rather than a
model being paid to notice nothing happened.
Its instructions are contracts/factory-loop.md: one iteration
written out in prose, a file you read and edit. Changing how a factory operates
is a commit, not a setting.
Every beat reads the world fresh, so nothing carries forward to be wrong and
any worker can be killed and replaced mid-task. There is no step four of seven
to be stuck on, only a gap that is either closed or not. Knowledge is the one
exception to that freshness: every worker reads docs/learnings/ in the repo
it is about to touch before it starts, and leaves at most one behind when it
finishes (contracts/learnings.md). A factory whose
four-hundredth worker is as ignorant as its first is forgetful.
Work that crosses repos is the case this shape is for. The gaffer holds one
RFC across every repo in its scope, opens the pull requests in an order that
accounts for what depends on what, and keeps a per-repo concurrency lane so two
agents never collide in the same tree. Each dispatch posts a line reading
▶ acme dispatched: … to the Slack channel you named at setup, if you named
one, so the RFC you approved answers back on its own. The first beat after a
fresh boot reports what it found and dispatches nothing, so the beat after it
is the first that builds anything.
How a beat runs is a per-factory setting, and runtime = "one-shot" is the
recommended one: the controller above, each beat a claude -p process that
runs an iteration and exits, so liveness is an exit code and a timestamp
instead of a guess about what a pane is doing. Between beats there is no
gaffer process at all. runtime = "resident" is the rollback — a claude
session kept alive in tmux that schedules its own next beat, a model
invocation every five minutes whether or not anything moved.