tk3.biz
Back to Blog

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.

→ github.com/istb16/dev-orchestra

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, a kind (feature, fix, refactor, docs, test, ci or integration), its files (what it changes) and depends_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 integration unit 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 Modify in backticks, like `textkit/slug.py`. When I wrote them without backticks, units check printed warning: 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 Write at all and it never wrote its files, yet its report said it had created them. units merge printed merged 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 merge exits 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> --resolved records it as resolved

I didn’t try a conflict, so this is only a description of how it works.

Summary

  • Writing a yaml block under ## Units in the plan splits the implementation into work units
  • units check refuses 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 integration unit) runs in place
  • jobs wait --any hands 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 count units merge prints

I try the standing per-role instructions in the per-role instructions post.

GitHub: istb16/dev-orchestra (a star is always appreciated)

Related Posts