DEV Community

Ordewell
Ordewell

Posted on Originally published at ordewell.ai

One worktree per task: where a coding agent's work actually goes

Run two coding agents at once in the same checkout and you have two writers on one tree. Both sessions look fine, both print a marker, and one has already overwritten the other's file. The fix is not a better prompt, it is a boundary: every task gets its own git worktree, and the results stay apart until you decide what lands.

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's completion marker still appears, 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//-, 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 Claude Code, Codex and OpenCode cannot develop three different ideas about how work is isolated. 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 are surfaced, not quietly resolved

Resolving a conflict means deciding which of two changes wins. Handing that to a model silently rewrites a merge nobody reviewed, and it makes the outcome depend on a model's word, the thing an evidence based verdict exists to avoid.

So the conflict is surfaced with the files that conflicted, and resolving it is opt in. A bounded repair attempt can run as a new attempt of the conflicted task in its own worktree, and it still lands through the same queue with the same checks: its branch has to contain the tip the repair started from and add no leftover conflict markers. If the repair did not really bring the branch along, it conflicts again rather than being taken at its word.

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
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)