Chapter 5 — Parallel Operators
Draft status: author draft; human verification pending. Outputs are real transcripts; the parallel work in the listings is performed by genuinely separate working trees on one shared store.
Two minds, one project
Volume two’s fifth chapter asked what happens when two operators share one
estate file, and the engine answered with locks and queues. This chapter asks
the version-control edition — a supervisor dispatches two agents against one
project, or a timer’s maintenance session wakes while an interactive session
works — and the answer git’s design wants is subtly different from the one
operators reach for untaught. The untaught reflexes are both wrong in
instructive ways. Sharing a working tree — two sessions editing one
checkout — recreates the lost-update chaos of volume two’s midden: staged
files interleave, one session’s checkout yanks the branch out from under the
other, and status reports a fiction assembled from both minds. Cloning per
session — a fresh full copy in every scratch directory — is safe and pays
for safety twice over: the whole object store duplicated per operator, and
the operators’ work stranded in separate repositories whose exchange now
requires a network hop or path-remote gymnastics. The instrument built for
exactly this shape sits between: git worktree gives each operator its own
working tree and its own checked-out branch, all backed by one shared
object store — chapter 1’s content-addressed ledger, which never needed
duplicating because it is append-only and hash-addressed, the two properties
that make sharing safe.
mkdir project && cd project
git init -q -b main; git config user.email [email protected]; git config user.name operator
echo base > shared.conf; git add -A; git commit -qm "base config"
git worktree add -q -b task-retries ../op-a
git worktree add -q -b task-logging ../op-b
( cd ../op-a && sed -i "s/base/retries: 8/" shared.conf && git commit -qam "raise retries" )
( cd ../op-b && echo "log_level: debug" >> shared.conf && git commit -qam "enable debug logging" )
git worktree list
echo "--- one object store, three histories:"
git log --all --oneline
/tmp/oailly-gate-6e95k55z/project 0fcb782 [main]
/tmp/oailly-gate-6e95k55z/op-a 0765a35 [task-retries]
/tmp/oailly-gate-6e95k55z/op-b f626d3b [task-logging]
--- one object store, three histories:
0fcb782 base config
f626d3b enable debug logging
0765a35 raise retries
Both parallel operators edited the same file — the classic collision — and
nothing collided, because each holds its own tree and its own branch; the
divergence is not an accident to prevent but the recorded, mergeable state
of two minds mid-work, visible whole from any of the three trees (--all
reaches every branch through the shared store). The supervisor’s dispatch
pattern falls straight out: one repository, one worktree per concurrent
task, each session told its directory and its branch — and volume one’s
scratch discipline supplies the frame it slots into, with worktree add
replacing mktemp -d for exactly the work that must survive and merge.
The economics of the shared store
What the worktree costs is worth one honest measurement, because the per-session-clone reflex survives on vague fears of sharing:
mkdir project && cd project
git init -q -b main; git config user.email [email protected]; git config user.name operator
echo x > f; git add -A; git commit -qm base
git worktree add -q -b t ../wt
echo "main tree git dir: $(git rev-parse --git-dir)"
echo "worktree git-common: $(git -C ../wt rev-parse --git-common-dir)"
echo "worktree size: $(du -s ../wt | cut -f1) KB full clone would carry the whole store"
git worktree remove ../wt && echo "removed cleanly"
main tree git dir: .git
worktree git-common: /tmp/oailly-gate-97nb2bh5/project/.git
worktree size: 8 KB full clone would carry the whole store
removed cleanly
Eight kilobytes: the checked-out file plus pointers, with rev-parse
--git-common-dir showing where the actual store lives — back in the primary
repository, shared. On a real project the arithmetic is decisive: a
repository whose store runs to a gigabyte spawns worktrees at the cost of
the checkout alone, and every object any operator commits is instantly
reachable from every other tree without fetch, push, or copy — the exchange
problem the per-session clone created, dissolved. The store-level operations
consolidate the same way: one fetch refreshes every worktree’s view of the
remotes, one maintenance pass (gc, repack) serves all, and volume two’s
instincts about splitting high-rate state from shared state find nothing to
split — the store’s append-only design already made concurrent object
writes safe, which is why git needed no WAL chapter.
Precision about what is not shared completes the model, because the division is exactly the working-state boundary this chapter opened on. Each worktree privately owns its HEAD (which branch it stands on), its index (chapter 2’s staging transaction — two seats can stage simultaneously without interleaving), and its tree-local metadata; the store shares objects, branches, tags, remotes, and configuration. The consequence operators should hold: anything committed anywhere is instantly everyone’s; anything staged-or-working is one seat’s private draft until it commits — the exact draft/publication line chapter 2 drew inside one operator, now drawn between them, by the tool’s own architecture.
The safety rule the sharing does impose is the single-writer truth in new clothing, enforced by the tool itself:
mkdir project && cd project
git init -q -b main; git config user.email [email protected]; git config user.name operator
echo x > f; git add -A; git commit -qm base
git worktree add -q -b task ../wt-one
git worktree add ../wt-two task 2>&1 | head -2
Preparing worktree (checking out 'task')
fatal: 'task' is already used by worktree at '/tmp/oailly-gate-z7udmi5d/wt-one'
One branch, one working tree — the refusal is git protecting the branch
ref from the two-minds-one-checkout chaos this chapter opened with, and
its reading follows volume two’s BUSY discipline: this is coordination
working, not breaking. The operator’s responses, in order of likelihood:
the second session wanted its own branch anyway (dispatch discipline —
one task, one branch — below); it wanted to read that branch, which
needs no checkout at all (git -C anywhere show task:file, log task —
chapter 3’s queries run against any ref from any tree); or it genuinely
found a stale claim — a dead session’s worktree still registered — which
is the inheritance problem, and the lifecycle section closes it.
Branch hygiene for machine fleets
Parallelism multiplies branches, and branches named by machines rot into
namespace landfill faster than humans manage — fix, fix2, temp,
agent-output-final — unless naming is treated as what it is in this
series: provenance. The register’s convention makes the branch name a
ledger header: lineage/task-slug, with the task slug stable enough to
join against volume two’s registry (session-94/raise-retry-budget), so
that branch --format listings read as a work registry and any branch’s
owner, purpose, and age are one query. The dispatch rule that keeps the
namespace meaningful: one task, one branch, born at dispatch, dead at
integration — branches are workspaces, not archives; the ledger is
the archive (chapter 1’s boundary, applied to refs). And the aging rule
mirrors volume two’s cursor staleness: a standing query for branches
whose last commit predates a threshold (for-each-ref --sort=committerdate
--format with a date cut) feeds the graveyard review — merged branches
deleted on integration by the workflow itself, unmerged stale ones
triaged with chapter 3’s reading tools (what does it hold that main
lacks?) and either salvaged into the ledger or closed with a recorded
reason, volume one’s quarantine discipline for the one kind of state
that never needed a graveyard directory, because deletion of a merged
branch deletes nothing the store does not keep.
The synchronization cadence
Divergence economics got their numbers in the fleet briefing; the policy
they argue for deserves its own statement, because fleets fail here by
default rather than by decision. A task branch drifts from main at
main’s velocity, and the cost of reconciling grows superlinearly — the
conflicts compound, and worse, they arrive at integration time, when
the work is done, the context is cold, and chapter 8’s reviewer is
waiting. The cadence rule inverts the arrival: task branches synchronize
with main early and often — each session’s open ritual includes the
fetch and the drift counts, and a branch more than a briefing-threshold
behind merges main in (or rebases onto it, per chapter 6’s boundary:
rebase while private, merge once shared) before new work, so conflicts
surface one day’s worth at a time, in warm context, resolved by the
mind that just created half of them. For stacked work, modern git
removes the classic tax: rebase --update-refs carries a whole stack
of dependent branches through one rebase, re-pointing each as its base
moves — the instrument that makes chapter 8’s stacked proposals
practical for machine fleets rather than heroic. The judgment call the
cadence rule leaves open is deliberate: a branch hours from integration
may reasonably freeze and reconcile once at the end rather than chase a
busy main commit by commit — cadence is drift management, not drift
phobia, and the briefing counts exist exactly so the choice is made
looking at numbers instead of made by forgetting.
The stash, read suspiciously
One instrument adjacent to this chapter earns a caution rather than a
recommendation. git stash shelves uncommitted changes into an anonymous
holding stack — the interactive human’s “hold my drink” for a quick branch
switch — and everything that makes it convenient for humans makes it
hazardous for fleets. Stashes are unattached to any branch, unnamed by
default, invisible to every chapter 3 query that reads branches, and owned
by nobody the registry can name: state parked outside the ledger, which is
this series’ definition of a midden. The register’s substitute is already
on the shelf: the WIP commit on the task branch (chapter 2’s checkpoint
pattern) parks the same state inside provenance — attached, attributed,
recoverable by the branch’s name, cleaned by the same reshaping pass that
was coming anyway. The operator therefore writes stashes rarely (a
worktree per task removes the branch-switching motive entirely), and reads
inherited ones with the unfinished-business protocol: stash list
--format='%gd %ci %gs' inventories the stack with dates and origin
branches, stash show -p stash@{n} reads each as evidence, and each is
either salvaged into a commit on its proper branch or discarded with a
recorded reason. A repository whose stash stack is deep and old is telling
the briefing something about its operators’ discipline — and volume two’s
staleness pricing applies to every entry in it.
The fleet and its remotes
Worktrees share one store, and the store’s view of the outside world —
its remotes — is therefore fleet-wide state with fleet-wide discipline.
Fetching benefits first: one fetch (scheduled, volume one’s timer
patterns) refreshes origin/* for every worktree at once, and the
register’s operators fetch before framing any decision that depends on
the remote’s state — a bisect frame, a merge, a review — because a stale
remote view is volume two’s meaning-rot in its most actionable form: the
question “am I behind?” (rev-list --count HEAD..origin/main, chapter
3’s arithmetic) is only as fresh as the last fetch, and the counts
belong in the session briefing. Pushing is per-branch and carries the
dispatch discipline outward: first push sets tracking (push -u origin
session-94/raise-retry-budget), after which status and the counts speak
the branch’s divergence natively; and every push is preceded by the
behind-check, because pushing into a branch that moved produces the
non-fast-forward refusal — a coordination signal whose correct and
incorrect readings differ so consequentially that the next chapter
spends a section on it. What no fleet member does is push shared
integration branches as a side effect of its task: task branches are the
operators’ to publish; main moves through the integration ceremony of
chapter 8, one authority at a time — the same one-writer-per-truth
instinct the worktree refusal enforced locally, applied at the remote.
Composing repositories, avoided knowingly
Fleets eventually ask how repositories themselves compose — the shared
library, the vendored dependency, the platform repo the product repos
lean on — and the built-in answer, submodules, earns this book’s most
explicit advisory: understand it, and reach for it last. A submodule
pins another repository at a hash inside a parent tree, which is
exactly right as a concept (content-addressed composition, chapter 1
approving) and operationally hostile to unattended work in practice:
clones arrive incomplete until a second command runs, status in the
parent goes ambiguous about child state, every briefing and gate in
this book needs submodule-aware variants, and the classic accident — a
parent commit pinning a child hash that exists only on some machine —
is a broken build with no local evidence, the calm face at
architecture scale. The register’s preference order for the same
needs: a real dependency gets a release artifact and a lockfile (the
ecosystem’s package manager is the instrument built for pinning);
code that must live in-tree gets vendored as content (a plain copy,
committed, with its origin and version in the ledger entry — chapter
2’s provenance carrying what submodule metadata would have), or
subtree-merged where history import matters; and only the case that
truly needs live dual-repo development — rare, and staffed by seats
that will maintain the discipline — earns submodules, wired into the
open ritual (clone --recurse-submodules, update policy explicit) so
the sharp edges are at least institutional rather than per-seat
surprises. Composition is real; the advisory is only that the default
instrument for it should be the one whose failure modes the fleet’s
existing disciplines already cover.
Constrained seats: sparse and shallow
Two reduced forms of the working arrangement serve the fleet’s edge
cases, and both are volume one’s least-privilege instinct wearing git’s
clothes. Sparse checkout scopes a worktree to a subtree
(sparse-checkout set services/billing after worktree add): the
operator dispatched against one component sees only that component,
which shrinks its blast radius (a pathspec mistake cannot stage what the
tree does not materialize), its noise floor (status and diff speak only
in-scope), and — for the enormous monorepos where this matters most —
its checkout cost. The seat still holds full history through the shared
store; only the visible working surface narrows, which is precisely
the shape a scoped task wants. Shallow clones (clone --depth=1)
narrow the other axis — history instead of surface — and belong to a
different niche: the read-only consumer (a CI-style build, a one-shot
analysis) that needs today’s tree and no ledger. The register uses them
knowingly for that niche and refuses them everywhere this book’s
techniques live, because a shallow store amputates exactly what the
techniques consume: chapter 3’s archaeology stops at the horizon,
chapter 4’s bisection cannot frame, and chapter 1’s inheritance briefing
reads a history one commit deep. The rule of thumb the two forms share:
constrain the surface freely (sparse seats compose with everything),
constrain the ledger only for seats that will never ask it questions
— and when in doubt about which seat a task needs, the full worktree’s
eight kilobytes were never the thing to economize.
Rejoining: the merge as integration entry
Parallel work exists to converge, and the convergence primitive deserves its plain demonstration before chapter 8 builds ceremony on it:
mkdir project && cd project
git init -q -b main; git config user.email [email protected]; git config user.name operator
echo base > shared.conf; git add -A; git commit -qm "base config"
git worktree add -q -b task-a ../ra; git worktree add -q -b task-b ../rb
( cd ../ra && echo "retries: 8" > retries.conf && git add -A && git commit -qm "retries policy" )
( cd ../rb && echo "level: debug" > logging.conf && git add -A && git commit -qm "logging policy" )
git merge -q --no-edit task-a && git merge -q --no-edit task-b
ls *.conf; git log --oneline | head -4
logging.conf
retries.conf
shared.conf
48b47e9 Merge branch 'task-b'
3c8b45c retries policy
e9dcb23 logging policy
0fcb782 base config
Two operators’ work, integrated sequentially into main, both files
present, the merge itself an entry in the ledger (the second merge
recorded as such; the first fast-forwarded silently — the two integration
shapes chapter 8 weighs). What this chapter takes from the demo is the
register’s framing of conflict, because conflict is where parallel
work’s bill arrives. A merge conflict is not an error; it is the tool
reporting that two minds changed the same lines and no algorithm can rank
their intentions — a finding, in volume one’s vocabulary, demanding
judgment. The non-interactive protocol: attempt the merge with the
combined diff already read (diff main...task-a, chapter 3’s three-dot
review form — an operator that reads before merging predicts most
conflicts before creating them); on conflict, read the markers as
evidence (diff shows both sides annotated; checkout --conflict=diff3
adds the ancestor, and the three-way view is the whole story); resolve
by decision, not deletion — the resolution is a judgment about intent
that belongs in the merge commit’s message (chapter 2: the body answers
why); and when the judgment exceeds the operator’s authority — two
plausible intentions, no basis to rank them — the honest move is volume
one’s escalation discipline: stop, record the conflict as a finding, and
hand the decision to the supervisor rather than guessing silently. The
worst resolution in the register is the quiet one: checkout --ours as
a reflex is deleting a colleague’s intention without a hearing, and the
ledger will remember only that the merge “succeeded.”
One conflict amenity deserves the fleet’s attention because it converts
judgment already spent into judgment reused: rerere (“reuse recorded
resolution”, enabled once per store) records each conflict’s shape and
its resolution, and replays the resolution automatically the next time
the same conflict appears — which in fleet practice is constantly, since
a long-lived task branch merging a moving main re-meets its own
conflicts on every synchronization. The register adds one caution to the
convenience: a replayed resolution is a judgment applied without a fresh
hearing, so sessions note when rerere fired (its output says so) and the
first resolution’s reasoning still lands in that integration entry’s
message — the replay then inherits a recorded why, rather than becoming
automation of an undocumented decision, which no volume of this series
has been willing to bless.
The reviewer’s seat
One worktree pattern serves chapter 8 directly enough to install here:
review happens in its own tree. The reviewing operator — machine or
human-driven — adds a worktree at the proposal’s branch
(worktree add ../review-212 origin/session-95/harden-retries,
detached or tracking), and the entire review toolkit runs there
without touching any working seat: the diff read at chapter 3’s
resolutions, the build and tests run live (review that executes is
worth two reviews that squint), the suspicious behavior probed with
volume one’s instruments — while the reviewer’s own task, in its own
tree, stays exactly as it was. The economics repeat the chapter’s
opening: the alternative reflexes are stashing or committing half-done
work to switch branches (state churn in the reviewer’s seat, the exact
cost worktrees exist to delete) or reviewing from the diff alone
(fine for prose, thin for behavior). And the lifecycle rules apply
unchanged — the review tree is removed when the verdict posts, or
ledgered as open business if the review spans sessions — so worktree
list keeps telling the fleet’s whole truth: every open review visible
beside every open task, each a named seat with an owner and an age,
which is what a fleet’s work-in-flight was always supposed to look
like.
The fleet briefing
The chapter’s instruments compose into the supervisor’s standing view —
the parallel-work edition of the briefings volumes one and two
institutionalized — and writing it out fixes the queries as a set. Seats:
worktree list --porcelain (the machine format, one stanza per tree)
answers what is checked out where, joined against liveness the way volume
two joined registry rows against processes — a worktree whose branch has
not moved in days and whose session the registry shows ended is inherited
unfinished business, triaged by the lifecycle protocol below. Work:
for-each-ref refs/heads --sort=-committerdate --format='%(refname:short)
%(committerdate:iso) %(subject)' is the task registry — every branch,
its owner-by-naming-convention, its freshness, its last claim — with the
staleness threshold marking candidates for the graveyard review.
Parked state: the stash inventory from the suspicious-reading section,
ideally empty. Divergence: the ahead/behind counts against origin for
main and for every active task branch — the numbers that say which work
is ready to integrate, which is drifting from a moving main (the earlier
a task branch merges main’s progress, the smaller chapter 8’s conflicts
— a fetch-and-count line in each session’s own briefing makes the drift
visible daily), and whether anyone is sitting on unpushed work the fleet
cannot see. Six queries, one transcript page, and the answer to the
question every supervisor of parallel machines actually has — what is
in flight, how stale, and what needs a decision — read from the
repository itself rather than from the operators’ self-reports. The
fleet briefing is also where this chapter’s disciplines become
observable: seats named by convention, no anonymous stashes, no
immortal branches, divergence counted daily. A fleet whose briefing is
boring is a fleet whose habits are working — and a briefing that suddenly
grew interesting names, by line, which habit slipped and which seat
slipped it, which is all a supervisor ever needed monitoring to do.
Lifecycle: worktrees end like sessions
Worktrees are session-shaped, and everything this series knows about
session ends applies. The clean end is worktree remove (shown above)
plus the branch’s integration-or-triage — the workspace gone, the work
merged or accounted for. The unclean end — a session dies holding a
worktree — leaves the registration behind, and the successor meets it
exactly as volume two taught: worktree list is the run registry
(every tree, its branch, its staleness), a dead session’s tree is
inspected before disposal (uncommitted changes in it are the dead
session’s unfinished stage — read, salvage into a commit on its task
branch, or record the discard), and worktree prune clears
registrations whose directories are already gone. The estate closes the
loop: a dispatch pattern that records worktree births and deaths in the
registry gives the fleet’s supervisor one query for “what is checked
out where, by whom, since when” — which is this chapter’s whole subject,
reduced to the standing question it always was. Parallel operators,
then: one store because the ledger shares safely, one tree and one
branch per mind because working state does not, names that carry
provenance, merges that record judgment, and endings — clean or
inherited — that leave the fleet’s workspace as legible as any single
operator’s. What parallelism has not yet touched is the ledger’s own
integrity across all these hands, and that is the next chapter’s
covenant.