AIエージェントを複数、同じリポジトリに放り込んだら起きたこと
エージェントのセッションを並列に立ち上げれば生産性が倍になると思っていた。実際にはブランチの乗っ取り、他人のブランチに載ったコミット、reflogで拾い上げた孤児コミット、同じ機能の二重実装まで、一週間のうちに実際の事故が立て続けに起きた。一つのチェックアウトを共有する並列エージェントが生む事故の類型学と、それを経て立てたルール。
要点まとめ
並列エージェントセッションの本当の敵はモデルの実力ではなく、共有された作業ディレクトリだ。HEADは全員のグローバル変数なので、あるセッションのブランチ切り替えが、別のセッションのコミットを見当違いの場所に載せてしまう。一週間で経験した4種類の事故と復旧の記録、そしてworktree隔離・コミット直前のブランチ再確認・検証ゲートのスコープ縮小という三つの防衛線。
目次
AIエージェントと仕事をしていると、自然と欲が出てくる。一つのセッションがバグを直している間に、別のセッションはリファクタリングをして、もう一つはイシューを調査すればいいのではないか。モデルはいくつでも立ち上げられるのだから、生産性もその分だけ増えるはずだと。
私もそうやって始めた。そして一週間で思い知った。並列エージェントの本当の敵はモデルの実力ではなく、彼らが共有する作業ディレクトリだということを。
HEADはグローバル変数だ
原因は一文に要約できる。一つのgitチェックアウトを複数のセッションで共有すると、「現在のブランチ」という状態が全員のグローバル変数になる。
人間が二人で一台のコンピュータを同時に使う状況を想像すれば、おかしいことはすぐ分かるのに、エージェントを立ち上げるときはこの発想が抜けていた。ターミナルのタブごとにセッションが一つずつ立っているので、互いに隔離されているように見える。だがファイルシステムは一つで、HEADも一つだ。あるセッションが git checkout をした瞬間、残りすべてのセッションの足元が変わる。
あの一週間に起きた事故は、類型がはっきりしていた。
ブランチの乗っ取り。 セッションAがトピックブランチで作業している最中に、セッションBが自分の作業をしようとブランチを切り替えた。Aはそれを知らないままコミットし、コミットはBのブランチの上に載った。逆方向もあった。Aがコミットしようとした瞬間、ブランチがdevelopに変わっていて、保護ブランチへの直接コミットを止めるフックが遮断してくれたおかげで事故を免れた。フックがなければそのまま入っていた。
孤児コミット。 セッションAのトピックブランチを、セッションBが整理作業の途中で削除した。Aのコミットはどのブランチにも属さない孤児になり、reflogを漁ってコミットハッシュを見つけ、cherry-pickで回収した。拾えたからよかったものの、reflogが期限切れだったり見つけられなかったりしたら、作業がまるごと蒸発するところだった。
ステージングの汚染。 セッションAがコミットを作る時点で、セッションBがステージングしておいたファイル削除がステージング領域に一緒に載っていた。そのままコミットしていたら、AのコミットにBの削除が混ざり込んでいた。diffを眺めていて見慣れない変更に気づいて弾いたが、エージェントがコミット直前にdiffを見ていなかったら分からなかっただろう。
二重実装。 いちばん虚しい類型。二つのセッションが互いの存在を知らないまま、同じ機能をそれぞれ実装した。どちらも真面目によく作ってあり、片方は丸ごと捨てるしかなかった。並列化で稼いだ時間を、そっくりそのまま返上したわけだ。
検証ゲートまで踏み合う
問題はファイルとブランチだけではなかった。うちのハーネスには、セッションが終わる前にリポジトリ全体の静的解析とテストを回して、壊れた状態では作業を終えられないようにするゲートがある。単独セッションでは優れた仕掛けだ。
並列セッションでは、これが互いの足を引っ張る罠になった。セッションAはドキュメントしか触っていないのに、セッションBが直している最中のファイルのコンパイルエラーのせいで、Aのゲートが落ちる。Aは「自分の変更ではない」を証明するのにターンを費やし、ひどい日はBが片付けるまで30分待たされた。最悪の事件は、あるセッションがゲートを通過しようとして、別のセッションが作業中のコードを直してしまったことだ。ゲートがかえって、他人の作業に手を出す誘因になった。
検査そのものが間違っているわけではない。検査のスコープが「リポジトリ全体」であることが、リポジトリがもはや一人のものでなくなった瞬間に間違いになったのだ。
三つの防衛線
事故を類型ごとに経験してから、防衛線を三重に張った。
- worktree隔離をデフォルトに。
git worktreeでセッションごとに独立した作業ディレクトリを与えれば、HEADはもう共有変数ではない。ブランチの乗っ取り、孤児コミット、ステージングの汚染が根こそぎ消える。上の事故類型4つのうち3つが、この一手でなくなる。ただしタダではない。モノレポならworktreeごとに依存関係のインストールとコード生成をやり直す必要があるし、git-cryptのようにworktreeと相性の悪いツールを使うリポジトリでは強制しにくい。うちもリポジトリ一つはこの制約のせいで共有チェックアウトを維持していて、だから残る二つの防衛線が要る。 - コミット直前にブランチを確認し直す。 セッションが作業を始めるときに「自分はこのブランチで働く」と記録しておき、コミット直前に現在のHEADと突き合わせる。違っていたらコミットを止めて、まず状況を把握する。ルールは拍子抜けするほど単純だが、ブランチ乗っ取りの事故はすべて「コミットする瞬間のHEADが、自分の知っているHEADではなかった」から起きていた。人間ならプロンプトにブランチ名が出ていて気づくだろうが、エージェントは明示的に指示しなければ確認しない。
- 検証ゲートを自分の変更にスコープする。 セッション終了ゲートが検査する範囲を、リポジトリ全体ではなく、そのセッションが実際に修正したファイルに絞る。他人のWIPに自分の終了を阻まれることも、ゲートを通過しようと他人のコードに手を出す誘因も消える。リポジトリ全体の健全性は、どのみちCIがコミット済みの状態を基準に見直す。セッションのゲートまで全体を見る必要はなかった。
ここに運用ルールを一つ載せた。セッションを立ち上げる前に、いま開いているブランチとPRをざっと眺めて、スコープが重なっていないかを見る。二重実装はgitが防いでくれる問題ではなく、仕事の割り振りの問題なので、ツールではなく習慣で防ぐしかなかった。
並行性の問題はデータベースだけのものではなかった
整理してみると、見慣れた図だ。共有リソース、ロックなしの同時アクセス、レースコンディション、そして分離レベル。私たちがデータベースとマルチスレッドのコードで数十年かけて学んだまさにあの問題が、作業ディレクトリの上で再演されたのだ。
エージェント一つと働くとき、この問題は存在しない。人間とエージェントが交互に働くときも、人間が暗黙のうちに調整役を務めている。問題は、エージェントが複数になり、人間が調整の手を離した瞬間に始まる。その時点から作業ディレクトリは並行性制御が必要な共有リソースであり、隔離なしで回せば、データベースがそうだったように必ずデータを失う。
並列エージェントを諦めろという話ではない。いまも毎日セッションを何個も立ち上げている。変わったのは一つだけ。もう彼らを同じ部屋には押し込めない。
関連記事
- 💻 開発
AIが直したコードを別のAIに反証させるパイプライン
同じモデルに自分のコードをレビューさせると、自分の出した結論をまた承認する。ほぼすべてのコードをAIと一緒に書く1人開発者が、一つのAIの仕事を別のAIに「反論してみろ」とぶつけるクロスチェックのループを常設パイプラインに固めた話。セルフレビューが見逃したバグを3ラウンド連続で捕まえた実例とプロンプト設計。
- 💻 開発
AIが「もう終わりました」と言ったとき、それを鵜呑みにしない仕組み
AIコーディングエージェントは、仕事を終えていなくても終わったと言う。テストを走らせずに通ったと言い、再現もせずに直したと言う。ほぼすべてのコードをAIと一緒に書く1人開発者が、完了を言葉ではなく証拠でしか認めないようにした検証ゲートの話。
- 💻 開発
エージェントの「完了」を信じないことにした - verify.shゲート、prove-it
AIコーディングエージェントはテストを回していないのに「テスト通過」と報告する。嘘ではなく構造の問題だ。報告を検査に変えるオープンソースのゲートprove-itを作りながら学んだこと - そして自作の5行フックが静かに失敗する4つの形。