Claude Code × Codex を並列スウォーム化する ― tmux+gitワークツリーで複数エージェントを同時走行
前回のコスト予算アドバイザー記事で「残量を見てから走らせる」仕組みを作りましたが、今回はその先 ―― 1体のCodexを順番に回すのをやめて、複数を同時に走らせるスウォーム構成の話です。
plan.json に宣言したワーカーを、orchestrate-worktrees.js がtmuxセッション+gitワークツリーに展開し、各Codexが互いに干渉せず並列実行する三層プロトコルを実コードで紹介します。
困りごと:同じブランチ上でCodexを直列に回している
単一リポジトリ上でCodexを順番に実行すると、
- 前のジョブのコミットが次のジョブの前提条件を変えてしまう
- タスクAが終わるまでタスクBが待ち続ける
- 競合が起きると「どちらの変更が正しいか」の判断が人間に戻ってくる
という問題が出ます。git worktree でブランチを分離すれば競合リスクはゼロになる。それとtmuxを組み合わせて並列起動する、というのが今回の設計です。
全体像:三層プロトコル
plan.json ← 宣言層(何をどのワーカーに任せるか)
↓
orchestrate-worktrees.js ← オーケストレーター層(worktree作成・tmux起動)
↓
orchestrate-codex-worker.sh ← ワーカー層(Codex実行・成果物書き出し)
オーケストレーターはワーカーの存在を知らず、ワーカーはお互いの存在を知らない。成果物は .orchestration/{session}/{worker_slug}/ 以下の3ファイルに集約されます。
| ファイル | 役割 |
|---|---|
task.md | ワーカーへの作業指示(オーケストレーターが生成) |
status.md | 状態: not started → running → completed / failed |
handoff.md | Codexの出力 + git status(ワーカーが書く) |
plan.json で宣言する
{
"sessionName": "refactor-sprint",
"repoRoot": "~/my-project",
"worktreeRoot": "~/worktrees",
"coordinationRoot": "~/my-project/.orchestration",
"baseRef": "HEAD",
"replaceExisting": true,
"launcherCommand": "bash ~/.claude/scripts/orchestrate-codex-worker.sh {task_file} {handoff_file} {status_file}",
"seedPaths": ["package.json", "tsconfig.json"],
"workers": [
{
"name": "api-types",
"task": "src/api/ のレスポンス型を zod スキーマに移行する。既存のテストが全て通ること。",
"seedPaths": ["src/api/"]
},
{
"name": "ui-cleanup",
"task": "src/components/ 内の PropTypes を削除し TypeScript 型に一本化する。",
"seedPaths": ["src/components/"]
},
{
"name": "test-coverage",
"task": "src/utils/ のユニットテストを追加しカバレッジ 80% 以上にする。",
"seedPaths": ["src/utils/"]
}
]
}
seedPaths はグローバルとワーカーごとの2箇所に書けます。グローバルに書いた package.json / tsconfig.json は全ワーカーのworktreeにコピーされ、ワーカー固有の seedPaths はそのワーカーにだけ降ります。
orchestrate-worktrees.js が何をするか
エントリーポイントは3つのモードを持ちます。
# ① dry-run(デフォルト): 何を作るかJSONで確認
node scripts/orchestrate-worktrees.js plan.json
# ② --write-only: worktree作成なし、task.md / status.md / handoff.md だけ書く
node scripts/orchestrate-worktrees.js plan.json --write-only
# ③ --execute: worktree生成・tmux起動まで全実行
node scripts/orchestrate-worktrees.js plan.json --execute
--execute を叩くと lib/tmux-worktree-orchestrator.js の executePlan が走り、以下の順で処理します。
git rev-parse --is-inside-work-treeとtmux -Vで前提チェックreplaceExisting: trueなら既存セッション・worktree・ブランチをクリーンアップmaterializePlanで.orchestration/以下に3ファイルセットを書く- ワーカーごとに
git worktree add -b <branch> <path> <baseRef>を実行 tmux new-session -d -s <session>でセッション生成- ワーカーごとに
split-window -P -F '#{pane_id}'→ レイアウトをtiled → pane名を設定 → launchCommandを送信
ブランチ名とworktreeパスは自動生成されます。
branch: orchestrator-{session_name}-{worker_slug}
worktree: {repo_name}-{session_name}-{worker_slug}/ (worktreeRoot 直下)
実行後にアタッチするには tmux attach -t refactor-sprint だけ。3ペインが並んでそれぞれCodexが走っています。
replaceExisting: false(デフォルト)の場合、同名のtmuxセッションが既に存在するとエラーで止まります。再実行するときはreplaceExisting: trueにするか、手動でtmux kill-session -t <name>してから。
ワーカー側:orchestrate-codex-worker.sh
ワーカースクリプトは3引数を受け取ります。
bash ~/.claude/scripts/orchestrate-codex-worker.sh \
<task-file> <handoff-file> <status-file>
plan.json の launcherCommand にテンプレート変数でそのまま書けます。
"launcherCommand": "bash ~/.claude/scripts/orchestrate-codex-worker.sh {task_file} {handoff_file} {status_file}"
内部でCodexを呼ぶ箇所はこうなっています(実コード)。
cat > "$prompt_file" <<EOF
You are one worker in an ECC tmux/worktree swarm.
Rules:
- Work only in the current git worktree.
- Do not touch sibling worktrees or the parent repo checkout.
- Complete the task from the task file below.
- Do not spawn subagents or external agents for this task.
- Report progress and final results in stdout only.
- Do not write handoff or status files yourself; the launcher manages those artifacts.
...
Task file: $task_file
$(cat "$task_file")
EOF
if codex exec -p yolo -m gpt-5.4 --color never -C "$(pwd)" -o "$output_file" - < "$prompt_file"; then
ポイントは - < "$prompt_file" でstdinに流している点。-C "$(pwd)" で実行ディレクトリをworktreeに固定しているので、Codexが親リポジトリのファイルを誤って触る心配がありません。
成功すると handoff.md にCodexの出力と git status --short が書かれ、status.md が completed に変わります。失敗すると failed になり、次のワーカーには影響しません。
テンプレート変数の配線
launcherCommand で使えるテンプレート変数は以下(実ソースから)。
| 変数 | 内容 |
|---|---|
{worker_name} | ワーカー名(例: api-types) |
{worker_slug} | スラッグ化した名前(例: api-types) |
{session_name} | tmuxセッション名 |
{repo_root} | リポジトリのルートパス |
{worktree_path} | このワーカーのworktreeパス |
{branch_name} | このワーカーのブランチ名 |
{task_file} | task.mdの絶対パス |
{handoff_file} | handoff.mdの絶対パス |
{status_file} | status.mdの絶対パス |
各変数には _sh サフィックス版(シェルクォート済み)と _raw 版も生成されます。スペースを含むパスを別のシェルスクリプトに渡すときは {worktree_path_sh} を使うと安全です。
renderTemplate の実装はシンプルで、未定義変数が含まれると即エラーになります(Unknown template variable: xxx)。タイポは実行前に発見できます。
seedPaths でファイルを worktree に降ろす
git worktree add 直後のworktreeは baseRef 時点のスナップショットですが、package.json や設定ファイルはローカルで書き換えた最新版が必要なことがあります。seedPaths はその問題をコピーで解決します。
function overlaySeedPaths({ repoRoot, seedPaths, worktreePath }) {
for (const seedPath of normalizedSeedPaths) {
const sourcePath = path.join(repoRoot, seedPath);
const destinationPath = path.join(worktreePath, seedPath);
fs.cpSync(sourcePath, destinationPath, {
dereference: false, force: true, preserveTimestamps: true, recursive: true
});
}
}
ただし seedPaths は repoRoot の外側には出られません。../../../etc/passwd のようなパスは normalizeSeedPaths が .. チェックで弾きます。
seedPathsでコピーされたファイルはworktree上での編集後にコミットされます。グローバルのpackage.jsonをseedしてworktreeで書き換えると、その変更がそのブランチに乗ります。意図しない変更が入らないよう、seedするのは実際に必要なファイルだけに絞ること。
ロールバック設計
executePlan は途中で失敗しても作りかけのリソースを巻き戻します。createdState にその時点で生成済みのものを記録し、エラー時に rollbackCreatedResources を呼びます。
1. tmuxセッションを kill-session
2. 各worktreeを git worktree remove --force
3. git worktree prune --expire now
4. 対応ブランチを git branch -D
5. coordinationDir が元々なければ削除
この順は逆順(後から作ったものから消す)になっており、途中で別のロールバックが失敗しても残りを継続し、最後にまとめてエラーを投げます。
踏んだ落とし穴
replaceExistingをfalseのまま再実行して詰まる → 同名セッションが残っているとエラー。最初はtrueにしておくか、開発中は手動で kill してから- worktreeパスにスペースが入るとlaunchCommandが壊れる →
{task_file_sh}など_shサフィックス版を使う - seedPathsを忘れてworktreeに設定ファイルが降りない → ワーカーが正しい設定を読めず失敗する。
--write-onlyでtask.mdを確認してから--execute - Codexが
codex execで別のworktreeのファイルを触りに行く → worker.shのプロンプトに「Work only in the current git worktree」と明記されているが、タスク記述に絶対パスを書くとそちらへ飛ぶ。タスクにはリポジトリ相対パスだけを使う - tmuxが入っていない環境でもスクリプトが起動する →
executePlanの先頭でtmux -Vを叩いて存在チェックする。ない場合は即エラー(メッセージは明瞭)
まとめ
plan.jsonにworkers配列でタスクを宣言し、--execute一発でworktree+tmux+Codexが全部起動する- ブランチ・worktreeを分離するのでワーカー間のコンフリクトリスクがゼロ
- 成果物は
task.md/status.md/handoff.mdの3ファイルに集約。Claude Codeが後からまとめてレビューできる launcherCommandはテンプレート変数で自由に配線でき、Codex以外のワーカーにも差し替えられる- 失敗時のロールバックは自動。途中で止まってもリポジトリが汚れない
次回は、各ワーカーが書き出した handoff.md をClaude Codeがまとめてレビューし、マージ可否を判定するオーケストレーターループの話を書きます。
Lily(@bokuwalily)― 個人開発者。Claude Code で自動化基盤を組みながら、iOSアプリやWebサービスを量産しています
- AIで「寝てても回る仕組み」を作って月120万にした話は noteの有料記事 に💰
- OSS: github.com/bokuwalily 🐙
- 最新情報・お問い合わせは X @bokuwalily へ🌍
皆さんの ❤️ やシェアが励みになります!