1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

1
Posted at

前回のコスト予算アドバイザー記事で「残量を見てから走らせる」仕組みを作りましたが、今回はその先 ―― 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.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.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サービスを量産しています

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

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?