| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
A lightning talk about parallel AI agents, and how every solution breeds the next problem.
The structure is a ladder. Each rung solves the problem below it and creates the one above it. The punchline is at the top: none of these problems are new, and the last bottleneck is one you can't buy your way out of.
Problem. The agent is fast, confident, and occasionally wrong in ways that look right. It invents a helper that already exists three packages over. It "fixes" a test by weakening the assertion. Output varies run to run, so you can't even reproduce the mistake to explain it.
Solution: guardrails and backpressure.
You can't review your way out of this. The volume is against you and always will be. The move is to make the machine's mistakes mechanically detectable — every convention you'd otherwise state in prose becomes something that fails a build:
| Concern | Guardrail |
|---|---|
| Style, formatting, obvious smells | linters, formatters, git hooks |
| "Did it actually test anything?" | mutation testing |
| Layering, dependency direction | ArchUnit, dependency-cruiser, import rules |
| Cross-service assumptions | contract tests (Pact) |
| Repo conventions | CLAUDE.md, skills, Claude Code hooks |
| Whole classes of bug | types, exhaustive matching, strict everything |
Backpressure is the key word. Not "the agent should be careful" — a wall it hits.
And now the next problem: one guarded agent works well. So you run several.
Problem. Three Claude sessions, one working directory. It goes wrong immediately:
The failure isn't that they write bad code. It's that the filesystem is shared mutable state and nobody is holding a lock.
Solution: git worktrees.
One checkout per agent, all backed by the same object store.
git worktree add ../feat-payments -b feat-payments
git worktree add ../fix-flaky-test -b fix-flaky-test
git worktree list
git worktree remove ../feat-paymentsWhat you get:
Worth showing live: git worktree list with four trees, then cat ../feat-payments/.git to reveal the pointer. It demystifies the whole thing in about five seconds.
You don't have to take my word for it — watch what the agent tools ship:
A plumbing command from 2015 became a checkbox in AI tooling in about eighteen months. That's the ecosystem agreeing on what the contended resource actually is: the filesystem.
And now the next problem: the trees are isolated. Rather more isolated than you wanted.
This is the rung people skip in the blog posts, and the one that actually burns you on day one.
Problem. Worktrees copy what git tracks. Everything git ignores is exactly what your app needs to run:
And then, once you get them running, they collide anyway:
Solution: treat tree creation as provisioning, not as a checkout.
A bootstrap script that runs on every worktree add:
cp ../main/.env .env # or symlink, if it's stable
export COMPOSE_PROJECT_NAME=$(basename "$PWD")
export PORT=$((3000 + $(git rev-list --count HEAD) % 100)) # or any per-tree allocation
npm cidirenv per tree, a devcontainer, or a nix shell all do this more properly. In Claude Code, a SessionStart hook is the natural home for it: the agent's environment is correct before it types anything.
The mental shift is the point: a worktree is an environment, not a folder.
And now the next problem: four agents, four green branches. Now land them.
Problem. Every branch is green. That means nothing.
Agent A's branch was green against main@abc. While its 12-minute suite ran, agent B landed. Now A must rebase, re-run, and hope it wins the race this time. With two agents it's annoying. With six, nobody ever finishes — you spend the whole day rebasing, and the branch that gets merged is simply the one whose tests happened to be shortest.
Worse, the merged state was never tested. main goes red from a combination that no CI run ever saw.
Solution: a merge queue.
The rule it enforces — Graydon Hoare called it "the not rocket science rule of software engineering", and it is genuinely the whole idea:
Automatically maintain a repository of code that always passes all the tests.
Not "test the branch". Test the state that would exist after merging, and merge only that exact state. The queue serialises the landing, does the rebase for you, and re-runs against the real target. Speculative execution lets it test several candidates optimistically in parallel and throw away the losers.
GitHub merge queue, Mergify, Graphite, Zuul — and Bors, which was doing this in 2014. Which is the first hint of where this talk ends up.
And now the next problem: you've made landing safe. You've also made it a single lane.
Problem. Twenty agents, a fifteen-minute pipeline, one lane. The maths is brutal and the queue backs up for hours. Agents are idle waiting to land, or worse, they keep working and the diffs get staler.
Then flakiness stops being annoying and becomes fatal. A speculative batch of five goes red because of one flaky test; the queue can't tell who's guilty, so it bisects, re-runs, and burns an hour attributing blame for a failure that wasn't real. The queue's entire premise is red means guilty. A flaky test is an attack on that premise.
Solution: make CI fast and make it honest.
And now the next problem: everything's green, everything's merged, and the product is broken.
The best material in the talk. Give it the most time.
Problem. Git merges text. Agents write meaning. Both branches green, merge clean, zero conflict markers, product broken:
Even a merge queue only half-saves you: it tests the merged state, so it catches this if you have a test that would notice. For the duplicate UserService, you don't.
Solution: partly tooling, mostly architecture.
Tooling: timestamp or hash your migration names instead of sequencing them. Lockfile discipline. Contract tests for the cross-boundary assumptions. ArchUnit for "there is exactly one of these, and it lives here".
But the real fix is disjoint blast radii — giving each agent a slice of the codebase that can't semantically collide with the others. Which is to say: your ability to run agents in parallel is bounded by how well your codebase decomposes.
If everything routes through one 4,000-line service class, you get one agent. That was always true for humans too. You just never ran ten of them at once, so you never found out.
And now the next problem: it all lands, and you have no idea what's in it.
Problem. Eight agents, eight PRs an hour, one of you. You start skimming. Then you start trusting the green check. The slop you built all those guardrails against gets in anyway — laundered through a passing pipeline and your own fatigue.
Solution: stop reviewing everything equally.
And now the next problem: the same mistake, three times, by three agents.
Problem. Agent A spends twenty minutes discovering that this repo's ORM does something cursed with lazy loading. It works around it, correctly. The session ends. That knowledge is gone. Agent B rediscovers it tomorrow, and works around it differently, and worse. Parallel agents don't just fail to share context — they actively diverge, and the codebase grows three dialects.
Solution: memory belongs in the repo, not the session.
If you've told an agent something twice, it belongs in the repo.
CLAUDE.md, skills, ADRs. And better than any prose: encode it as a rule that fails the build, because a convention that's merely documented is a convention that's optional. Which is rung 0 again — the ladder closes.
Look at what's on this list. Worktrees are per-developer checkouts. A merge queue is Bors, 2014. Contract tests are microservices, 2015. Quarantining flakes, trunk-based development, small reviewable diffs, writing your conventions down — every rung is something the industry built for human teams, decades ago, and then half-adopted because you could get away with half-adopting it.
AI didn't invent new problems. It turned the dial from five developers to fifty and made every latent weakness in your process load-bearing.
And watch where the bottleneck goes as you climb:
agent speed → machine resources → merge throughput → review capacity → architecture
You can buy your way out of the first four. Faster models, bigger runners, more lanes, more reviewers.
Your codebase's parallelism ceiling is your architecture. AI just found it.
Backup-slide material. Mostly you won't show this, but it's what people ask about afterwards.
| Command | What it's for |
|---|---|
| git worktree add <path> -b <branch> | new branch, new tree — the 90% case |
| git worktree add <path> <branch> | check out an existing branch |
| git worktree add <path> | branch is named after the directory |
| git worktree add --detach <path> | no branch — throwaway tree for bisecting or "just look" |
| git worktree add --guess-remote <path> | track origin/<dir-name> if it exists |
| git worktree list [--porcelain] | what exists; --porcelain is the scripting hook |
| git worktree remove <path> | the correct way to delete one (--force if dirty) |
| git worktree prune | clean up after someone rm -rf'd a tree instead |
| git worktree move <from> <to> | relocate a tree |
| git worktree repair | fix the pointers after you moved things by hand |
| git worktree lock / unlock | stop prune touching a tree on a detached drive |
What it actually looks like (git 2.43, real output):
$ git worktree list
/scratch/demo d2aff83 [main]
/scratch/feat-payments d2aff83 [feat-payments]
$ cat ../feat-payments/.git
gitdir: /scratch/demo/.git/worktrees/feat-paymentsThe branch mutex, verbatim:
$ git worktree add ../another feat-payments
Preparing worktree (checking out 'feat-payments')
fatal: 'feat-payments' is already used by worktree at '/scratch/feat-payments'And the gotcha everyone hits — delete the directory by hand and git still has the metadata, so the branch stays locked until you prune:
$ rm -rf ../feat-payments
$ git worktree list
/scratch/demo d2aff83 [main]
/scratch/feat-payments d2aff83 [feat-payments] prunable # ← still there
$ git worktree prune # now the branch is free againGUI support is the weak spot. This is a CLI-first feature and the tooling is uneven: lazygit has a worktrees panel, GitKraken and Tower support them, VS Code has no native UI (each tree is simply another window — extensions fill the gap), and JetBrains varies by version. Support moves fast enough that you should check your own machine before demoing anything. Demo the CLI.
The single most useful table in the talk, because everything below is a consequence of it:
| Per-tree (isolated) | Shared (one copy, common dir) |
|---|---|
| HEAD, index, working files | objects, refs, branches, tags |
| in-progress rebase / merge / bisect | git stash ← surprises everyone |
| HEAD's reflog, ORIG_HEAD | hooks, remotes, config |
| sparse-checkout settings | submodule git dirs (.git/modules/*) |
refs/stash lives in the common dir, so there is one stash stack for every worktree. Agent A stashes, agent B pops, and the isolation you just bought evaporates. If you need per-tree config, git config --worktree works — but only after git config extensions.worktreeConfig true.
One rule decides every case:
Share what the lockfile determines. Never share what your source determines.
node_modules is a pure function of the lockfile — two trees on the same lockfile want byte-identical contents, so sharing is free. bin/, obj/, dist/, target/ are functions of your code, which is precisely the thing that differs per tree. Share those and you have rebuilt the original problem: agent A's build output overwrites agent B's and the tests run against the wrong binary. (In .NET it's worse than wrong — obj/project.assets.json bakes in absolute paths, so even copying it is broken.)
How to actually share the shareable half:
Per ecosystem:
| Share | Never share | |
|---|---|---|
| Node | store via pnpm, or ~/.npm cache | dist/, .next/ |
| .NET | ~/.nuget/packages (already global) | bin/, obj/ |
| Java | ~/.m2, Gradle cache | target/, build/ |
| Rust | CARGO_TARGET_DIR can be shared — cargo locks and fingerprints it, saving real disk, at the cost of builds serialising on that lock | — |
| Python | the wheel cache; use uv | the venv itself — absolute paths are baked into shebangs and pyvenv.cfg, so it is neither shareable nor movable |
One .NET-specific landmine: if someone has set a shared BaseOutputPath or ArtifactsPath in Directory.Build.props, every worktree writes build output to the same place and you get the collision anyway. Check before you scale up.
git clone --bare git@github.com:org/repo .bare
echo "gitdir: ./.bare" > .git
git config remote.origin.fetch '+refs/heads/*:refs/remotes/origin/*'
git fetch origin
git worktree add main
git worktree add feat-payments -b feat-paymentsrepo/ ├── .bare/ # the object database ├── .git # a file: "gitdir: ./.bare" ├── main/ # just a worktree └── feat-payments/ # also just a worktree
That third command is not optional. git clone --bare sets no fetch refspec, so without it git fetch populates no remote-tracking branches and you will spend twenty confusing minutes wondering where origin/main went.
Why bother?
Cost: the setup ritual, that refspec footgun, and the occasional tool that assumes a normal clone.
Two problems, both verified rather than folklore.
They don't come along. git worktree add leaves submodule directories empty. Every new tree needs git submodule update --init --recursive by hand — one more reason tree creation should be a script, not a command.
They aren't isolated. A submodule's git dir lives in the superproject's common directory:
$ cat libs/sub/.git
gitdir: ../../.git/modules/libs/sub.git/modules/ is shared by every worktree. So the isolation you bought at the top level does not reach all the way down: two trees pinning different submodule commits are contending over the same submodule state. git worktree repair fixes pointers after moves, but it doesn't fix the underlying sharing.
If you're on submodules and going parallel: test it hard before betting a workflow on it. This is one of the better arguments for package registries over submodules.
Five minutes is four beats, not eight.
Cut to: 0 → 1 → 3 → 5, then the close. Slop, worktrees, merge queue, semantic conflicts, bottleneck migration.
Notes on delivery:
The appendix is your Q&A ammunition — commands, node_modules vs bin/obj, the bare layout, submodules. Expect the node_modules question every single time.
Candidate topics, none currently in the cut. Written up to be argued about later.
The "here's where this is heading" slide. jj is git-backed — jj git init --colocate adopts an existing repository, so this isn't a migration — and its equivalent of worktrees is workspaces. But the interesting part isn't the workspaces, it's the model underneath, which happens to remove three of our rungs by construction:
Read together: most of what we're bolting onto git for agents, jj already treats as the default. Verdict: excellent closing slide, dangerous middle slide. It opens a second front — half the room won't know it, and "consider replacing your VCS" is a large ask in five minutes. Best as a single forward-looking beat after the real close, if at all.
The strongest candidate, because it reinforces rung 5 instead of adding a topic.
git clone --filter=blob:none git@github.com:org/monorepo # history without the blobs
git sparse-checkout set --cone apps/web libs/ui # materialise only this sliceCrucially, sparse-checkout settings are per-worktree (see the shared/per-tree table), so every agent's tree can carry a different cone. On a large monorepo that's the difference between eight trees being unthinkable and being routine: eight 200 MB slices instead of eight 4 GB clones.
The conceptual payoff is better than the disk saving, though. "Disjoint blast radii" stops being an instruction you give an agent and becomes a property of its filesystem — it cannot edit, or even read, code that was never materialised. That is context engineering by way of git plumbing: a smaller tree is also a smaller space for an agent to wander into.
Caveats: --filter needs server support (GitHub has it), and build tools or IDEs that assume a complete checkout will need convincing. Verdict: the one I'd actually add. It costs about forty seconds and makes rung 5 land harder.
Rung 2 established that worktrees aren't hermetic. This generalises it into a spectrum:
worktree → devcontainer → remote VM / cloud session
filesystem + ports, services + CPU, network, blast radius
toolchain versions
Each step isolates more and costs more. Worktrees do nothing about ports, databases, global caches, the version of Node on your PATH, or anything an agent does outside the repository — npm i -g, docker state, system files. Containers cover that; remote sessions cover the machine itself.
The useful framing is a rule rather than a recommendation: isolate at the cheapest rung that separates the thing actually colliding. Most teams need worktrees plus a bootstrap script, not a container per agent. Verdict: one line inside rung 2, not its own slide. Low novelty — everyone already knows containers exist — but it stops the "why not just use Docker?" question from derailing Q&A.
Small, practical, chronically underused.
git config rerere.enabled trueGit records how you resolved a conflict and replays that resolution automatically when the same conflict reappears. With a merge queue this is worth more than it looks: agents rebase repeatedly against a moving target and hit the same conflict on every retry. Enable rerere and you resolve it once.
There's a neat tie-in with the shared/per-tree table — rr-cache lives in the common dir, so it's shared across every worktree. Agent A's resolution silently helps agent B. That's the sharing model working for you for once.
The catch is the same mechanism: it applies a remembered resolution quietly, so if the right answer has changed since, you get a stale merge with no announcement. git rerere diff to inspect, git rerere forget <path> to reset. Verdict: a one-line "turn this on" aside during rung 3. Real value, no narrative weight.
The honest counter-argument, and worth taking seriously because the whole talk assumes branch-per-agent.
Trunk-based development says the merge problem is self-inflicted: don't accumulate divergence and you won't need machinery to reconcile it. Agents commit small increments straight to main, incomplete work hides behind feature flags, integration happens continuously, and rungs 3, 4 and 5 shrink dramatically — there's no rebase race, no stale branch, no week of semantic drift to discover at merge time.
What it costs: rung 0 has to be much stronger, because there's no branch quarantine — bad code reaches main immediately. You've traded merge risk for production risk, which you then manage with flags and progressive delivery. Whether that's a good trade depends on how much you trust your guardrails, which is a nice callback.
And note what survives: agents still need separate working directories even when every one of them targets main. Branches are the optional part; worktrees aren't. That's the sharpest line in this section and it arguably belongs in the talk regardless.
Verdict: worth thirty seconds of steelmanning. Naming the strongest objection yourself makes everything before it more credible.
| Back | FazBrowse Home | New Git URL |