Xan Torres
All writing
August 26, 20265 min readAI agents · Git · Developer tools

State for parallel coding agents

Two coding agents in one repository fail in predictable ways. The fixes are old Git plumbing: worktrees, atomic claims, and a merge driver.

One coding agent working a repository is a solved problem. The moment I ran two or three in parallel on the same repo, everything broke in the same four ways.

Two agents edited the same file on different branches, and the merge needed me to untangle it. I had four terminal tabs open and no idea which agent was doing what. Two dispatch runs picked up the same task, and one of them threw away hours of work. And agents drift: you hand one a task, it touches files that were never part of it, and you find out while reviewing the PR.

None of this is exotic. It is what any distributed system does when workers share mutable state without coordination. The workers happen to be coding agents now, and the shared state happens to be your repository.

I built RepoKernel to fix it. The part worth writing about is not the tool. It is that every fix turned out to be Git plumbing that has existed for years.

The repo is the source of truth

The first decision was where coordination state lives. The reflex answer is a service: a daemon that tracks tasks, a database of claims, a dashboard on a port. I did not want the tool that guards my repository to be another process that can crash, drift, or need an update, so I took the opposite constraint: plain files and Git, nothing else. Tasks, sprints, reviews, and claims are files in the repo, versioned with the code they orchestrate.

That constraint forced the three mechanisms below. If state lives in files, then isolation, mutual exclusion, and conflict resolution all have to come from things Git and the filesystem already do.

One worktree per task

git worktree is old, boring, and exactly right for this. Every task runs in its own worktree, an isolated checkout of the repository on its own branch. Two agents can now edit the same file at the same time without touching each other, because each one works a physically separate copy. Main stays clean until something earns a merge.

Isolation alone does not stop drift, though. An agent inside its own worktree can still wander into files that were never part of the task. So every sprint declares the paths it owns up front, and the diff classifier marks anything committed outside that scope as out of scope, which holds it at the review gate. Not a filesystem lock, a declared boundary with an enforcement point. The agent can technically type anywhere; the work cannot land outside its lane.

Claims, not checks

The double-claim bug is a textbook race. Dispatch loop A reads the sprint list, sees S-12 unclaimed, and starts it. Loop B did the same read a moment earlier. Both spend an hour on S-12, one merge wins, and the other hour is garbage.

Check-then-act does not work on shared state, so claiming had to become atomic. Claiming a sprint means creating a claim file keyed by the sprint id, and file creation is the atomic primitive: whoever creates claims/<sprint-id>.json first owns the sprint, and the loser gets a clean refusal instead of a wasted hour. There is a retrying lock around the edges, but the principle is the whole fix: turn look-then-take into a single operation that can only succeed once.

A merge driver for state

The nastiest failure was the state registry itself. RepoKernel keeps a JSON registry of tasks and their statuses inside the repo, which means two concurrent branches both update it, and a textual merge produces conflict markers in the middle of a machine-read JSON file. The orchestrator chokes on its own state, and a human ends up hand-editing JSON, which is exactly the babysitting the tool exists to remove.

Git has had the answer for a long time: a custom merge driver. A .gitattributes line routes merges of that one file through a command instead of the textual algorithm.

# .gitattributes, installed by rk init
.repokernel/registry.json merge=repokernel-registry

# per-clone Git config, also installed by rk init
[merge "repokernel-registry"]
    driver = rk registry-merge-driver --current %A --other %B --base %O

The driver merges the registry semantically. Entries union by id. When both sides carry the same entry, the more progressed status wins, and ties break lexicographically, so the result is deterministic and order-independent: merging a into b gives the same registry as merging b into a. No conflict markers, no hand-editing, no divergence between two merge orders.

One honest caveat, because it changed how I run the tool. Merge drivers are local configuration. A clone that has not run rk init does not have the driver, and the merge buttons on GitHub and GitLab never execute it, because hosted merges do not run your local Git config. So the deterministic guarantee holds for local merges, and CI validation backstops everything else, catching a registry that a hosted merge let drift.

The gate

Isolation and claims control how work runs. The gate controls what lands. Nothing merges until the configured check command passes and a review verdict is recorded. A failed check leaves the sprint active rather than merged, so a half-done task stays visibly half-done until I retry it or discard it on purpose.

The same idea shapes how runs stop. A crashed or paused run records a structured halt reason, a specific well-known state, instead of leaving a stack trace to interpret. Resuming means reading state, not doing archaeology on logs.

What changed, and what I will not claim

My role changed shape. Running agents used to mean babysitting each one; now I define tasks and boundaries, agents execute in parallel, and I review what comes out. The judgment stays mine. The typing does not.

What I will not claim: adoption. I built RepoKernel for my own daily use and published it on npm under MIT, and I am not going to pretend there is a community around it. The harnesses have also been catching up; worktree isolation, for example, has been appearing natively in the platforms since I built mine. The parts that are still distinct are declared path ownership, dependency sequencing between tasks, and the review gate before merge. Watching the platforms move in the direction I built toward reads, to me, as evidence the problem was real.

It is also deliberately narrow, and the wrong tool for plenty of situations. A one-off script, a throwaway prototype, a non-Git workflow, or a team that already gates everything through CI and branch protection does not need this layer, and the README says so.

The memory half of this story, engram, a local-first store that agents share over MCP with consent gates on sensitive facts, deserves its own write-up. The short version of both: agents supply the speed, and Git plumbing keeps the state honest.