repo 間の Agent タスク連鎖 — 中央 immutable queue による cross-repo handoff
Multi-Repo Agent Orchestrator の続編. 上流ライブラリの semantic change が下流の研究結果を動かす問題に対し, repo から repo への有向タスク連鎖を「中央 immutable queue + 1 ファイル 1 所有者 + 実行ゲート (preflight)」で運ぶ機構を作った. git の shared-write 衝突を所有権で原理的に消す設計, enqueue 自動 / 実行ゲートの分離, AI 同士の相互検証で初版設計が棄却された経緯を解説する.
Multi-Repo Agent Orchestrator で, 複数リポの Agent を束ねる上位レイヤを紹介した. あの構成の通信モデルは「各リポの Agent ↔︎ Orchestrator」のハブであり, repo 同士は直接話すことができない. 状態の報告も知識もタスクも, いったん中央 (inbox/outbox) or 人間 を経由し, 毎時の巡回が配り直す形式になっている.
このハブがた運用だと repo から repo への, 有向の, 低遅延なタスクの受け渡しの度に人間が何かしらの処理(プロンプトで指示等)を挟む必要があり,非常に手間がかかるので,直接別repoの間のAgentが話せるようにした.
本記事はそのために追加した中央 immutable queue による cross-repo handoff の仕組みと設計判断の話です. 前回の続編だが, 「複数の git repo に複数の Agent が並行で作業している時, repo 間の依頼をどう運ぶか」という一般的な問題として読めるはず.
ハブでは何が運べないか
既存の inbox/outbox ハブは repo ↔︎ Orchestrator の汎用レーンで, これはこれで機能している. しかし上の用途には 3 点で合わない.
- 有向でない: ハブの宛先は「Orchestrator」であって「特定の repo」ではない. 上流 Agent が「この変更は下流のあのリポに影響する」と分かっているのに, いったん中央の解釈を挟む必然がない.
- 低遅延でない: 配り直しは毎時巡回 (≤1h) 経由である. cmux でライブラリ側と論文側のセッションが同時に生きている作業日には, 1 時間の遅延は並行作業の意味を削ぐ.
- 連鎖を表現できない: 「version X の追従が終わってから version Y の追従をする」のような依存つきのタスク列 (DAG) を, ハブの単発タスクでは表現できない.
実は当初は, もっと素朴な解決策 — 上流 Agent が下流リポの plan ディレクトリへ直接タスクファイルを書き込む「mailbox 方式」 — を設計していた. これがレビューで棄却された経緯は後述するとして, 先に最終形を見せる.
解決策 — 中央 immutable queue
機構の全体は, Orchestrator 領域 (git 追跡) に置かれた 2 種類のファイルだけでできている.
state/cross-repo-handoffs/
<宛先リポ>/
queue/ <chain_id>.md ← 送信者 (上流 Agent) 所有. immutable, write-once = request
receipts/<chain_id>.md ← 受信者 (下流 Agent) 所有 = 処理結果 (disposition)
request (queue 側) はこういう形をしている. ポイントは class と verify と depends_on_items の 3 フィールドである.
---
chain_id: <上流>-<commit|version>-<slug> # 一意. 他 request の依存から参照される
from: <上流リポ>
to: <下流リポ>
trigger: { kind: commit | release, ref: <hash|version> }
class: mechanical | substantive # 迷ったら substantive
depends_on_items: [] # 先行すべき他 chain_id (DAG)
on_dependency_rejected: ask # 先行が却下された時: ask | abandon | continue
verify: ["stack build", "..."] # 受信側が preflight で走らせる検証
created: 2026-06-11
---
## なぜ (上流で何が変わったか)
## 受信側でやること (action)
## 受け入れ基準どの repo がどの repo の上流かは, Orchestrator の設定 (1 枚の YAML) に depends_on エッジとして宣言されている. このエッジが queue の張られるトポロジを決め, 巡回が監視する対象も決める.
設計判断 1 — 所有権の分離で git 衝突を原理的に消す
このシステムのファイルはすべて git 追跡された markdown であり, 複数の Agent (とユーザ) が並行で触る. すると当然, 同一ファイルへの並行書込み (shared-write) が衝突源になる. 解決策として採用したのは Learn 層の iCloud 競合対策と同じ指針 — 衝突はマージアルゴリズムでなく所有権で避ける — である.
- request は送信者所有で, 書いたら不変 (write-once). 受信者は request に一切触らない.
- 受信者の状態は別ファイル (receipt) に書く. 1 chain_id = 1 receipt で, これは受信者だけが書く.
- 処理の作業コピーは受信側リポの plan ディレクトリへ import し, そこは受信者の領域.
「queue item に対応する receipt が無い」= 未処理, という判定もファイルの存在だけで決まるので, 状態フィールドの更新合戦が起きない. request を後から訂正したい時も, 元ファイルは編集せず新しい chain_id を立てて旧を superseded 指定する. event-sourcing を markdown と git でやっている, と言ってもよい.
この所有権モデルのおかげで, 同一ファイルへの shared-write が設計上存在しない (= git conflict が原理的に起きない). 並行 Agent の数が増えても通信路の不変条件は変わらない.
設計判断 2 — enqueue は自動, 実行はゲート
次に「いつ送ってよいか / いつ実行してよいか」の規律. ここは非対称にした.
enqueue (送る側) は自動・即時・承認不要である. 上流 Agent は semantic change (下流の挙動を動かしうる変更) を land した時点で, 影響先の queue へ request を積む. 迷ったら積む. これが許されるのは, 積むだけでは何も実行されないからである. queue は不変の依頼書の束であって, 積むこと自体に副作用がない.
drain (実行する側) はゲート付きである. 下流 Agent は session 冒頭に自分宛て queue を走査し, class で分岐する.
- mechanical (新しい解釈を要さない反映 — 版番号の追従, pin の更新, 決定的再実行) でも, 自動で許されるのは preflight = 検証コマンドの実行と diff の生成まで. その結果 diff が空で統計判定も図表値も動かない時だけ, ユーザ指示なしで完了する. これが自動完了の唯一の経路である.
- preflight で非ゼロ diff, 統計判定の変化, 図表値の変化が出たら, その request は自動で substantive に昇格し, 承認ゲートに回る (receipt に昇格の事実が記録される).
- substantive (定義の改訂, 結論に関わる数値解釈, 本文主張の変更) は最初から承認ゲート行き. 承認まで実行されない.
この設計の核は, 動機の事故をそのまま裏返したものである. 「機械的な版更新」と分類されたタスクが, 実行してみたら結論を動かす — これが一番危ない取りこぼしだった. だから分類を信用せず, preflight の結果で再分類する. mechanical とは「安全だと宣言されたタスク」ではなく「安全かどうかを安く確認できるタスク」のことだ, と言い換えてもよい.
依存連鎖 (DAG) の側にも対応するゲートがある. 先行 chain_id が done になるまで後続は処理されない. では先行が却下されたら? — ここで黙って待ち続けるとデッドロックになるので, request 自身が on_dependency_rejected (ask / abandon / continue) で振る舞いを宣言しておく. 循環依存や参照先不明の request は blocked として隔離され, ユーザに報告される. 「待ちで固まる」状態を作らないことは, 人間が常時監視しない系では特に効く.
設計判断 3 — 二重トランスポート
通信路は 2 本あり, どちらも同じ queue を指す.
| パス | 経路 | 遅延 | 役割 |
|---|---|---|---|
| 即時 | 上流 Agent が中央 queue へ直接 write | ゼロ | 並行 session が同時に生きている時の低遅延 handoff |
| 永続 | Orchestrator 巡回が未 drain 件数を下流の context-pack / briefing に surface | ≤1h | session・日・版を跨いだ取りこぼし防止 |
即時パスだけだと, 下流のセッションが立っていない時に request が放置される. 永続パスだけだと冒頭の低遅延要件に戻ってしまう. 2 本を併設し, ただし状態は queue という 1 箇所にだけ持つ (パスはどちらも「気づかせる」だけ) ので, 二重計上や食い違いは起きない.
ハブ (inbox/outbox) との関係も同じ整理である. ハブは repo ↔︎ Orchestrator の汎用レーン, 本機構は repo ↔︎ repo の有向タスク連鎖で, 役割が違うので置き換えず併存させる. 通信路を増やす時は「既存路の代替か, 別役割か」を先に決めておくと, 後で経路の選択に迷わない.
初期廃案 — AI 同士の相互検証
最後に設計の経緯を書いておきたい. 冒頭で触れたとおり, 初版の設計は「上流 Agent が下流リポの plan ディレクトリへ直接 request を書き込む mailbox 方式」だった. 直感的だし, 中央を経由しない分シンプルに見える.
この案を, 普段から使っている相互検証の枠組み — Claude が設計し, 独立した別モデル (Codex) が懐疑的にレビューする — に掛けたところ, 4 点の指摘で棄却された.
- 受信者が request の status を更新すると, 送信者と受信者の同一ファイル shared-write になる (git 衝突源).
- mechanical の自動実行は, 「機械的な版更新が結論を覆す」事故 (まさに動機の実例) を取りこぼす.
- 一部の下流リポは「plan ディレクトリは Orchestrator が取り込まない」という既存契約を持っており, 上流 Agent がそこへ書き込む設計は契約と衝突する.
- リポによって plan を git 追跡していたりいなかったりして (gitignore), mailbox の永続性が非対称になる.
どれも言われてみればその通りで, 特に 2 は失敗例があり上手く働かないことが分かっていた. この指摘を受けて「中央 immutable queue + 所有権分離 + preflight 止まり」へ改設計したのが本記事の形である. 設計文書には, 採用した原則だけでなく初版が何で棄却されたかも why として記録してあり, 将来似た判断をする Agent がそこを読む.
一人で開発していると設計レビューの相手がいない, というのは個人開発の古典的な弱点だが, 独立した複数の AI に同じ設計を別角度から読ませる体制は, その代替として実用になりつつあると感じる.