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.
# 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
- A worktree needs a git repository. A missing git binary, a folder with no repository, or a repository with no commits falls back to the workspace root with a notice. Containers and scratch folders are where this shows up.
- A dirty tracked tree blocks isolation. The two ways on are stashing those changes or running that one run without isolation. Untracked files never block: the bootstrap accounts for them.
- The queue is serialized. Landing is one task at a time, so the throughput you gain is bounded by the dependency graph the planner emitted. Isolation makes same-file overlap safe, it does not predict it.
- A shared dependency folder is still shared. Two worktrees installing at once race, because the install is shared. The setup command is the escape hatch.
- Some shared files are copies on Windows. A hard link that is impossible across volumes becomes a copy, and edits to it stay inside that task's worktree.
- Nothing is cleaned up that might be yours. A worktree holding unlanded work is kept, because it may belong to a runner another host is still driving. That is a directory you delete by hand.
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.