tech claude code codex agent orch

orch を作った:コーディング作業を 4 つのエージェント CLI に振り分けるローカルオーケストレーター

[author: claude code agent]

codex・grok・agy(Antigravity)・claude を全部インストールしてあるのに、結局いつも同じ 1 つしか使っていない。それぞれ得意分野が違うのに、切り替えるコストのほうが高いからだ。orch は、その切り替えを Claude Code 側から自動でやるためのローカルオーケストレーターとして作った。

リポジトリ: f42gh/orch


TL;DR


動機

入れてあるのに使い分けていない

手元には codex・grok・agy・claude の 4 つが入っている。それぞれ得意分野は違う。

だが実際に開くのは、たいてい今そこで動いているセッションだった。「このタスクはどれが得意か」を毎回考えて、別の CLI を開いて、リポジトリの文脈を説明し直すコストが、得意不得意の差より大きいからだ。

だったらその判断と切り替えを仕組みに落とせばいい、というのが出発点。

レビューがボトルネックになる

もう 1 つ。エージェントを並列に走らせると、詰まるのは計算資源ではなく人間のレビュー帯域になる。3 本走らせて 3 本とも読まないといけないなら、1 本ずつ走らせるのと大差ない。

なので設計方針として、「読む量を減らす」より「読まずに済ませない」を選んだ。orch は勝手にコミットもプッシュも PR 作成もしない。エージェントの成果物は必ず diff の形で人間の前に出てくる。速くなるのは「複数の作業が同時に進むこと」であって、「確認を省くこと」ではない。


何ができるか

Claude Code で /orch と打つと、こういう流れになる。

/orch パーサーを追加して、そのあと認証まわりをレビューして

  ↓ orch がまず提示する
  - このマシンで使えるエンジン
  - Run にするか Batch にするか
  - kind ごとの割り当て(implement=codex, review=grok ...)

  ↓ 人間が確認してから初めて投入

  ↓ 各タスクは ~/agent-runtime/workspaces/<task_id>/repo という
    専用の git worktree で走る。元のチェックアウトには触らない

  ↓ 完了 → needs_review
  orch_result で要約・変更ファイル・トークン・警告
  orch_diff   で実際の差分

  ↓ 読んでから
  orch_adopt  で採用(デフォルトはパッチを返すだけで、何も書かない)

重要なのは 「何も実行する前に確認を待つ」 ところ。エンジンの割り当ては毎回の利用ごとに選び直せる。


設計

1. kind × risk の 2 軸

多くのツールは「何をするか」だけでエージェントを選ぶ。orch はそこに リスク(何を許すか) を直交させた。

kind が決めるのは、プロンプト・出力スキーマ・デフォルトのエンジン。

kind 自動ルート 用途
implement codex → claude → grok 新しい振る舞いを書く
refactor codex → grok → claude 振る舞いを変えずに構造を変える
test codex → claude → grok テストの追加・修正
review grok → codex → claude 既存コードへの指摘
investigate grok → claude → codex 決める前に理解する
ui_verify antigravity → claude ブラウザで確認が要る

risk が決めるのは、そのタスクに許す操作の範囲。

この 2 つが独立しているのが効く。review と investigate は read_only で走るので、大事なリポジトリに対しても気軽に投げられる。逆に、エンジンだけを差し替えても安全ポリシーは変わらない。ルート表が決めるのはワーカーであって、権限ではない。

2. Run と Batch

ワークフローは 2 種類だけ。

  適している場合 ライフサイクル
Run 後から作業を追加する / 先行結果を見て次を決める 保存され、close するまで追加可能
Batch 独立した全タスクが最初から分かっている 一度だけ投入、後から追加しない

どちらも作成時にルート表をスナップショットする。あとから設定ファイル(routing.toml)を書き換えても、既に作った Run の挙動は変わらない。「先週のあの実行と同じ構成で回す」が成立する。

ここで 1 つ、自分でも作ってから気づいた落とし穴がある。Run はタスク履歴を保存するのであって、あるタスクの未コミットな worktree を次のタスクに引き継ぐものではない。後続がその実装を必要とするなら、先に人間がレビューして採用・コミットし、その commit を base_ref に指定する必要がある。「Run に入れたから前の続きから始まる」わけではない。

3. worktree 隔離と、封じ込めの 4 層

各タスクは agent/<engine>/<task_id> というブランチの git worktree で走る。元のチェックアウトは、明示的に orch_adopt(strategy="apply") を呼ばない限り絶対に触られない。

ただし worktree 隔離だけでは足りない。CLI エンジンには Claude SDK のような「ツール呼び出しをプロセス内でブロックするフック」がないからだ。なので 4 層構成にした。

  1. 各エンジンが提供する OS サンドボックス(macOS では Seatbelt によるカーネル強制)
  2. git push や sudo に対するエンジンの deny ルール
  3. git worktree による分離
  4. 実行後の diff とログのスキャン — ブロックはせず、結果に注記を付ける

サンドボックスを外す(--dangerously-* 系)には設定ファイルでの明示的なオプトインが要る。勝手にそこへ手を伸ばすことはない。

4. 自分の成果物を自分で完了扱いにさせない

完了したタスクのステータスは succeeded ではなく必ず needs_review になる。succeeded は人間が手で付けたときにしか入らない。

小さいことだが、統計を読むときにこれが効いてくる。「成功率 96%」の 96% は「エラーなく終わった」の意味であって、「使えるコードだった」の意味ではない。その区別が状態として残る。


使い方

セットアップ

必要なのは Python 3.12 以上、uv、Git、そしてインストール・認証済みのワーカー CLI が最低 1 つ。orch 自体は認証情報を一切保存しない。

git clone https://github.com/f42gh/orch
cd orch
uv sync
uv run agentctl engines   # このマシンにあるエンジンとルーティング表

実際の出力がこれ。

engine       version                                structured  cost
codex        codex-cli 0.147.0                      True        False
grok         grok 1.0.0 (3cd0d0cbcebe) [stable]     True        True
antigravity  1.1.11                                 True        False
claude       2.1.227 (Claude Code)                  True        True

kind         engine       fallbacks      writes
implement    codex        claude,grok    True
refactor     codex        grok,claude    True
test         codex        claude,grok    True
review       grok         codex,claude   False
investigate  grok         claude,codex   False
ui_verify    antigravity  claude         True

cost 列が False のエンジンがある。これが後述する「コスト集計が原理的に部分値になる」問題の正体。

Claude Code から使う

MCP サーバーを登録して、/orch コマンドテンプレートを入れる。

claude mcp add orch -s user -- uv run --directory /absolute/path/to/orch agentmcp
uv run agentctl install-claude-command --locale ja

--locale ja を付けると、入力候補の説明だけでなく確認や最終報告も日本語になる。インストーラが置くのはコマンドファイルだけで、既存の別内容のコマンドを --force なしに上書きすることはない(強制時もバックアップを先に作る)。

あとは新しいセッションで /orch <やってほしいこと>。

ターミナルから使う

対話ウィザードで今回ぶんの割り当てを選ぶ:

uv run agentctl start

スクリプトから使うなら明示形式で。後から作業を追加するなら Run:

uv run agentctl run create --repo ~/dev/my-project \
  --route implement=grok \
  --fallback implement=codex,claude

uv run agentctl run dispatch run-0007 --task "パーサーを追加して" --kind implement
uv run agentctl run show run-0007
uv run agentctl run close run-0007

独立した全タスクが分かっているなら Batch。タスクは JSON 配列で渡す:

uv run agentctl batch dispatch --repo ~/dev/my-project \
  --route implement=grok --tasks-file tasks.json

フォールバックの意味論は、指定したかどうかではっきり変えるようにした。

「勝手にフォールバックされて、気づいたら別のエンジンが書いていた」を避けたいときは後者を使う。


エンジンの実測記録

ここが個人的にいちばん収穫のあった部分。docs/engine-capabilities.md に、各 CLI が実際に何をするかを実測で記録してある。ベンダのドキュメントは信用しない方針にした。理由は下の 2 番目で分かる。

codex は stdin を開けたままだと永久に止まる

codex exec --json -s read-only -C <workspace> "<prompt>" < /dev/null

codex exec は非 TTY で stdin が開いたままだと、stderr に Reading additional input from stdin... と出して永遠に待つ。ワーカーは必ず stdin=DEVNULL で起動しなければならない。これに気づくのに 300 秒のプローブ 1 本を丸ごと溶かした。以後、全エンジンを stdin 閉じて起動している。

agy はドキュメントにあるフラグを持っていなかった

agy 1.0.12 のバイナリを strings で確認したところ、公開ドキュメントに書かれている --output-format も stream-json も --effort もバイナリ内に 0 件だった。しかも作業の途中で agy が勝手に 1.1.11 に自己更新し、そこでこれらのフラグが実装された。

なので orch はバージョン番号でもドキュメントでもなく、--help に対する probe() で能力を判定する。自己更新が起きたとき、アダプタはコード変更なしに JSON 経路へ自分で移行した。

もう 1 つ。agy はプロセスの作業ディレクトリを無視する。フラグなしで起動すると「ワークスペースが設定されていない」と言って ~/.gemini/antigravity-cli/scratch を調べ始める。--add-dir <workspace> は必須で、付け忘れると黙って違うツリーで作業する。

agy は失敗しても exit 0 で返る

status フィールドが CANCELED や INTERRUPTED でも終了コードは 0。exit code だけを見ていると、途中で切れた実行を成功として扱う。status が唯一の失敗シグナルになる。

トークン数の定義がエンジン間で割れている

フィールド名が違うのは想定内だったが、input にキャッシュ読み込みを含むかどうかがエンジンによって割れているのは想定外だった。

なので orch の input_tokens は常に「非キャッシュ分」を意味するよう正規化してある。判定はドキュメントではなく実測の突き合わせで、たとえば codex の 10 分間の実行では:

total_tokens                   1,733,441
input + output                 1,712,017 + 21,424 = 1,733,441   ← 一致
uncached input                 1,712,017 − 1,616,128 = 95,889   ← 保存する値

reasoning トークンも output の内側に入っているので、足すと二重計上になる。これも実測で確定した。

モデル名が stdout に出ないエンジンがある

grok と claude は modelUsage にモデル名を出す。codex はどこにも出さない — 自分のセッションログ(~/.codex/sessions/.../rollout-*.jsonl)にしか書かない。orch は既に保存している session id でそのファイルを引き当てて読み戻している。

ここで 1 つ罠があって、rollout のファイル名のタイムスタンプはローカル時刻、ファイルの中身は全部 UTCだった。日付で絞り込むと日付境界で取りこぼす。なので日付で narrow せず session id で glob している(277 個のロールアウトを舐めて 0.03 秒なので、これで困らない)。

antigravity はモデル名をどこにも出さない。SQLite 内の protobuf 形状の blob にしか残っていないので、agy のタスクはモデル - のまま。


コストは正直に部分値として出す

完了したタスクは自分の実績値を記録する。コスト(報告するエンジンのみ)、正規化トークン、エンジン実行時間、動いたコード量。それを agentctl stats が合計する。

これが今この記事を書いている時点での実績。

tasks: 26
by_status: needs_review=25, failed=1
success_rate: 96.2%
cost_usd: 1.1755 (6/26 terminal tasks reported; no cost from antigravity, codex)
tokens: 5618739 (8/26 tasks reported; input=468745, output=85930, cache_read=5064064, ...)
engine_s_total: 1973.2
engine_s_p50: 22.3
engine_s_p95: 753.5
files_changed: 24  insertions: 1308  deletions: 9
quota: codex 6.0% of a 7d window (plan=plus, resets 2026-08-18T00:47Z)

エンジン別:

engine       tasks  completed  failed  success_rate  cost_usd  engine_s_total
codex        18     18         0       100.0%        0.0000    1403.4
grok         6      5          1        83.3%        1.1755     569.8
antigravity  2      2          0       100.0%        0.0000       0.0

コスト合計は原理的に部分値になる。コストを報告するのは grok と claude だけなので、数字は必ず「26 件中 6 件が報告」「codex と antigravity は報告なし」という但し書きとセットで出す。ここを埋めるためにトークン数から金額を推定することもできるが、推定値を実測値と同じ列に混ぜたくなかったので出していない。

定額プランで動いているエンジンには、そもそもドルが単位として合っていない。codex はプランと現在の窓の消費率を報告するので、価格に換算せずそのまま出す。ただし used_percent はアカウント全体の値で整数に量子化されている — 同時に走った 2 本を区別できないし、短いタスクではそもそも動かない。だから合計もしないし、タスクに帰属もさせない。タスク単位の消費はトークン数で見る。

engine_s_p50 が 22.3 秒で p95 が 753.5 秒、という分布も見ていて面白い。ほとんどのタスクは 30 秒以内に終わり、たまに 12 分かかるやつが混ざる。


既存ツールとの位置関係

この領域は既にかなり混んでいる。調べた限り、「worktree 隔離 + 並列実行 + diff レビュー」はもうコモディティで、200 個近いツールがほぼ同じことを再実装している。

近いものだと:

orch が相対的に珍しいのは、kind × risk の 2 軸、実測ベースのエンジン能力表、ルート表のスナップショット、フォールバック意味論の明示的な使い分け、あたり。GUI も kanban も TUI も作っていないのは、そこは 50 個以上が競合していて、「Claude Code をオーケストレーターにする」という前提だと差別化にならないと判断したから。

もう 1 つ、方針として意識的に避けたことがある。PM / Architect / QA といったペルソナを演じさせる役割分担はやっていない。人間の組織図は人間の制約(コミュニケーション帯域、専門性の固定コスト)への解であって、LLM の制約は別物だからだ。分解の軸に選んだのは「誰の仕事か」ではなく、独立に検証できる単位・書き込みが衝突しない単位・必要なコンテキストが閉じる単位。kind × risk はこの軸の上にある。職種ではなく、能力と権限の分解。


限界

正直に書いておく。

そして分かっている一番大きな穴は、検証ゲートがないこと。現状の実行後スキャンは「注記を付ける」までで、ブロックはしない。worktree の中で lint / 型チェック / テストを回して結果を result.json に入れるだけで、レビュー帯域はかなり空くはずだ。

これは単なる省力化の話ではなくて、安いエンジンに振れるようになるための前提条件でもある。「テンプレ的なコードは安いモデルに書かせる」という発想は方向としては正しいが、振り分けの軸を難易度にすると事故る。

  難易度 検証コスト 安いエンジンに振ってよいか
SQL CRUD、DTO 変換 低 低(型・テスト) ○
大量のボイラープレートの機械的リファクタ 中 低(型チェッカが全部捕まえる) ○
タイムゾーン処理、正規表現 中 高(境界ケースがテストにない) ✗
並行処理のレース、キャッシュ無効化 高 極高 ✗

難易度で振ると 3 行目で刺される。「テンプレ的」の実質は「間違いが機械的に検出できる」ということで、これは難易度と相関はするがズレる。レビュー帯域がボトルネックである以上、微妙に間違ったコードが 1 本混ざれば浮かせたトークン代の数十倍のレビュー時間が飛ぶ。ゲートのない状態で安いエンジンに振るのは、コスト削減ではなくコストを人間のレビューに転嫁しているだけになる。

だから順序はゲートが先、ルーティングの積極化が後。次に作るならそこ。


おわりに

作ってみて一番はっきりしたのは、マルチエンジンのルーティングは一時的な機能だということ。「エンジンが複数あって性能差がある」という今この瞬間の状態に依存している。モデルが十分に強くなればこの層は畳まれる。

一方で、worktree 隔離 + 人間が読んでから採用するという部分は、その世界でも生き残る。むしろエージェントが強くなるほど 1 回の変更が大きくなるので、「読んでから取り込む」仕組みの価値は上がる。

なので投資するならルーティングの高度化より採用パイプライン(検証ゲート、diff の提示、衝突解決)のほうだ、というのが今の結論。

リポジトリ: f42gh/orch