tk3.biz
ブログ一覧に戻る Back to Blog

dev-orchestra の作業単位で、計画を分けて並べて実装してみた Splitting a Plan into dev-orchestra Work Units and Implementing Them Side by Side

はじめに

dev-orchestra 1.2.0 で、計画を作業単位に分けて実装できるようになりました。互いに関係しない作業単位は、それぞれ専用の git worktree で並べて走り、終わったものから作業ツリーへマージされます。v1.1.0 から v1.2.1 までの変更全体は、リリースの記事にまとめています。

ふだんは、オーケストレーター(Claude Code など)がこの手順をすべて自動で進めます。今回は何が起きているのかを見るために、同じコマンドを手で 1 つずつ実行しました。

試した環境です。

  • Windows 11
  • dev-orchestra 1.2.1
  • Claude Code 2.1.309(implementer は Claude の sonnet)
  • Python 3.14.7

試すときは、普段の設定とは別のフォルダを DEV_ORCHESTRA_HOME に指定し、作業用に作った小さなリポジトリ(textkit)で動かしています。

dev-orchestra は GitHub で公開しています(MIT ライセンス)。役に立ちそうと思ったら、スターを付けてもらえるとうれしいです。

→ github.com/istb16/dev-orchestra

手順

1. 計画に ## Units を書く

題材は、textkit という小さな Python のパッケージに、slugify(text) と word_count(text) の 2 つの関数を足す計画です。2 つは別々のファイルなので並べて作れます。README と CHANGELOG は両方の結果を書くので、最後にまとめて直します。

計画(plan.md)に、## Units の見出しと、yaml のコードブロックを書きます。

## 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.
  • name、kind(feature、fix、refactor、docs、test、ci、integration)、files(変えるファイル)、depends_on(先に終わっている必要がある作業単位)を書く
  • 作業単位ごとに ### Unit: <name> の節を作り、その作業単位の手順を書く
  • CHANGELOG のように全員が触るファイルは、ほかのすべてに依存する integration の作業単位 1 つにまとめる

実際には architect が計画を書くときに、このブロックも書きます(ファイルが分かれる作業か、手順が 4 つを超える計画のとき)。今回は手で書きました。

## Files to Modify には、パスを `textkit/slug.py` のようにバッククォートで囲んで並べます。囲まずに書いたら、units check が「Files to Modify にない」という warning: を出しました(拒否はされません)。

2. units check で走らせられるか確かめる

最初、README を slug と count の両方の files に入れていました。この状態で units check を実行すると、拒否されます。

$ 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

終了コードは 2 です。並んで走りうる 2 つの作業単位が同じファイルを挙げると、マージでぶつかるので、走らせる前に止めてくれます。README を docs の作業単位だけに移すと通りました。

$ dev-orchestra units check
3 units in 2 waves (implementation.max_parallel: 2):
  wave 1: slug, count
  wave 2: docs

slug と count が 1 つ目の波で並んで走り、docs が 2 つ目の波で走る、という予定です。同時に走らせる数は implementation.max_parallel(既定 2、最大 8)で決まります。

3. 作業単位のテストをマージ後に回す設定にする

今回の環境では、implementer の Claude がシェルのコマンドを実行しようとすると承認待ちになり、テストを流せませんでした(dev-orchestra は Claude の implementer を --permission-mode acceptEdits で起動するので、ファイルの編集は通りますが、シェルのコマンドは承認が要ります)。

そこで、1.2.1 で入った implementation.unit_tests: after_merge をリポジトリの .dev-orchestra.yaml に書きました。

version: 1
implementation:
  unit_tests: after_merge

こうすると、各作業単位はコードとテストを書くだけにし、テストはすべてをマージしたあとに 1 回だけ流します。テスト用のデータベースやポートを共有していて、並べて流すとぶつかるプロジェクト向けの設定です。

最初はこの設定なしで走らせました。すると片方の作業単位は、ツールの記録に Write が 1 度もなく、ファイルを書いていなかったのに、報告では「ファイルを作った」と書いていました。units merge は merged slug: 0 file(s) と出したので、マージしたファイルの数を見れば気づけます。

4. 計画を承認して、2 つの作業単位を並べて走らせる

implementer は、承認された計画でないと走りません。design approve で承認を記録します(ふだんはオーケストレーターがあなたに計画を見せ、あなたが「はい」と言ったときだけ実行します)。

$ dev-orchestra design approve
approved .ai/workflows/units/plan.md (sha256 f108913500da) for workflow units

run implementer --unit <name> --detach で、作業単位を 1 つずつ裏で走らせます。依頼のファイル(implement-request.md)は全作業単位で 1 つで、作業単位の名前・ファイル・計画の節は CLI が前に付けます。

$ 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 を見ると、2 つが running で、それぞれ別の worktree にいます。worktree は、リポジトリの隣の textkit.units フォルダに作られていました(パスは一部省略しています)。

$ 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)

作業単位に渡ったプロンプトの先頭には、こんなヘッダーが付いていました(最初の試行のもの)。

# 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. jobs wait --any で、終わったものから受け取る

1.2.0 から、jobs wait に複数のジョブ id を渡せます。--any を付けると、どれか 1 つが終わった時点で戻ります(出力は抜粋)。

$ 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 が先に終わり、slug はまだ走っています。2 つとも 30 秒前後で終わる小さな作業でした。

6. units merge で作業ツリーに戻す

終わった作業単位から、units merge で作業ツリーに戻します。ファイルごとのマージで、index、HEAD、ブランチには触りません。

$ dev-orchestra units merge count
merged count: 2 file(s)

slug も終わるのを待ってマージしました。

$ 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

マージした作業単位の worktree は片付けられています。作業ツリーには、4 つのファイルが追加された状態になりました。

$ git status --short
?? tests/test_count.py
?? tests/test_slug.py
?? textkit/count.py
?? textkit/slug.py

7. 統合の作業単位は、その場で走る

残った docs は、ほかに並んで走るものがありません。この場合、worktree を作らず、作業ツリーでそのまま走ります(--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

その場で走った作業単位は、終わった時点でマージ済みです。README には 2 つの関数の説明が、CHANGELOG には ### Added が足されていました。

8. マージしたツリーでテストを流す

after_merge なので、最後にテストを 1 回だけ流します。

$ python -m unittest
.........
----------------------------------------------------------------------
Ran 9 tests in 0.000s

OK

budget show を見ると、3 つの作業単位を走らせても、implementer の試行は 1 回しか使っていません(出力は抜粋)。

$ dev-orchestra budget show
  implementer    1/5 used
  delegated runs 3/40 used
  implementer    pass open: slug, count, docs run (plan f108913500da)

1 つの計画の作業単位は、試行を 1 回分け合います。同じ作業単位をやり直したときだけ、もう 1 回使います。実行の回数そのもの(delegated runs)は、3 回と数えられています。

9. 競合したとき(今回は起きなかった)

今回はマージで競合しませんでした。リファレンスによると、作業単位が走っている間に作業ツリーの同じファイルが変わっていた場合などに競合し、こうなります。

  • units merge が終了コード 7 で終わり、.ai/execution/units/<name>-conflicts.md に指示書を残す
  • オーケストレーターがその指示書で review_fixer に競合を解かせる
  • units merge <name> --resolved で解決済みとして記録する

競合は試していないので、ここでは仕組みの紹介だけにします。

まとめ

  • 計画に ## Units の yaml を書くと、作業単位に分けて実装できる
  • units check が、ファイルが重なる作業単位を走らせる前に拒否し、波を表示する
  • 依存しない作業単位は 別々の git worktree で並べて走り、units merge でファイルごとに作業ツリーへ戻る。index やブランチには触らない
  • ほかと並ばない作業単位(今回の integration)は、その場で走る
  • jobs wait --any で、終わったものから受け取れる
  • 1 つの計画の作業単位は、implementer の試行を 1 回分け合う
  • テストを並べて流せないなら implementation.unit_tests: after_merge。units merge が出すファイルの数も見ておく

役割ごとに決まった指示を渡す機能は、役割ごとの指示の記事で試しています。

GitHub: istb16/dev-orchestra(スターを付けてもらえると励みになります)

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

dev-orchestra で役割ごとに決まった指示を渡してみた Giving dev-orchestra Standing Instructions per Role

dev-orchestra 1.2.1 で入った instructions を試しました。implementer やレビュアーへの決まった指示をリポジトリに書いておくと、毎回のプロンプトに入ります。どう表示され、どこに入り、指示を変える変更がどう扱われるかを、実際の出力で確かめます。 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: 作業単位の並列実行と役割ごとの指示を追加 v1.1.0 - v1.2.1 [Released] dev-orchestra: Parallel Work Units and Per-Role Instructions v1.1.0 - v1.2.1

dev-orchestra の v1.1.0 から v1.2.1 までの変更をまとめました。計画を作業単位に分けて並べて走らせる機能、役割ごとに決まった指示を渡す機能、設計の前の質問、進み具合の報告の設定が入りました。Windows で出力を取り込んでいる人は、文字コードの変更に注意してください。 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.

dev-orchestra をアップデートしてみた(Claude Code / Codex / Antigravity) Updating dev-orchestra (Claude Code / Codex / Antigravity)

dev-orchestra を 1.0.0 から 1.1.1 に、Claude Code・Codex・Antigravity のそれぞれで実際に更新してみました。Claude Code は claude plugin update だけでは更新されず、先にマーケットプレイスの更新が要りました。今の版の確かめ方、CLI ごとの手順、更新したあとに確かめることをまとめます。 I actually updated dev-orchestra from 1.0.0 to 1.1.1 in each of Claude Code, Codex, and Antigravity. In Claude Code, claude plugin update alone did nothing; the marketplace had to be refreshed first. I cover how to check your version, the steps for each CLI, and what to check after updating.