TL;DR — The moment a loop runs more than one agent, files collide. Worktrees remove the mechanical collision (each agent gets its own working directory); the maker/checker split removes the quality collision (no agent merges its own work). Use a three-role fleet: explorer, implementer, verifier. And the ceiling on fleet size isn't the tool — it's your review bandwidth.
Part 5 of the Loop Engineering series on ShipWithAI. Read the full article →
Two agents, one directory, two different disasters
The failure isn't one thing. It's two, and they need different fixes:
| Mechanical collision | Quality collision | |
|---|---|---|
| Symptom | Corrupted tree, broken merge, lost edits | Plausible-but-wrong change lands unreviewed |
| Cause | One working directory, racing writes | Author is also the only reviewer |
| Fixed by | Worktree per lane | Maker/checker split |
That second fix isn't house style, by the way — it's the evaluator-optimizer pattern from Anthropic's Building Effective AI Agents, where a generator and a separate evaluator pass work back and forth until the evaluator is satisfied.
Two agents in one directory is two engineers sharing one keyboard. The problem was never the agents, it was the missing isolation and the missing review.
Worktree vs branch — the distinction people get wrong
Straight from the FAQ, because this trips up a lot of people:
A branch is a pointer in history; a worktree is a separate checked-out directory for a branch. Two agents can be on two branches but still fight over one working directory.
Branches alone do not save you. Worktrees give each agent its own files on disk, which is what actually prevents the racing-write collision.
And in a loop, you don't create worktrees — you declare them:
# fleet config: each implementer lane runs isolated
- role: implementer
isolation: worktree # orchestrator provisions a private worktree + branch per lane
on_exit: auto-cleanup # finished or failed, the dead lane's worktree is removed
In an interactive session you create a worktree; in a loop you declare one and let the orchestrator provision and reap it. The difference is who cleans up when no one is watching.
Note this is opt-in. Subagents don't get isolation automatically — without isolation: worktree, every lane shares one directory, which is exactly the collision you're trying to avoid.
The three-role fleet
- Explorer — finds work, emits a list of independent findings. It does not fix anything; it produces the work list the fleet fans out over.
- Implementer (maker) — takes one finding, produces a diff in its own worktree. One implementer per lane, one finding per implementer.
- Verifier (checker) — reviews that diff against a verifiable stop condition, returns accept or reject.
Parallelism is for independent findings; the pipeline within a finding is always serial. An implementer that grades its own diff is not a fleet, it is one agent wearing two hats.
When to parallelize, when to serialize
| Situation | Decision |
|---|---|
| Findings touch different files, no shared state | Parallelize |
| Stage B needs stage A's output | Serialize |
| Two findings edit the same file | Serialize (one lane) |
| Later finding depends on an earlier merge | Serialize |
Note the asymmetry: you fan out across findings, never inside one.
The hands-on run
Read the caveat first. The author is explicit: this is a mechanism proof, not a Haiku-graded autonomous fleet. The maker step is a deterministic edit rather than an LLM agent turn, and nothing was instrumented, so there are no token figures to report (run dated 2026-06-15). What it proves is that the orchestration plumbing — isolate, make, check, merge, reap — works as described.
Worth skimming Part 3 and Part 4 first, and knowing what a worktree is — the article deliberately doesn't re-teach the git mechanic.
Two independent bugs:
-
Finding A —
mathutil.add(a, b)returnsa - b. Failstest_mathutil.py::test_add -
Finding B —
strutil.shout(s)returnss.lower(). Failstest_strutil.py::test_shout
Isolate:
$ git worktree add ../wt-finding-a -b fix/finding-a
$ git worktree add ../wt-finding-b -b fix/finding-b
$ git worktree list
/home/you/p5-fleet 25ee424 [master]
/home/you/wt-finding-a 25ee424 [fix/finding-a]
/home/you/wt-finding-b 25ee424 [fix/finding-b]
Make — each lane edits only its own file:
# in wt-finding-a
def add(a, b):
- # BUG (Finding A): returns difference instead of sum
- return a - b
+ # FIX (Finding A): correct sum
+ return a + b
Lane B gets the same treatment on strutil.py. And the isolation is real, not asserted — from inside lane A, lane B's file is still broken:
$ git show HEAD:strutil.py # in wt-finding-a
def shout(s):
# BUG (Finding B): lowercases instead of uppercasing
return s.lower()
Check — each verifier runs only its lane's test:
$ pytest -q test_mathutil.py # in wt-finding-a
1 passed in 0.00s # exit 0
$ pytest -q test_strutil.py # in wt-finding-b
1 passed in 0.00s # exit 0
Merge — both lanes land in one state file (Part 4's schema), findings moving open → tried + passed:
## tried
- [finding-a] worktree wt-finding-a / branch fix/finding-a: maker edited mathutil.py (a-b to a+b)
- [finding-b] worktree wt-finding-b / branch fix/finding-b: maker edited strutil.py (lower to upper)
## passed
- [finding-a] test_mathutil.py::test_add green in wt-finding-a (checker pytest exit 0)
- [finding-b] test_strutil.py::test_shout green in wt-finding-b (checker pytest exit 0)
## open
## blocked
The merge is the moment of truth. Two isolated lanes are only useful if their results land in one place the next run can read, and that place is the state file from Part 4.
Reap:
$ git worktree remove ../wt-finding-a
$ git worktree remove ../wt-finding-b
$ git worktree list
/home/you/p5-fleet 25ee424 [master]
If a lane fails mid-run and you have on_exit: auto-cleanup set, its worktree is removed so the loop doesn't accumulate orphan directories across iterations — and the finding stays open (or moves to blocked), so the next run retries without re-deriving what already happened.
The orchestration tax
Every lane you add is a trade, not a free win:
| Per lane added | Effect |
|---|---|
| Token cost | Up (≈ N× per-agent cost + orchestration overhead) |
| Wall-clock to results | Down (lanes run concurrently) |
| Your review load | Up (every green diff still needs a human merge) |
That third row is the one that bites.
Worktrees and checkers let you run more agents. Your review bandwidth decides how many you should. The tool's ceiling and your ceiling are not the same number, and yours is lower.
The rule of thumb: if you can honestly review N PRs a day, run at most N lanes. The author flags this as his own framing — a heuristic, not a measured constant. Calibrate it to your team.
Prior art
Plus Anthropic's Building Effective AI Agents for the evaluator-optimizer pattern that the maker/checker split descends from.
Try it this week
Take two genuinely independent bugs. Give each its own worktree and branch. Have something other than the fixer run the test. Merge both into one state file, then reap the worktrees.
If you can't name why the two findings are independent, you have one lane, not two.
This is a condensed summary. The full article has the complete fleet walkthrough, both diffs, and the FAQ on failure handling and fleet sizing:
👉 Git Worktrees for Agents: Parallel Loops Without Chaos — Part 5, ShipWithAI
Earlier: Part 1 — Why You Should Stop Prompting · Part 2 — Anatomy of a Loop · Part 3 — Stop Conditions · Part 4 — State File Pattern
Next: giving the fleet a heartbeat — scheduled discovery, and runs that archive themselves.
Top comments (0)