Where the work goes

Running several coding agents on one repo without them overwriting each other

Since September 25, most of Ordewell has been built by Ordewell. Its git history holds 129 merge commits named after a task, one for each piece of work an agent finished in its own worktree and that landed on a run's integration branch before I reviewed and merged it. On October 7 alone that was 23 tasks: race fixes, a lexer split, cleanup, and the release itself.

Before that, every agent worked in the same checkout, and the planner was asked to keep parallel tasks on different files. That is advice to a model, not a boundary, and when it fails, it fails quietly: one agent rewrites a file another has just changed, both report done, and the first passes its check against a tree that no longer holds its work. Here is how the isolation works now, and what it took to make a fresh worktree usable at all.

Disclosure: I build Ordewell, so this is how it does it rather than a survey of the field. It is free and Apache 2.0. The mechanics matter whether or not you install it, because every harness that runs several sessions answers the same question.

The problem with one shared checkout

The first version is a directory and a loop: several sessions run in the workspace root, and the prompt asks the planner to keep parallel tasks on different files. That is advice to a model, not a boundary.

It fails in both directions. Two tasks that genuinely need the same file get serialized behind an invented dependency, which costs the parallelism you were paying for. And when the overlap happens anyway, it fails silently: runner B rewrites the file runner A just changed, A still reports done, and A verifies as passed against a tree that no longer holds A's work. There is a second loss too: no per-task record of what changed, so no way to undo one task without unpicking the others.

One worktree per task, one branch per run

When the workspace is a git repository, each task runs in its own worktree, created from the integration tip of the run rather than from your working tree. The worktrees sit under .ordewell/worktrees/<run-id>/<order>-<slug>, which is already git-ignored, and each task carries its own branch.

The runner is handed a cwd and nothing else. Git never enters the runner adapters, so every agent isolates work the same way. A workspace that holds more than one repository is treated as a group: one worktree per repository per task, all on the same branch name.

Making a fresh worktree actually runnable

A fresh worktree with no node_modules cannot run your tests, so ignored artifacts are linked in: dependency folders, virtualenvs, environment files, agent config directories. .ordewell/ is never linked, because session and skills state belongs at the main root where a runner cannot corrupt it. A configured setup command replaces the linking where sharing is wrong.

Two details bite. Links are recorded per task, because an ignore rule such as node_modules/ does not match a symlink, and without that record they would be committed into the task's branch. And a whole folder link is wrong for workspaces: npm and pnpm install a workspace package as a link inside node_modules, which through a folder link resolves from the main checkout, so a task changing a package tests its dependents against the stale copy there. Entries are mirrored one at a time instead, and workspace links are recreated to resolve inside the worktree.

Integration is a queue, and it is serialized on purpose

Isolation makes overlap safe. It does not make overlap free. Verdicts can arrive at the same moment, so landing is a queue: one task at a time, with the lowest plan order going first among the tasks already waiting.

Each task lands as a merge commit on the run's integration branch, not as loose files copied over your code. A merge commit preserves any commits the runner made itself, shows who did what in git log, and leaves the task branch an ancestor, which is what makes deleting it safe.

A task that did not land is not a lost cause. The merge is aborted and both refs are kept, so the work is still there to inspect, retry or hand to a repair attempt. Landing across several repositories is atomic: if one repository conflicts, the merges already made for that task in the others are rolled back, so the task is conflicted as a whole rather than half landed.

Conflicts get a bounded repair, judged by evidence

Most conflicts a parallel run hits are not real disagreements. The integration branch moved while a task worked, and the task's branch is now behind it. Stopping the whole run for a human every time does not scale as parallelism grows. Letting a model resolve conflicts unchecked is worse: it silently rewrites a merge nobody reviewed.

So a conflicting landing starts a repair: a new attempt of the same task, in its kept worktree, on the same runner, model and mode, told to merge the current integration tip and resolve the listed files so both sides survive. The repair counts only on evidence: the task reports done, its branch now contains the tip it started from, git diff --check finds no leftover conflict markers, and the normal landing then goes through. If the repair did not really bring the branch along, it conflicts again rather than being taken at its word.

It is bounded: two repairs per task by default (conflictRepairAttempts, and 0 turns it off). After that the task waits for you with the conflicting files named and its worktree kept, while the rest of the run carries on. The handoff lists every task that landed through a repair, with its files, so review knows where to look.

The end of a run is a handoff, not a merge

Ordewell never merges into the branch you have checked out on its own. That is the one irreversible step, and your tree may hold work in progress. A run ends with the integration branch parked for review, the base ref it came from, and a list of what landed.

Merging on request preflights every repository first: no merge already in progress, no conflict against what you have checked out, no uncommitted edit to a file the merge would change. If any repository fails the preflight, nothing is merged anywhere. Discarding removes the worktrees and task branches, and never deletes landed work you have not merged yourself.

bash
# a dirty tracked tree in any repo holds the whole run
ordewell run --stash
ordewell run --without-isolation

# end of run: integration branches parked, one per repo
ordewell handoff review
ordewell handoff merge
ordewell handoff discard

Honest limits

Source and design notes

The decisions above are written down in the repository rather than summarised from memory: ADR 0013, worktree isolation, ADR 0014, multi-repo workspaces, ADR 0015, conflict repair, the isolation interface, and github.com/ordewell/ordewell.