Splitting a Plan into dev-orchestra Work Units and Implementing Them Side by Side
Introduction
dev-orchestra 1.2.0 can split a plan into work units. Units that don’t depend on each other run side by side, each in its own git worktree, and are merged back into the working tree as they finish. All the changes from v1.1.0 to v1.2.1 are in the release post.
Normally, the orchestrator (Claude Code or another agent) runs all of these steps for you. This time, to see what happens, I ran the same commands by hand, one at a time.
My environment:
- Windows 11
- dev-orchestra 1.2.1
- Claude Code 2.1.309 (the implementer was Claude’s sonnet)
- Python 3.14.7
For the test, I pointed DEV_ORCHESTRA_HOME at a folder separate from my everyday configuration and worked in a small repository made for it (textkit).
dev-orchestra is open source on GitHub (MIT license). If it looks useful, a star would mean a lot.
Steps
1. Write ## Units in the plan
The example is a plan to add two functions, slugify(text) and word_count(text), to a small Python package called textkit. They live in separate files, so they can be built side by side. The README and CHANGELOG describe both, so they are updated together at the end.
In the plan (plan.md), add a ## Units heading and a yaml code block.
## Units
```yaml
units:
- name: slug
kind: feature
files: [textkit/slug.py, tests/test_slug.py]
depends_on: []
- name: count
kind: feature
files: [textkit/count.py, tests/test_count.py]
depends_on: []
- name: docs
kind: integration
files: [README.md, CHANGELOG.md]
depends_on: [slug, count]
```
### Unit: slug
1. Add `slugify(text)` in textkit/slug.py: lowercase, runs of non-alphanumerics become one `-`, strip `-` at both ends.
2. Add tests/test_slug.py.
- Give each unit a
name, akind(feature,fix,refactor,docs,test,ciorintegration), itsfiles(what it changes) anddepends_on(the units that must finish first) - Give each unit a
### Unit: <name>section with its steps - Files every piece touches, such as CHANGELOG, go to one
integrationunit that depends on all the others
In practice, the architect writes this block when it writes the plan (when the work splits across files, or the plan has more than four steps). This time I wrote it by hand.
List the paths under
## Files to Modifyin backticks, like`textkit/slug.py`. When I wrote them without backticks,units checkprintedwarning:lines saying the files were not in Files to Modify (it didn’t refuse).
2. Check with units check that the units can run
At first I had README in the files of both slug and count. In that state, units check refuses.
$ dev-orchestra units check
.ai/workflows/demo/plan.md: the units cannot be run:
- slug and count may run in parallel but both list README.md
The exit code is 2. Two units that may run side by side listing the same file would collide when merged, so it stops before anything runs. After moving README to the docs unit only, it passed.
$ dev-orchestra units check
3 units in 2 waves (implementation.max_parallel: 2):
wave 1: slug, count
wave 2: docs
slug and count run side by side in the first wave, and docs in the second. How many run at once is set by implementation.max_parallel (default 2, up to 8).
3. Run the units’ tests after merging
In my environment, whenever the implementer’s Claude tried to run a shell command, it waited for approval, so it couldn’t run the tests. (dev-orchestra starts a Claude implementer with --permission-mode acceptEdits, so file edits go through but shell commands need approval.)
So I put implementation.unit_tests: after_merge, added in 1.2.1, in the repository’s .dev-orchestra.yaml.
version: 1
implementation:
unit_tests: after_merge
With this, each unit only writes code and tests, and the test suite runs once, after everything is merged. It is meant for projects whose tests share a database or a port and would collide if run side by side.
The first time, I ran without this setting. One unit’s tool log had no
Writeat all and it never wrote its files, yet its report said it had created them.units mergeprintedmerged slug: 0 file(s), so the number of merged files is how you’d notice.
4. Approve the plan and run two units side by side
The implementer only runs on an approved plan. design approve records the approval (normally the orchestrator shows you the plan and runs this only when you say yes).
$ dev-orchestra design approve
approved .ai/workflows/units/plan.md (sha256 f108913500da) for workflow units
run implementer --unit <name> --detach starts one unit in the background. One request file (implement-request.md) serves every unit; the CLI puts the unit’s name, files and plan section in front of it.
$ dev-orchestra run implementer --unit slug --prompt-file .ai/workflows/units/execution/implement-request.md --detach
started implementer as job implementer-20261025-165514-ee46f2
$ dev-orchestra run implementer --unit count --prompt-file .ai/workflows/units/execution/implement-request.md --detach
started implementer as job implementer-20261025-165515-d7c62b
units status shows both running, each in its own worktree. The worktrees were created in a textkit.units folder next to the repository (paths partly omitted).
$ dev-orchestra units status
slug feature running job implementer-20261025-165514-ee46f2 base d736c6342cc7 ...\textkit.units\d736c634-slug
count feature running job implementer-20261025-165515-d7c62b base daf7aed8edd8 ...\textkit.units\daf7aed8-count
docs integration pending job - base - -
Ready to start: none
Unit tests: after_merge (units skip their tests; run them once on the merged tree)
The prompt a unit received started with a header like this (from the first try).
# Work unit: slug (feature)
This run implements one unit of the plan, and only that unit.
Its files: textkit/slug.py, tests/test_slug.py
5. Pick up whichever finishes first with jobs wait --any
Since 1.2.0, jobs wait takes several job ids. With --any, it returns as soon as any one of them finishes (output excerpted).
$ dev-orchestra jobs wait implementer-20261025-165514-ee46f2 implementer-20261025-165515-d7c62b --any
implementer-20261025-165514-ee46f2 RUNNING
unit: slug (...\textkit.units\d736c634-slug)
elapsed: 0m30s
activity: 9 tool uses, context 51,040 tokens
+00:15 Write textkit/slug.py
+00:17 Write tests/test_slug.py
...
implementer-20261025-165515-d7c62b SUCCEEDED
unit: count (...\textkit.units\daf7aed8-count)
activity: 8 tool uses, context 49,706 tokens
+00:15 Write textkit/count.py
+00:17 Write tests/test_count.py
...
count finished first while slug was still running. Both were small jobs that took around 30 seconds.
6. Bring them back with units merge
Each finished unit is brought back into the working tree with units merge. It merges file by file and never touches the index, HEAD or the branch.
$ dev-orchestra units merge count
merged count: 2 file(s)
I waited for slug to finish and merged it too.
$ dev-orchestra units merge slug
merged slug: 2 file(s)
$ dev-orchestra units status
slug feature merged job implementer-20261025-165514-ee46f2 base d736c6342cc7 -
count feature merged job implementer-20261025-165515-d7c62b base daf7aed8edd8 -
docs integration pending job - base - -
Ready to start: docs
The merged units’ worktrees were cleaned up. The working tree now had four new files.
$ git status --short
?? tests/test_count.py
?? tests/test_slug.py
?? textkit/count.py
?? textkit/slug.py
7. The integration unit runs in place
Nothing can run beside the remaining docs unit. In that case no worktree is made, and it runs right in the working tree (I ran it without --detach).
$ dev-orchestra run implementer --unit docs --prompt-file .ai/workflows/units/execution/implement-request.md
docs ユニットを実装しました。変更したのは README.md と CHANGELOG.md だけです。
...
$ dev-orchestra units status
slug feature merged ...
count feature merged ...
docs integration merged job - base - in place
Ready to start: none
(The implementer’s report is in Japanese because my environment asks for replies in Japanese. It says it changed only README.md and CHANGELOG.md.) A unit that ran in place counts as merged when it finishes. README gained a description of both functions, and CHANGELOG an ### Added section.
8. Run the tests on the merged tree
With after_merge, the tests run once at the end.
$ python -m unittest
.........
----------------------------------------------------------------------
Ran 9 tests in 0.000s
OK
budget show shows that running three units used only one implementer attempt (output excerpted).
$ dev-orchestra budget show
implementer 1/5 used
delegated runs 3/40 used
implementer pass open: slug, count, docs run (plan f108913500da)
The units of one plan share one attempt; only rerunning a unit spends another. The number of runs itself (delegated runs) still counts three.
9. When a merge conflicts (didn’t happen this time)
My merges didn’t conflict. According to the reference, a conflict happens when, for example, the same file changed in the working tree while the unit was running, and then:
units mergeexits with code 7 and leaves a brief at.ai/execution/units/<name>-conflicts.md- The orchestrator has the review_fixer resolve the conflict from that brief
units merge <name> --resolvedrecords it as resolved
I didn’t try a conflict, so this is only a description of how it works.
Summary
- Writing a
yamlblock under## Unitsin the plan splits the implementation into work units units checkrefuses units whose files overlap before anything runs, and prints the waves- Independent units run side by side in separate git worktrees and come back file by file with
units merge, which never touches the index or the branch - A unit nothing else can run beside (here, the
integrationunit) runs in place jobs wait --anyhands you whichever job finishes first- The units of one plan share one implementer attempt
- If your tests can’t run side by side, use
implementation.unit_tests: after_merge. And watch the file countunits mergeprints
I try the standing per-role instructions in the per-role instructions post.
GitHub: istb16/dev-orchestra (a star is always appreciated)
Related Posts
I Measured dev-orchestra: Time per Request, and Findings by Number of Reviewers
With dev-orchestra 1.2.1, I made one small feature request and measured how long it took (about 6 minutes, not counting my approval) and how many findings were reported and accepted with 1, 2, and 4 reviewers. More reviewers meant more accepted findings, but the extra ones were about tests.
Giving dev-orchestra Standing Instructions per Role
I tried instructions, added in dev-orchestra 1.2.1. Standing instructions for the implementer or reviewers written in the repository go into every prompt. I check, with real output, how they are shown, where they go, and how a change to the instructions is treated.
[Released] dev-orchestra: Parallel Work Units and Per-Role Instructions v1.1.0 - v1.2.1
A roundup of dev-orchestra's changes from v1.1.0 to v1.2.1: running a plan as work units side by side, standing instructions per role, questions before the design, and settings for progress reports. If you capture its output on Windows, note the encoding change.