🐝 Claude Code × Codex を並列スウォーム化する ― tmux+gitワークツリーで複数エージェントを同時走行 — リーダー×
🐝

Claude Code × Codex を並列スウォーム化する ― tmux+gitワークツリーで複数エージェントを同時走行

#claudecode#codex#automation#git2026-07-03 · 約10

前回のコスト予算アドバイザー記事で「残量を見てから走らせる」仕組みを作りましたが、今回はその先 ―― 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 startedrunningcompleted / failed
handoff.mdCodexの出力 + 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.jsexecutePlan が走り、以下の順で処理します。

  1. git rev-parse --is-inside-work-treetmux -V で前提チェック
  2. replaceExisting: true なら既存セッション・worktree・ブランチをクリーンアップ
  3. materializePlan.orchestration/ 以下に3ファイルセットを書く
  4. ワーカーごとに git worktree add -b <branch> <path> <baseRef> を実行
  5. tmux new-session -d -s <session> でセッション生成
  6. ワーカーごとに 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.jsonlauncherCommand にテンプレート変数でそのまま書けます。

"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.mdcompleted に変わります。失敗すると 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
    });
  }
}

ただし seedPathsrepoRoot の外側には出られません。../../../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 が元々なければ削除

この順は逆順(後から作ったものから消す)になっており、途中で別のロールバックが失敗しても残りを継続し、最後にまとめてエラーを投げます。

踏んだ落とし穴

  • replaceExistingfalse のまま再実行して詰まる → 同名セッションが残っているとエラー。最初は 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.jsonworkers 配列でタスクを宣言し、--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サービスを量産しています

皆さんの ❤️ やシェアが励みになります!