claude-code-conductor 0.2.4__tar.gz → 0.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. claude_code_conductor-0.3.0/.claude/commands/develop.md +11 -0
  2. {claude_code_conductor-0.2.4 → claude_code_conductor-0.3.0}/.claude/skills/dev-workflow.md +13 -23
  3. claude_code_conductor-0.3.0/.claude/skills/wave-execution.md +220 -0
  4. {claude_code_conductor-0.2.4 → claude_code_conductor-0.3.0}/CHANGELOG.md +42 -0
  5. {claude_code_conductor-0.2.4 → claude_code_conductor-0.3.0}/PKG-INFO +1 -1
  6. {claude_code_conductor-0.2.4 → claude_code_conductor-0.3.0}/src/c3/__init__.py +1 -1
  7. claude_code_conductor-0.3.0/src/c3/cli_po.py +208 -0
  8. {claude_code_conductor-0.2.4 → claude_code_conductor-0.3.0}/src/c3/po/manifest.py +146 -0
  9. claude_code_conductor-0.2.4/.claude/commands/develop.md +0 -10
  10. claude_code_conductor-0.2.4/.claude/skills/parallel-execution.md +0 -121
  11. claude_code_conductor-0.2.4/src/c3/cli_po.py +0 -102
  12. {claude_code_conductor-0.2.4 → claude_code_conductor-0.3.0}/.claude/CLAUDE.md +0 -0
  13. {claude_code_conductor-0.2.4 → claude_code_conductor-0.3.0}/.claude/agents/architect.md +0 -0
  14. {claude_code_conductor-0.2.4 → claude_code_conductor-0.3.0}/.claude/agents/code-reviewer.md +0 -0
  15. {claude_code_conductor-0.2.4 → claude_code_conductor-0.3.0}/.claude/agents/developer.md +0 -0
  16. {claude_code_conductor-0.2.4 → claude_code_conductor-0.3.0}/.claude/agents/doc-writer.md +0 -0
  17. {claude_code_conductor-0.2.4 → claude_code_conductor-0.3.0}/.claude/agents/interviewer.md +0 -0
  18. {claude_code_conductor-0.2.4 → claude_code_conductor-0.3.0}/.claude/agents/planner.md +0 -0
  19. {claude_code_conductor-0.2.4 → claude_code_conductor-0.3.0}/.claude/agents/project-setup.md +0 -0
  20. {claude_code_conductor-0.2.4 → claude_code_conductor-0.3.0}/.claude/agents/security-reviewer.md +0 -0
  21. {claude_code_conductor-0.2.4 → claude_code_conductor-0.3.0}/.claude/agents/tdd-develop.md +0 -0
  22. {claude_code_conductor-0.2.4 → claude_code_conductor-0.3.0}/.claude/agents/tester.md +0 -0
  23. {claude_code_conductor-0.2.4 → claude_code_conductor-0.3.0}/.claude/commands/doc.md +0 -0
  24. {claude_code_conductor-0.2.4 → claude_code_conductor-0.3.0}/.claude/commands/extract-lib.md +0 -0
  25. {claude_code_conductor-0.2.4 → claude_code_conductor-0.3.0}/.claude/commands/init-session.md +0 -0
  26. {claude_code_conductor-0.2.4 → claude_code_conductor-0.3.0}/.claude/commands/mcp.md +0 -0
  27. {claude_code_conductor-0.2.4 → claude_code_conductor-0.3.0}/.claude/commands/promote-pattern.md +0 -0
  28. {claude_code_conductor-0.2.4 → claude_code_conductor-0.3.0}/.claude/commands/review.md +0 -0
  29. {claude_code_conductor-0.2.4 → claude_code_conductor-0.3.0}/.claude/commands/setup.md +0 -0
  30. {claude_code_conductor-0.2.4 → claude_code_conductor-0.3.0}/.claude/commands/start.md +0 -0
  31. {claude_code_conductor-0.2.4 → claude_code_conductor-0.3.0}/.claude/docs/parallel-orchestra-manifest.md +0 -0
  32. {claude_code_conductor-0.2.4 → claude_code_conductor-0.3.0}/.claude/hooks/clear_file_history.py +0 -0
  33. {claude_code_conductor-0.2.4 → claude_code_conductor-0.3.0}/.claude/hooks/enable_sandbox.py +0 -0
  34. {claude_code_conductor-0.2.4 → claude_code_conductor-0.3.0}/.claude/hooks/pre_compact.py +0 -0
  35. {claude_code_conductor-0.2.4 → claude_code_conductor-0.3.0}/.claude/hooks/pre_tool.py +0 -0
  36. {claude_code_conductor-0.2.4 → claude_code_conductor-0.3.0}/.claude/hooks/statusline.py +0 -0
  37. {claude_code_conductor-0.2.4 → claude_code_conductor-0.3.0}/.claude/hooks/stop.py +0 -0
  38. {claude_code_conductor-0.2.4 → claude_code_conductor-0.3.0}/.claude/hooks/validate_skill_change.py +0 -0
  39. {claude_code_conductor-0.2.4 → claude_code_conductor-0.3.0}/.claude/hooks/worktree_guard.py +0 -0
  40. {claude_code_conductor-0.2.4 → claude_code_conductor-0.3.0}/.claude/memory/.gitkeep +0 -0
  41. {claude_code_conductor-0.2.4 → claude_code_conductor-0.3.0}/.claude/rules/code-review-checklist.md +0 -0
  42. {claude_code_conductor-0.2.4 → claude_code_conductor-0.3.0}/.claude/rules/promoted/index.md +0 -0
  43. {claude_code_conductor-0.2.4 → claude_code_conductor-0.3.0}/.claude/rules/security-review-checklist.md +0 -0
  44. {claude_code_conductor-0.2.4 → claude_code_conductor-0.3.0}/.claude/settings.json +0 -0
  45. {claude_code_conductor-0.2.4 → claude_code_conductor-0.3.0}/.claude/settings.local.json +0 -0
  46. {claude_code_conductor-0.2.4 → claude_code_conductor-0.3.0}/.claude/skills/promoted/index.md +0 -0
  47. {claude_code_conductor-0.2.4 → claude_code_conductor-0.3.0}/.claude/skills/worktree-tdd-workflow.md +0 -0
  48. {claude_code_conductor-0.2.4 → claude_code_conductor-0.3.0}/.gitignore +0 -0
  49. {claude_code_conductor-0.2.4 → claude_code_conductor-0.3.0}/LICENSE +0 -0
  50. {claude_code_conductor-0.2.4 → claude_code_conductor-0.3.0}/README.md +0 -0
  51. {claude_code_conductor-0.2.4 → claude_code_conductor-0.3.0}/hatch_build.py +0 -0
  52. {claude_code_conductor-0.2.4 → claude_code_conductor-0.3.0}/pyproject.toml +0 -0
  53. {claude_code_conductor-0.2.4 → claude_code_conductor-0.3.0}/src/c3/__main__.py +0 -0
  54. {claude_code_conductor-0.2.4 → claude_code_conductor-0.3.0}/src/c3/_excludes.py +0 -0
  55. {claude_code_conductor-0.2.4 → claude_code_conductor-0.3.0}/src/c3/cli.py +0 -0
  56. {claude_code_conductor-0.2.4 → claude_code_conductor-0.3.0}/src/c3/cli_doctor.py +0 -0
  57. {claude_code_conductor-0.2.4 → claude_code_conductor-0.3.0}/src/c3/cli_init.py +0 -0
  58. {claude_code_conductor-0.2.4 → claude_code_conductor-0.3.0}/src/c3/cli_list.py +0 -0
  59. {claude_code_conductor-0.2.4 → claude_code_conductor-0.3.0}/src/c3/cli_update.py +0 -0
  60. {claude_code_conductor-0.2.4 → claude_code_conductor-0.3.0}/src/c3/paths.py +0 -0
  61. {claude_code_conductor-0.2.4 → claude_code_conductor-0.3.0}/src/c3/po/__init__.py +0 -0
  62. {claude_code_conductor-0.2.4 → claude_code_conductor-0.3.0}/src/c3/po/detect.py +0 -0
  63. {claude_code_conductor-0.2.4 → claude_code_conductor-0.3.0}/src/c3/po/run.py +0 -0
@@ -0,0 +1,11 @@
1
+ # /develop コマンド
2
+
3
+ plan-report に基づいて実装フェーズを実行する。
4
+
5
+ ## 必ず守ること
6
+
7
+ 1. **最初に必ず** `.claude/skills/dev-workflow.md` を Read する。記憶・推測で進めない
8
+ 2. **フェーズ D(実装)** から実行する
9
+ 3. dev-workflow.md の AskUserQuestion・Edit・セッションファイル更新の手順を省略しない
10
+ 4. D-0 で plan-report に YAML フロントマター(`po_plan_version`)が検出された場合は、続けて **必ず** `.claude/skills/wave-execution.md` を Read してその手順に従う(C3 メイン + PO スポット並列モード)
11
+ 5. フロントマターが無い場合は legacy の D-1〜D-5 ceremony(tester→developer→tester の TDD 逐次実行)にフォールバックする
@@ -217,39 +217,29 @@ AskUserQuestion ツール:
217
217
 
218
218
  ---
219
219
 
220
- ## フェーズ D: TDD
220
+ ## フェーズ D: 実装
221
221
 
222
222
  **フェーズ C から続いている場合:** plan-report はコンテキスト内にあるため読み直し不要。
223
223
  **直接開始の場合:** Glob で `.claude/reports/plan-report-*.md` の最新を Read する。存在しない場合はフェーズ C から始めるよう案内して終了する。
224
224
 
225
+ ### D-0: 実行モード自動判別
226
+
227
+ plan-report の冒頭を Read し、YAML フロントマター(`---` で始まり `po_plan_version: "0.1"` を含む)の有無を確認する。
228
+
229
+ **フロントマターありの場合:**
230
+ 1. **最初に必ず** `.claude/skills/wave-execution.md` を Read する(記憶・推測で進めない)
231
+ 2. `wave-execution.md` の手順に完全に従って wave 単位で実装を進める
232
+ 3. 全 wave 完了後はフェーズ E(レビュー)へ進む(wave に reviewer タスクが含まれていれば E をスキップ可能と案内する)
233
+
234
+ **フロントマターなしの場合(legacy フォールバック):**
235
+
225
236
  今日のセッションファイルに以下を追記する(未登録の場合のみ):
226
237
  - `- [ ] tester: Red フェーズ`
227
238
  - `- [ ] developer: Green フェーズ`
228
239
  - `- [ ] developer: Refactor フェーズ`
229
240
  - `- [ ] tester: 最終確認`
230
241
 
231
- ### D-0: 実行モード選択
232
-
233
- AskUserQuestion ツール:
234
- ```json
235
- {
236
- "questions": [{
237
- "question": "TDD サイクルの実行モードを選んでください",
238
- "options": [
239
- { "label": "TDD 逐次実行", "description": "従来の D-1〜D-5(tester→developer→tester)で1タスクずつ進める" },
240
- { "label": "PO 並列実行", "description": "parallel-orchestra で plan-report の独立タスクを git worktree 並列実行する(PO のインストールが必要)" }
241
- ]
242
- }]
243
- }
244
- ```
245
-
246
- **「TDD 逐次実行」の場合:** D-1 へ進む。
247
-
248
- **「PO 並列実行」の場合:**
249
- 1. **最初に必ず** `.claude/skills/parallel-execution.md` を Read する(記憶・推測で進めない)
250
- 2. セッションファイルに `- [ ] PO 並列実行` を追記する
251
- 3. `parallel-execution.md` の手順を完全に守る
252
- 4. 完了後はフェーズ E(レビュー)へ進む
242
+ D-1 へ進む。
253
243
 
254
244
  ### D-1: tester(Red フェーズ)
255
245
 
@@ -0,0 +1,220 @@
1
+ # Wave Execution
2
+
3
+ `/develop` のフェーズ D で plan-report に YAML フロントマター(`po_plan_version: "0.1"`)が含まれるときに Read される手順。
4
+
5
+ C3 の親 Claude が plan-report の DAG を **wave 単位**で歩く。
6
+ - 単独 wave(タスク 1 件) → C3 が直接実行(Agent ツール起動 or ペルソナ採用)
7
+ - 複数 wave(タスク 2 件以上) → parallel-orchestra(PO)にスポット並列委譲
8
+
9
+ 各 wave 完了後にユーザーの承認を取る。
10
+
11
+ ---
12
+
13
+ ## 前提条件
14
+
15
+ - `claude-code-conductor` がインストール済みで `c3` コマンドが PATH 上にあること
16
+ - plan-report が `.claude/reports/plan-report-*.md` の形式で配置され、YAML フロントマターを持つこと(フロントマターが無ければ `dev-workflow.md` の D-1〜D-5 ceremony へフォールバック)
17
+
18
+ ---
19
+
20
+ ## Step 0: 妥当性チェック
21
+
22
+ 1. Glob で `.claude/reports/plan-report-*.md` の最新ファイルパスを取得する
23
+ 2. Read で内容(フロントマター含む)を確認する
24
+ 3. Bash で以下を実行する:
25
+
26
+ ```
27
+ c3 po dry-run <plan-report-path>
28
+ ```
29
+
30
+ - 終了コード `0` = マニフェスト妥当 → Step 1 へ
31
+ - 終了コード `2` = マニフェストエラー(フィールド欠損・agent 不在等)→ stderr のメッセージを整形してユーザーに提示し、`/start` のフェーズ C(計画)を再実行するか手動で plan-report を編集するよう案内してこのスキルを終了する
32
+ - 終了コード `1` = PO 未インストール → 以下の案内文を**そのまま提示**してこのスキルを終了する:
33
+
34
+ > 並列実行を使うには `pip install parallel-orchestra` を実行してください。
35
+ > 詳細: https://pypi.org/project/parallel-orchestra/
36
+
37
+ 親 Claude は逐次実行(`dev-workflow.md` の D-1〜D-5)に切り替えるか、PO をインストールして `/develop` を再実行するかをユーザーに選んでもらう。
38
+
39
+ ---
40
+
41
+ ## Step 1: wave 分解
42
+
43
+ Bash で以下を実行する:
44
+
45
+ ```
46
+ c3 po waves <plan-report-path>
47
+ ```
48
+
49
+ stdout の JSON は以下の形式になる:
50
+
51
+ ```json
52
+ {
53
+ "waves": [
54
+ {
55
+ "index": 0,
56
+ "tasks": [
57
+ { "id": "tdd-login", "agent": "tdd-develop", "read_only": false, "writes": ["src/auth/login.py"], "prompt": "..." },
58
+ { "id": "tdd-logout", "agent": "tdd-develop", "read_only": false, "writes": ["src/auth/logout.py"], "prompt": "..." }
59
+ ]
60
+ },
61
+ {
62
+ "index": 1,
63
+ "tasks": [
64
+ { "id": "review-auth", "agent": "code-reviewer", "read_only": true, "writes": [], "prompt": "..." }
65
+ ]
66
+ }
67
+ ]
68
+ }
69
+ ```
70
+
71
+ - 終了コード `0` = 分解成功 → 親 Claude が JSON をパースして `waves` 配列を取得し Step 2 へ
72
+ - 終了コード `2` = フロントマター不正・循環依存等 → stderr メッセージを提示して終了
73
+
74
+ セッションファイル(`.claude/memory/sessions/YYYYMMDD.tmp`)に以下を追記する(未登録の場合のみ):
75
+ - 各 wave につき `- [ ] Wave {N} ({task_count} tasks)` を 1 行ずつ
76
+
77
+ ---
78
+
79
+ ## Step 2: wave ごとに実行する
80
+
81
+ `waves` 配列を index 順にループする。各 wave で以下を順に行う。
82
+
83
+ ### 2-A: wave 内容を提示する
84
+
85
+ 親 Claude が wave のタスク一覧を Markdown 表で提示する:
86
+
87
+ | id | agent | read_only | writes |
88
+ |---|---|---|---|
89
+ | tdd-login | tdd-develop | false | src/auth/login.py |
90
+ | tdd-logout | tdd-develop | false | src/auth/logout.py |
91
+
92
+ ### 2-B: 実行可否をユーザーに確認する
93
+
94
+ AskUserQuestion ツール:
95
+
96
+ ```json
97
+ {
98
+ "questions": [{
99
+ "question": "Wave {N} を実行してよいですか?",
100
+ "options": [
101
+ { "label": "承認・進む", "description": "この wave を実行する" },
102
+ { "label": "中断", "description": "ここで wave 実行を停止する。完了済みの wave はそのまま残る" }
103
+ ]
104
+ }]
105
+ }
106
+ ```
107
+
108
+ 「中断」の場合、セッションファイルの `## 試みたが失敗したアプローチ` に中断理由を追記してスキル終了。
109
+
110
+ ### 2-C: ランナーを選んで実行する
111
+
112
+ `len(wave.tasks)` で分岐する。
113
+
114
+ #### case A: 単独 wave(タスク 1 件)
115
+
116
+ タスクの `agent` フィールドで更に分岐する。
117
+
118
+ ##### A-1: `agent == "tdd-develop"` のとき
119
+
120
+ **ペルソナ採用パターン**で実行する。Agent ツールで起動するとサブエージェント depth 1 制限により内部の tester/developer サブエージェントが spawn できないため、親 Claude が直接 tdd-develop ペルソナで動く。
121
+
122
+ 1. `.claude/agents/tdd-develop.md` を Read してペルソナを採用する
123
+ 2. `.claude/skills/worktree-tdd-workflow.md` を Read して TDD ループ手順(tester→developer→tester)を取得する
124
+ 3. タスクの `prompt` を実装内容として、worktree-tdd-workflow.md の Step 1〜4 を **親 Claude が直接** 実行する。tester / developer は Agent ツールでスポーン可能(depth 1 で完結する)
125
+ 4. ループ完了後、結果を 2-D の集約に渡す
126
+
127
+ ##### A-2: `agent` がそれ以外(`code-reviewer` / `security-reviewer` / `developer` / `tester` 等)
128
+
129
+ **Agent ツール起動パターン**で実行する。これらの agent は内部で更に subagent を spawn しないため depth 1 制限に抵触しない。
130
+
131
+ 1. Agent ツールで `subagent_type` は指定せず(カスタム agent は subagent_type 不可)、プロンプトに以下を含める:
132
+ - 「`.claude/agents/{agent_name}.md` を Read してペルソナを採用すること」
133
+ - タスクの `prompt` 本文
134
+ 2. Agent の返答を 2-D の集約に渡す
135
+
136
+ ##### 将来 agent を追加する場合
137
+
138
+ 「内部で更に subagent を呼ぶ agent」を新たに作る場合は、A-1 のペルソナ採用パターン側に追加する。判断基準: その agent の定義ファイルが「内部で Agent ツールを使う」前提になっているか。tdd-develop が現状の唯一の例。
139
+
140
+ #### case B: 複数 wave(タスク 2 件以上)
141
+
142
+ **PO スポット委譲**で実行する。
143
+
144
+ Bash で以下を実行する:
145
+
146
+ ```
147
+ c3 po run-wave <plan-report-path> --wave-index {N} --report .claude/reports/po-run-report-wave-{N}-{ts}.json
148
+ ```
149
+
150
+ - `{N}` は wave の index
151
+ - `{ts}` は `YYYYMMDD-HHMMSS` 形式。Bash で `python -c "from datetime import datetime; print(datetime.now().strftime('%Y%m%d-%H%M%S'))"` を実行して取得
152
+ - `c3 po run-wave` は `.claude/tmp/po-manifest-wave-{N}-{ts}.md` に ephemeral マニフェストを生成し、PO に subprocess 委譲する。各タスクは PO が `claude -p --agent <name>` を独立 Claude セッション(depth 0)として起動するため、tdd-develop も内部で tester/developer を spawn できる
153
+
154
+ 終了コードと意味:
155
+
156
+ | exit code | 意味 | 次のアクション |
157
+ |---|---|---|
158
+ | `0` | wave 内全タスク成功 | 2-D へ |
159
+ | `1` | 1件以上のタスクが失敗 | 2-D へ(失敗一覧を含めて提示) |
160
+ | `2` | マニフェストエラー(Step 0 をすり抜けた)| エラー内容を提示しスキル終了 |
161
+ | `3` | runner エラー(claude バイナリ不在等)| `c3 doctor` の結果を提示してスキル終了 |
162
+
163
+ 実行完了後、生成された `po-run-report-wave-{N}-{ts}.json` を Read してタスクごとのステータスを取得する。
164
+
165
+ ### 2-D: 結果を集約してユーザーに提示する
166
+
167
+ ランナー種別ごとに以下の形式で結果を提示する。
168
+
169
+ **case A(単独 wave):**
170
+ - A-1: 親 Claude の TDD ループ結果(tester/developer の Agent 返答の要約、最終 tester の合否)
171
+ - A-2: Agent の返答テキスト(必要なら要約)
172
+
173
+ **case B(複数 wave / PO 委譲):**
174
+ - `po-run-report-wave-{N}-*.json` を Markdown 表に整形:
175
+
176
+ | id | agent | status | worktree | duration | last_error_summary |
177
+ |---|---|---|---|---|---|
178
+ | tdd-login | tdd-develop | success | wt-tdd-login | 124s | - |
179
+ | tdd-logout | tdd-develop | failure | wt-tdd-logout | 89s | TestLogout::test_csrf failed |
180
+
181
+ ### 2-E: 失敗があったら方針を確認する
182
+
183
+ case A で TDD ループが不合格のまま終わった、または case B で 1 件以上失敗した場合、AskUserQuestion で次を確認する:
184
+
185
+ ```json
186
+ {
187
+ "questions": [{
188
+ "question": "Wave {N} に失敗があります。どうしますか?",
189
+ "options": [
190
+ { "label": "リトライ", "description": "同じ wave をもう一度実行する" },
191
+ { "label": "スキップして次の wave へ", "description": "失敗を残したまま次の wave に進む" },
192
+ { "label": "中断", "description": "ここで wave 実行を停止する。完了済みの wave はそのまま残る" }
193
+ ]
194
+ }]
195
+ }
196
+ ```
197
+
198
+ - 「リトライ」 → 2-A から同じ wave を再実行
199
+ - 「スキップして次の wave へ」 → 失敗内容をセッションファイルの `## 試みたが失敗したアプローチ` に追記して次の wave へ
200
+ - 「中断」 → セッションファイルの該当 wave 行は `- [ ]` のままにしてスキル終了
201
+
202
+ ### 2-F: wave 完了をセッションに記録する
203
+
204
+ 全タスク成功した wave のみ、セッションファイルの `- [ ] Wave {N} (M tasks)` を `- [x] Wave {N} (M tasks)` に Edit する。
205
+
206
+ ---
207
+
208
+ ## Step 3: フェーズ E への遷移
209
+
210
+ 全 wave 完了後、フェーズ E(レビュー)への遷移を案内する。
211
+
212
+ - plan-report に reviewer タスク(agent が `code-reviewer` / `security-reviewer`)が含まれている場合は wave で既に実行済みなので、二重レビューを避けるためにユーザーへ「wave 内で reviewer タスクが完了済みなので E をスキップしてもよい」と提示する
213
+ - reviewer タスクが含まれていない場合は通常通りフェーズ E へ進む
214
+
215
+ ---
216
+
217
+ ## 知識蓄積
218
+
219
+ - wave 実行で**特定パターンが詰まりがち**(例: tdd-develop の persona 起動で context が膨らみやすい)と気付いたら、セッションファイルの `## 試みたが失敗したアプローチ` に追記し `patterns` に登録する
220
+ - wave 分解の精度(planner の出力品質)に関する観察も同様に記録する。これは将来 planner 側のルール改善に繋がる
@@ -1,5 +1,47 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.3.0] - 2026-04-30
4
+
5
+ ### Changed (breaking)
6
+ - `/develop` now auto-detects YAML frontmatter on the latest plan-report and
7
+ switches between two modes:
8
+ - **frontmatter present** → new "C3 main + PO spot" workflow. C3 walks the
9
+ DAG wave-by-wave, asks for user approval before each wave, and dispatches
10
+ each wave to the right runner: solo waves run on the C3 host (Agent-tool
11
+ spawn for `code-reviewer` / `developer` / `tester`, parent-Claude persona
12
+ adoption for `tdd-develop` to avoid the depth-1 nested-spawn limit), and
13
+ multi-task waves are delegated to parallel-orchestra via an ephemeral
14
+ wave-only manifest under `.claude/tmp/`.
15
+ - **no frontmatter** → legacy D-1〜D-5 sequential TDD ceremony, unchanged.
16
+ - The previous "PO 全委譲" model (D-0 two-choice prompt) and
17
+ `.claude/skills/parallel-execution.md` are removed. The new flow is
18
+ documented in `.claude/skills/wave-execution.md`.
19
+
20
+ ### Added
21
+ - `c3 po waves <plan-report>` — prints the topological wave decomposition of
22
+ a manifest as JSON. Used by `wave-execution.md` to drive the per-wave loop.
23
+ - `c3 po run-wave <plan-report> --wave-index N` — generates a wave-only
24
+ ephemeral manifest under `.claude/tmp/po-manifest-wave-{N}-{ts}.md` and
25
+ hands it to parallel-orchestra.
26
+ - `c3.po.manifest.compute_waves(frontmatter)` — Kahn's-algorithm topological
27
+ wave decomposition. Detects cycles, unknown dependency ids, and duplicate
28
+ task ids.
29
+ - `c3.po.manifest.build_wave_manifest_text(frontmatter, wave_index)` —
30
+ emits a parseable plan-report Markdown for one wave, dropping `depends_on`
31
+ and webhook fields and decorating the manifest name with ` - wave N`.
32
+ - `tests/test_po_waves.py` (16 tests) and `tests/test_cli_po.py` (5 tests)
33
+ covering wave decomposition, ephemeral-manifest generation, CLI exit
34
+ codes, and frontmatter round-trip.
35
+
36
+ ### Notes
37
+ - The persona-adoption pattern for `tdd-develop` in solo waves is the direct
38
+ consequence of Claude Code's depth-1 nested-spawn limit (a sub-agent
39
+ spawned via the Agent tool cannot itself spawn another sub-agent). For
40
+ agents that internally spawn sub-agents (today: `tdd-develop`), the parent
41
+ Claude reads the agent definition and adopts its persona instead. Other
42
+ agents (`code-reviewer`, `security-reviewer`, `developer`, `tester`) keep
43
+ using the standard Agent-tool spawn path.
44
+
3
45
  ## [0.2.4] - 2026-05-01
4
46
 
5
47
  ### Changed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: claude-code-conductor
3
- Version: 0.2.4
3
+ Version: 0.3.0
4
4
  Summary: Multi-agent orchestration framework for Claude Code (C3)
5
5
  Project-URL: Homepage, https://github.com/satoh-y-0323/claude-code-conductor
6
6
  Project-URL: Repository, https://github.com/satoh-y-0323/claude-code-conductor
@@ -1,3 +1,3 @@
1
1
  """Claude Code Conductor (C3) - multi-agent orchestration framework for Claude Code."""
2
2
 
3
- __version__ = "0.2.4"
3
+ __version__ = "0.3.0"
@@ -0,0 +1,208 @@
1
+ """``c3 po`` - thin wrapper around the parallel-orchestra CLI."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import json
7
+ import sys
8
+ from datetime import datetime
9
+ from pathlib import Path
10
+
11
+ from c3.paths import claude_root_for
12
+ from c3.po.detect import detect_po
13
+ from c3.po.manifest import (
14
+ build_wave_manifest_text,
15
+ compute_waves,
16
+ extract_frontmatter,
17
+ validate_manifest,
18
+ )
19
+ from c3.po.run import run_manifest
20
+
21
+
22
+ _NOT_INSTALLED_MSG = (
23
+ "parallel-orchestra is not installed. "
24
+ "並列実行を使うには `pip install parallel-orchestra` を実行してください。"
25
+ "詳細: https://pypi.org/project/parallel-orchestra/"
26
+ )
27
+
28
+
29
+ def register(subparsers: argparse._SubParsersAction) -> None:
30
+ parser = subparsers.add_parser(
31
+ "po",
32
+ help="Run a plan-report as a parallel-orchestra manifest",
33
+ )
34
+ inner = parser.add_subparsers(dest="po_command", metavar="<subcommand>")
35
+ inner.required = True
36
+
37
+ dry = inner.add_parser(
38
+ "dry-run",
39
+ help="Validate the manifest without executing tasks",
40
+ )
41
+ dry.add_argument("manifest", type=Path)
42
+ dry.set_defaults(handler=_handle_dry_run)
43
+
44
+ run = inner.add_parser(
45
+ "run",
46
+ help="Execute the full manifest in one PO invocation",
47
+ )
48
+ run.add_argument("manifest", type=Path)
49
+ run.add_argument("--max-workers", type=int, default=None)
50
+ run.add_argument("--report", type=Path, default=None)
51
+ run.add_argument("--quiet", action="store_true")
52
+ run.add_argument("--claude-exe", default=None)
53
+ run.set_defaults(handler=_handle_run)
54
+
55
+ waves = inner.add_parser(
56
+ "waves",
57
+ help="Print the topological-wave decomposition of the manifest as JSON",
58
+ )
59
+ waves.add_argument("manifest", type=Path)
60
+ waves.set_defaults(handler=_handle_waves)
61
+
62
+ run_wave = inner.add_parser(
63
+ "run-wave",
64
+ help="Execute only the wave at the given index via PO",
65
+ )
66
+ run_wave.add_argument("manifest", type=Path)
67
+ run_wave.add_argument("--wave-index", type=int, required=True)
68
+ run_wave.add_argument("--max-workers", type=int, default=None)
69
+ run_wave.add_argument("--report", type=Path, default=None)
70
+ run_wave.add_argument("--quiet", action="store_true")
71
+ run_wave.add_argument("--claude-exe", default=None)
72
+ run_wave.set_defaults(handler=_handle_run_wave)
73
+
74
+
75
+ def _ensure_po_available() -> int:
76
+ available, _, _ = detect_po()
77
+ if not available:
78
+ print(_NOT_INSTALLED_MSG, file=sys.stderr)
79
+ return 1
80
+ return 0
81
+
82
+
83
+ def _preflight(manifest: Path) -> int:
84
+ if not manifest.is_file():
85
+ print(f"manifest not found: {manifest}", file=sys.stderr)
86
+ return 2
87
+ root = claude_root_for(manifest.parent) or claude_root_for(Path.cwd())
88
+ if root is None:
89
+ print("could not locate .claude/ directory for agent lookup", file=sys.stderr)
90
+ return 2
91
+ errors = validate_manifest(manifest, root)
92
+ if errors:
93
+ for err in errors:
94
+ print(err, file=sys.stderr)
95
+ return 2
96
+ return 0
97
+
98
+
99
+ def _handle_dry_run(args: argparse.Namespace) -> int:
100
+ rc = _preflight(args.manifest)
101
+ if rc != 0:
102
+ return rc
103
+ if (rc := _ensure_po_available()) != 0:
104
+ return rc
105
+ result = run_manifest(args.manifest, dry_run=True)
106
+ if result.status == "not_installed":
107
+ print(_NOT_INSTALLED_MSG, file=sys.stderr)
108
+ return 1
109
+ return result.exit_code if result.exit_code >= 0 else 1
110
+
111
+
112
+ def _handle_run(args: argparse.Namespace) -> int:
113
+ rc = _preflight(args.manifest)
114
+ if rc != 0:
115
+ return rc
116
+ if (rc := _ensure_po_available()) != 0:
117
+ return rc
118
+ result = run_manifest(
119
+ args.manifest,
120
+ max_workers=args.max_workers,
121
+ report=args.report,
122
+ quiet=args.quiet,
123
+ claude_exe=args.claude_exe,
124
+ )
125
+ if result.status == "not_installed":
126
+ print(_NOT_INSTALLED_MSG, file=sys.stderr)
127
+ return 1
128
+ return result.exit_code if result.exit_code >= 0 else 1
129
+
130
+
131
+ def _handle_waves(args: argparse.Namespace) -> int:
132
+ rc = _preflight(args.manifest)
133
+ if rc != 0:
134
+ return rc
135
+ fm = extract_frontmatter(args.manifest)
136
+ if fm is None:
137
+ # Should not happen after _preflight passed, but be defensive.
138
+ print("could not parse frontmatter", file=sys.stderr)
139
+ return 2
140
+ try:
141
+ waves = compute_waves(fm)
142
+ except ValueError as exc:
143
+ print(str(exc), file=sys.stderr)
144
+ return 2
145
+ output = {
146
+ "waves": [
147
+ {
148
+ "index": index,
149
+ "tasks": [
150
+ {
151
+ "id": task["id"],
152
+ "agent": task.get("agent"),
153
+ "read_only": task.get("read_only"),
154
+ "writes": task.get("writes") or [],
155
+ "prompt": task.get("prompt", ""),
156
+ }
157
+ for task in wave_tasks
158
+ ],
159
+ }
160
+ for index, wave_tasks in enumerate(waves)
161
+ ]
162
+ }
163
+ json.dump(output, sys.stdout, ensure_ascii=False, indent=2)
164
+ sys.stdout.write("\n")
165
+ return 0
166
+
167
+
168
+ def _handle_run_wave(args: argparse.Namespace) -> int:
169
+ rc = _preflight(args.manifest)
170
+ if rc != 0:
171
+ return rc
172
+ if (rc := _ensure_po_available()) != 0:
173
+ return rc
174
+
175
+ fm = extract_frontmatter(args.manifest)
176
+ if fm is None:
177
+ print("could not parse frontmatter", file=sys.stderr)
178
+ return 2
179
+ try:
180
+ wave_text = build_wave_manifest_text(fm, args.wave_index)
181
+ except (IndexError, ValueError) as exc:
182
+ print(str(exc), file=sys.stderr)
183
+ return 2
184
+
185
+ root = claude_root_for(args.manifest.parent) or claude_root_for(Path.cwd())
186
+ if root is None:
187
+ print(
188
+ "could not locate .claude/ directory to materialise the wave manifest",
189
+ file=sys.stderr,
190
+ )
191
+ return 2
192
+ tmp_dir = root / ".claude" / "tmp"
193
+ tmp_dir.mkdir(parents=True, exist_ok=True)
194
+ timestamp = datetime.now().strftime("%Y%m%d-%H%M%S")
195
+ wave_path = tmp_dir / f"po-manifest-wave-{args.wave_index}-{timestamp}.md"
196
+ wave_path.write_text(wave_text, encoding="utf-8")
197
+
198
+ result = run_manifest(
199
+ wave_path,
200
+ max_workers=args.max_workers,
201
+ report=args.report,
202
+ quiet=args.quiet,
203
+ claude_exe=args.claude_exe,
204
+ )
205
+ if result.status == "not_installed":
206
+ print(_NOT_INSTALLED_MSG, file=sys.stderr)
207
+ return 1
208
+ return result.exit_code if result.exit_code >= 0 else 1
@@ -23,6 +23,152 @@ from typing import Any
23
23
  _FRONTMATTER_RE = re.compile(r"^---\s*\n(.*?)\n---\s*(?:\n|\Z)", re.DOTALL)
24
24
 
25
25
 
26
+ # ---------------------------------------------------------------------------
27
+ # Topological wave decomposition
28
+ # ---------------------------------------------------------------------------
29
+
30
+
31
+ def compute_waves(frontmatter: dict) -> list[list[dict]]:
32
+ """Group ``frontmatter['tasks']`` into topological waves.
33
+
34
+ Wave 0 contains tasks with no ``depends_on`` references that are still
35
+ pending. Wave N contains tasks whose ``depends_on`` are all in waves
36
+ < N. Within each wave, tasks are independent and may execute in
37
+ parallel.
38
+
39
+ Wave order is deterministic: tasks within a wave are sorted by ``id``.
40
+
41
+ Raises:
42
+ ValueError: a cycle exists or a ``depends_on`` references an
43
+ unknown task id.
44
+ """
45
+ tasks = frontmatter.get("tasks") or []
46
+ if not isinstance(tasks, list) or not tasks:
47
+ return []
48
+
49
+ by_id: dict[str, dict] = {}
50
+ for task in tasks:
51
+ if not isinstance(task, dict):
52
+ raise ValueError("each task must be a mapping")
53
+ task_id = task.get("id")
54
+ if not isinstance(task_id, str) or not task_id:
55
+ raise ValueError("each task must have a string `id`")
56
+ if task_id in by_id:
57
+ raise ValueError(f"duplicate task id: {task_id!r}")
58
+ by_id[task_id] = task
59
+
60
+ for task in by_id.values():
61
+ for dep in task.get("depends_on", []) or []:
62
+ if dep not in by_id:
63
+ raise ValueError(
64
+ f"task {task['id']!r} depends_on unknown id {dep!r}"
65
+ )
66
+
67
+ remaining = dict(by_id)
68
+ waves: list[list[dict]] = []
69
+ while remaining:
70
+ ready = [
71
+ t for t in remaining.values()
72
+ if all(d not in remaining for d in t.get("depends_on", []) or [])
73
+ ]
74
+ if not ready:
75
+ raise ValueError(
76
+ f"cycle detected among tasks: {sorted(remaining)}"
77
+ )
78
+ waves.append(sorted(ready, key=lambda t: t["id"]))
79
+ for t in ready:
80
+ del remaining[t["id"]]
81
+ return waves
82
+
83
+
84
+ def build_wave_manifest_text(
85
+ frontmatter: dict, wave_index: int, *, body: str = ""
86
+ ) -> str:
87
+ """Render an ephemeral PO manifest containing only the wave's tasks.
88
+
89
+ All ``depends_on`` references are dropped (tasks within a single wave are
90
+ independent by construction). Top-level fields ``po_plan_version`` /
91
+ ``name`` / ``cwd`` / ``defaults`` / ``concurrency_limits`` are preserved;
92
+ ``on_complete`` / ``on_failure`` webhooks are dropped because they are
93
+ plan-level (not per-wave) lifecycle hooks.
94
+
95
+ Raises:
96
+ IndexError: ``wave_index`` is out of range.
97
+ """
98
+ waves = compute_waves(frontmatter)
99
+ if wave_index < 0 or wave_index >= len(waves):
100
+ raise IndexError(
101
+ f"wave_index {wave_index} out of range (have {len(waves)} waves)"
102
+ )
103
+ wave_tasks = waves[wave_index]
104
+
105
+ lines: list[str] = ["---"]
106
+ lines.append(f'po_plan_version: "{frontmatter.get("po_plan_version", "0.1")}"')
107
+ base_name = frontmatter.get("name", "wave")
108
+ lines.append(f"name: {_yaml_quote(f'{base_name} - wave {wave_index}')}")
109
+ lines.append(f"cwd: {_yaml_quote(frontmatter['cwd'])}")
110
+ defaults = frontmatter.get("defaults")
111
+ if isinstance(defaults, dict) and defaults:
112
+ lines.append("defaults:")
113
+ for k, v in defaults.items():
114
+ lines.append(f" {k}: {_yaml_scalar(v)}")
115
+ cgroups = frontmatter.get("concurrency_limits")
116
+ if isinstance(cgroups, dict) and cgroups:
117
+ lines.append("concurrency_limits:")
118
+ for k, v in cgroups.items():
119
+ lines.append(f" {k}: {_yaml_scalar(v)}")
120
+ lines.append("")
121
+ lines.append("tasks:")
122
+ for task in wave_tasks:
123
+ lines.append(f" - id: {task['id']}")
124
+ lines.append(f" agent: {task['agent']}")
125
+ lines.append(f" read_only: {'true' if task['read_only'] else 'false'}")
126
+ prompt = task.get("prompt", "")
127
+ if "\n" in prompt or len(prompt) > 80:
128
+ lines.append(" prompt: |")
129
+ for pline in prompt.rstrip("\n").splitlines():
130
+ lines.append(f" {pline}")
131
+ else:
132
+ lines.append(f" prompt: {_yaml_quote(prompt)}")
133
+ writes = task.get("writes")
134
+ if isinstance(writes, list) and writes:
135
+ lines.append(" writes:")
136
+ for w in writes:
137
+ lines.append(f" - {w}")
138
+ max_retries = task.get("max_retries")
139
+ if isinstance(max_retries, int):
140
+ lines.append(f" max_retries: {max_retries}")
141
+ cgroup = task.get("concurrency_group")
142
+ if isinstance(cgroup, str) and cgroup:
143
+ lines.append(f" concurrency_group: {cgroup}")
144
+ # Intentionally no depends_on: wave is internally independent.
145
+ lines.append("---")
146
+ lines.append("")
147
+ if body:
148
+ lines.append(body.rstrip("\n"))
149
+ else:
150
+ lines.append(f"# Wave {wave_index} manifest (generated by c3 po run-wave)")
151
+ return "\n".join(lines) + "\n"
152
+
153
+
154
+ def _yaml_quote(s: str) -> str:
155
+ if s == "":
156
+ return '""'
157
+ return '"' + s.replace("\\", "\\\\").replace('"', '\\"') + '"'
158
+
159
+
160
+ def _yaml_scalar(v: Any) -> str:
161
+ if v is None:
162
+ return "null"
163
+ if isinstance(v, bool):
164
+ return "true" if v else "false"
165
+ if isinstance(v, (int, float)):
166
+ return str(v)
167
+ if isinstance(v, str):
168
+ return _yaml_quote(v)
169
+ raise TypeError(f"unsupported scalar type: {type(v)}")
170
+
171
+
26
172
  # ---------------------------------------------------------------------------
27
173
  # Frontmatter extraction
28
174
  # ---------------------------------------------------------------------------
@@ -1,10 +0,0 @@
1
- # /develop コマンド
2
-
3
- plan-report に基づき TDD サイクルで実装を進める。
4
-
5
- ## 必ず守ること
6
-
7
- 1. **最初に必ず** `.claude/skills/dev-workflow.md` を Read する。記憶・推測で進めない
8
- 2. **フェーズ D(TDD)** から実行する
9
- 3. dev-workflow.md の AskUserQuestion・Edit・セッションファイル更新の手順を省略しない
10
- 4. D-0 で「PO 並列実行」が選ばれた場合は、続けて **必ず** `.claude/skills/parallel-execution.md` を Read してその手順に従う
@@ -1,121 +0,0 @@
1
- # Parallel Execution (parallel-orchestra)
2
-
3
- `/develop` の **D-0** で「PO 並列実行」が選ばれたときに Read される手順。
4
- parallel-orchestra (PO) を subprocess で起動し、plan-report を YAML フロントマター付きマニフェストとして並列実行する。
5
-
6
- ---
7
-
8
- ## 前提条件
9
-
10
- - `.claude/reports/plan-report-*.md` の最新ファイルに YAML フロントマター(`po_plan_version: "0.1"` を含む)が付いていること
11
- - `claude-code-conductor` がインストール済みで `c3` コマンドが PATH 上にあること
12
-
13
- ---
14
-
15
- ## Step 0: PO の利用可否を確認する
16
-
17
- Bash ツールで以下を実行する:
18
-
19
- ```
20
- c3 doctor --check po-only --quiet
21
- ```
22
-
23
- - 終了コード `0`(出力なし)= PO 利用可能 → Step 1 へ
24
- - 終了コード `1` = PO 未インストール → 以下の案内文を**そのまま提示**してこのスキルを終了する:
25
-
26
- > 並列実行を使うには `pip install parallel-orchestra` を実行してください。
27
- > 詳細: https://pypi.org/project/parallel-orchestra/
28
-
29
- **注意:** 案内はエラーではなく **情報メッセージ**として出すこと。`/develop` 全体は失敗扱いにせず、ユーザーに「逐次実行(D-0 の選択肢 [A])に切り替える」または「PO をインストールして再実行する」のいずれかを選んでもらう。
30
-
31
- ---
32
-
33
- ## Step 1: マニフェストの妥当性確認
34
-
35
- 1. Glob で `.claude/reports/plan-report-*.md` の最新ファイルパスを取得
36
- 2. Read で内容(フロントマター含む)を確認
37
- 3. Bash で以下を実行:
38
-
39
- ```
40
- c3 po dry-run <plan-report-path>
41
- ```
42
-
43
- - 終了コード `0` = マニフェスト妥当 → Step 2 へ
44
- - 終了コード `2` = マニフェストエラー(フィールド欠損・agent 不在等)→ stderr のメッセージを整形してユーザーに提示し、`/start` の **フェーズ C(計画)** を再実行するか手動で plan-report を編集するよう案内してこのスキルを終了する
45
- - 終了コード `1` = PO 未インストール(Step 0 をすり抜けた場合のフォールバック)→ Step 0 と同じ案内文を出して終了
46
-
47
- ---
48
-
49
- ## Step 2: ユーザー承認
50
-
51
- 承認なしで先に進まない。並列実行は git worktree を作って auto-commit するため副作用が大きい。
52
-
53
- 1. 親 Claude が plan-report の **フロントマター** を Read で再パースし、以下を要約してテキストで提示:
54
- - タスク総数
55
- - うち `read_only: false` のタスク数(= 作成される worktree 数)
56
- - 各タスクの `id` / `agent` / `writes`(あれば)の表
57
- 2. AskUserQuestion ツールで実行可否を確認:
58
-
59
- ```json
60
- {
61
- "questions": [{
62
- "question": "並列実行を開始してよいですか?",
63
- "options": [
64
- { "label": "承認", "description": "parallel-orchestra で実行を開始する" },
65
- { "label": "max_workers を変更して承認", "description": "次の入力で並列度を指定する(デフォルトは PO 側で 3)" },
66
- { "label": "キャンセル", "description": "並列実行を中止する。/develop の D-0 から逐次実行に切り替えられる" }
67
- ]
68
- }]
69
- }
70
- ```
71
-
72
- - 「max_workers を変更して承認」が選ばれた場合、追加 AskUserQuestion で並列度(整数)を聞く
73
- - 「キャンセル」の場合、セッションファイルに `## 試みたが失敗したアプローチ` として理由を追記してスキル終了
74
-
75
- ---
76
-
77
- ## Step 3: parallel-orchestra を起動する
78
-
79
- Bash で以下を実行:
80
-
81
- ```
82
- c3 po run <plan-report-path> --report .claude/reports/po-run-report-{timestamp}.json [--max-workers N]
83
- ```
84
-
85
- - `{timestamp}` は `YYYYMMDD-HHMMSS` 形式。Bash で `python -c "from datetime import datetime; print(datetime.now().strftime('%Y%m%d-%H%M%S'))"` を実行して取得
86
- - `--max-workers` は Step 2 でユーザーが指定した場合のみ付与
87
- - 進捗ダッシュボードはターミナルに直接出力される(親 Claude のコンテキストには末尾の状態のみ流れる)
88
-
89
- 終了コードと意味:
90
-
91
- | exit code | 意味 | 次のアクション |
92
- |---|---|---|
93
- | `0` | 全タスク成功 | Step 4 へ |
94
- | `1` | 1件以上のタスクが失敗 | Step 4 へ(失敗タスクの再実行案内を出す) |
95
- | `2` | マニフェストエラー(Step 1 をすり抜けた) | エラー内容を提示し計画フェーズへ戻るよう案内してスキル終了 |
96
- | `3` | runner エラー(claude バイナリ不在等) | `c3 doctor` の結果を提示してスキル終了 |
97
-
98
- ---
99
-
100
- ## Step 4: レポートの要約とセッション更新
101
-
102
- 1. Step 3 で生成された `.claude/reports/po-run-report-{timestamp}.json` を Read
103
- 2. 親 Claude がタスクごとのステータスを Markdown 表に整形してユーザーに提示する:
104
- - 列: `id` / `agent` / `status` / `worktree` / `duration` / 失敗時は `last_error_summary`
105
- 3. 失敗タスクがある場合、該当タスクのみ `/develop` の D-A 経路(逐次実行)で再実行する選択肢を AskUserQuestion で提示:
106
-
107
- ```json
108
- {
109
- "questions": [{
110
- "question": "失敗タスクをどうしますか?",
111
- "options": [
112
- { "label": "逐次実行で再修正する", "description": "失敗タスクのみ /develop の D-A(TDD 逐次)で再実行する" },
113
- { "label": "計画フェーズへ戻る", "description": "失敗の原因が設計レベルなら /start のフェーズ C で再計画する" },
114
- { "label": "ここで終わる", "description": "現状で停止し、フェーズ E(レビュー)には進まない" }
115
- ]
116
- }]
117
- }
118
- ```
119
-
120
- 4. セッションファイル(`.claude/memory/sessions/YYYYMMDD.tmp`)の `- [ ] PO 並列実行` を `- [x]` に Edit する。失敗タスクが残った場合は `- [ ]` のままにし、失敗内容を `## 試みたが失敗したアプローチ` に追記する
121
- 5. 全タスク成功した場合のみ、フェーズ E(レビュー)へ進む案内を出す
@@ -1,102 +0,0 @@
1
- """``c3 po`` - thin wrapper around the parallel-orchestra CLI."""
2
-
3
- from __future__ import annotations
4
-
5
- import argparse
6
- import sys
7
- from pathlib import Path
8
-
9
- from c3.paths import claude_root_for
10
- from c3.po.detect import detect_po
11
- from c3.po.manifest import validate_manifest
12
- from c3.po.run import run_manifest
13
-
14
-
15
- _NOT_INSTALLED_MSG = (
16
- "parallel-orchestra is not installed. "
17
- "並列実行を使うには `pip install parallel-orchestra` を実行してください。"
18
- "詳細: https://pypi.org/project/parallel-orchestra/"
19
- )
20
-
21
-
22
- def register(subparsers: argparse._SubParsersAction) -> None:
23
- parser = subparsers.add_parser(
24
- "po",
25
- help="Run a plan-report as a parallel-orchestra manifest",
26
- )
27
- inner = parser.add_subparsers(dest="po_command", metavar="<subcommand>")
28
- inner.required = True
29
-
30
- dry = inner.add_parser(
31
- "dry-run",
32
- help="Validate the manifest without executing tasks",
33
- )
34
- dry.add_argument("manifest", type=Path)
35
- dry.set_defaults(handler=_handle_dry_run)
36
-
37
- run = inner.add_parser(
38
- "run",
39
- help="Execute the manifest",
40
- )
41
- run.add_argument("manifest", type=Path)
42
- run.add_argument("--max-workers", type=int, default=None)
43
- run.add_argument("--report", type=Path, default=None)
44
- run.add_argument("--quiet", action="store_true")
45
- run.add_argument("--claude-exe", default=None)
46
- run.set_defaults(handler=_handle_run)
47
-
48
-
49
- def _ensure_po_available() -> int:
50
- available, _, _ = detect_po()
51
- if not available:
52
- print(_NOT_INSTALLED_MSG, file=sys.stderr)
53
- return 1
54
- return 0
55
-
56
-
57
- def _preflight(manifest: Path) -> int:
58
- if not manifest.is_file():
59
- print(f"manifest not found: {manifest}", file=sys.stderr)
60
- return 2
61
- root = claude_root_for(manifest.parent) or claude_root_for(Path.cwd())
62
- if root is None:
63
- print("could not locate .claude/ directory for agent lookup", file=sys.stderr)
64
- return 2
65
- errors = validate_manifest(manifest, root)
66
- if errors:
67
- for err in errors:
68
- print(err, file=sys.stderr)
69
- return 2
70
- return 0
71
-
72
-
73
- def _handle_dry_run(args: argparse.Namespace) -> int:
74
- rc = _preflight(args.manifest)
75
- if rc != 0:
76
- return rc
77
- if (rc := _ensure_po_available()) != 0:
78
- return rc
79
- result = run_manifest(args.manifest, dry_run=True)
80
- if result.status == "not_installed":
81
- print(_NOT_INSTALLED_MSG, file=sys.stderr)
82
- return 1
83
- return result.exit_code if result.exit_code >= 0 else 1
84
-
85
-
86
- def _handle_run(args: argparse.Namespace) -> int:
87
- rc = _preflight(args.manifest)
88
- if rc != 0:
89
- return rc
90
- if (rc := _ensure_po_available()) != 0:
91
- return rc
92
- result = run_manifest(
93
- args.manifest,
94
- max_workers=args.max_workers,
95
- report=args.report,
96
- quiet=args.quiet,
97
- claude_exe=args.claude_exe,
98
- )
99
- if result.status == "not_installed":
100
- print(_NOT_INSTALLED_MSG, file=sys.stderr)
101
- return 1
102
- return result.exit_code if result.exit_code >= 0 else 1