「コードは悪くないやつだ」——そう思うのに、手が止まる。
ビルドが通らない。評価役のAIは正直に「ビルドが失敗しています」と報告してくる。ところが同じコードを本番のCIに乗せると、あっさり緑になる。落ちていたのはコードではなく実行環境のほうだった——。
AIエージェントに実装から評価まで任せる自動化を組んだことがある人なら、この空振りに覚えがあると思います。コードは無実だと頭では分かっているのに、ループは律儀に止まる。エージェントは嘘をついていません。手元では本当にビルドが失敗している。その失敗は、コードの欠陥ではなく隔離環境(sandbox)がプロセス生成やポートバインドを制限しているせいだったりします。
問題は、ループがこの2種類の「赤」を区別しないことです。区別しないと、環境のせいで落ちただけの結果を「バグあり」とみなして、ループが無駄に止まるか、直す必要のないコードを無駄に直し始める。どちらも自動化で取り戻したはずの時間を静かに溶かします。
AIに評価まで任せたとき、この「環境のせいの赤」にどう向き合えばいいのか。それが今回の主題です。
この記事は、私たちの自動開発ワークフロー(社内では dev-flow)を Claude Code(Pro $20/月、Max $100-200/月)上で運用している記録です。直近の改修で「環境のせいの失敗」を仕分けて空振りを止めた過程を、コミット履歴ベースで振り返ります。
この記事で学べること
- 評価時の指摘を「環境要因」と「コード欠陥」に分類する設計
- 環境要因を、CIが緑という決定論的証拠だけで機械的に閉じる仕組み
- 判定できないときは閉じずに人間へ残す fail-open の考え方
- CI連携でハマった、CI未設定PRの独特な挙動対処
前提:その赤は誰のせいか
以前、品質チェックを任せたAIが、チェックの攻略を始めたという記事で、品質ゲートが甘すぎてすり抜けられる話を書きました。テストを緑にしろと指示すると、AIはテストのほうを弱体化させて緑にしにくる。ゲートが空振りして赤にすべきものを緑で通す方向の失敗です。
今回は逆方向の失敗を扱います。ゲートが厳しすぎて緑にすべきものを赤で止めてしまう。原因はAIのズルではなく実行環境のノイズです。「ゲートの校正」でも向きが真逆です。
具体例を挙げます。隔離環境の中で本番ビルドを走らせると、プロセス生成やポートバインドの制限で内部エラーで決定的に失敗することがあります。実運用では、次世代バンドラのビルドが TurbopackInternalError (os error 1) で毎回こける既知事象がありました。コードは何も悪くない。ただこの環境ではそのビルド経路が使えないだけです。
エージェントから見ると、これは「ビルド失敗」という赤い結果でしかありません。人間なら「あ、sandboxのやつね」と一瞬で読み替えられる。その読み替えをループにも持たせる必要があります。
まず仕分ける
最初にやったのは、評価役エージェントの指摘を表示前に分類することでした。
それまでは評価の指摘を一律で並べていました。sandbox 由来の環境要因(ビルドが環境制限で落ちた等)も実コードの欠陥も同じ「concern」として同じ場所に表示されます。レビューする側は毎回、本物の指摘とノイズを目で選り分けることになります。指摘が10個あっても6個が環境ノイズだと残り4個の重要度が薄まってしまう。
そこで、指摘を決定論的なルールで2つに振り分けました。
- 環境要因: 既知パターン(sandbox のビルド失敗、権限エラーなど)にマッチするもの。折りたたんだ「環境ノート」へ回す
- コード欠陥: それ以外。従来どおり対応必須の指摘として残す
ポイントは、この振り分けをLLMの判断に委ねなかったことです。分類ロジックは既知パターンとのマッチングだけで動く決定論関数にしました。LLMに毎回聞くと判定がぶれ、ぶれること自体が別のノイズになる。仕分けの基準は揺れない場所に置くべきです。
これで、レビュー時に見るべき指摘とそうでないものが視覚的に分離されました。ただ環境ノートに回しただけでは「注意喚起」が残り続けます。次に必要なのは、この環境ノートを根拠を持って閉じる手段です。
決定論で閉じる:CI の green を「証拠」にする
環境ノートに「sandboxではビルドできませんでした」とあるとして、本当にコードの問題ではないと、どう証明するのか。
答えは、エージェントの手元ではなく本番のCIに聞くことでした。sandbox で検証できないなら、ちゃんとしたCI環境で同じビルドが緑かを見ればいい。緑なら、その環境ノートは「コードは問題なかった」という決定論的な証拠付きで閉じられます。
実装では、マージ手前の判定時にビルド系のCI checkの合格状態を取得し、該当項目を機械的にチェック済みにしました。ここでも判定はLLMに委ねていません。この意図をこう明文化しています。
buildChecksGreen(決定論)のみで LLM に委ねず
この閉じる操作には厳しい制限を付けました。CI green を根拠に閉じてよいのは、あらかじめ許可リストに載せた項目だけです。無条件に「緑だから全部OK」とはしない。証拠が意味を持つ範囲を明示的に絞ったわけです。
環境ノートの表には「状態」列を足し、CIで確認済みの項目には印を付けました。レビューする人が「なぜ閉じたのか」を後から追えるようにするためです。
一般化するとき、誤爆が牙をむく
最初はビルド専用だったこの仕組みを他の環境要因にも広げたくなります。テストランナーが sandbox に入っておらず検証できなかった、というノートも、CIで該当テストが緑なら同じ理屈で閉じられるはずです。
そこで、green 判定を一般化しました。実装メモには狙いをこう書き留めています。
env_key ごとの check-name regex 判定 (envChecksGreen) に一般化
環境要因の種類ごとに「どのCI check名を見れば緑と判断できるか」を正規表現で持たせ、ビルド以外も同じ経路で閉じられるようにした改修です。
ここで派手にやらかしました。判定を広げた分岐が、汎用的なテストcheckが緑なだけで「緑だ」と判定してしまったのです。無関係な緑を根拠に環境ノートを閉じてしまう。偽の緑です。
さらに、check名を部分文字列マッチで探していたため、たまたま部分一致する無関係なcheckにも反応しました。実装メモにこう書き留めてあります。
部分文字列マッチで無関係な check(Contest Deploy 等)にも誤爆
test を含むcheckを探したら、コンテスト用デプロイの Contest に反応した、というオチです。誤爆で勝手に閉じられると本物の問題が緑の顔をして通ってしまう。
修正では、check名のマッチをそのテストランナーを指す表現だけに絞り込みました。対象のcheckが見つからないときは無理に判定せず据え置くようにし、回帰テストで固定しました。「マッチが無い」を「緑」と読み替えないこと。
実世界のCIは素直じゃない
CIの合格状態を取りに行く、と書きましたが、現実のCLIの挙動は思ったより素直ではありませんでした。
CI check一覧の取得コマンドは、checkが未設定のPRに「空っぽです」と返すと思っていました。ところが実際は終了コード1(=失敗)と一緒に、標準エラーへ次のメッセージを返す挙動でした。
no checks reported on the '<branch>' branch
これに最初は引っかかりました。「配列が空なら check なしと判定する」分岐を書いていましたが、そもそも空配列が返ってこない。終了コード1が返るので一律「本物のAPIエラー」扱いになり、CI確認ステップが error で止まる。しかも状態は確定しているのにリトライで無駄に待つおまけ付きでした。
対処はシンプルです。リトライループの中で「終了コード1、かつ標準エラーに no checks reported を含む」ケースだけを特別扱いします。これを「checkなし(確定)」と判定して即座に抜ける。本物のAPIエラーは従来どおりエラーのまま残します。
CI未設定のPRは「CIによる異議なし」として扱えばいい。それをCLIの終了コードの都合で error に落とさないための地味だけど効くパッチでした。外部ツールの終了コードは意味ではなく都合で決まっていることがある。
判断できないときは、閉じない
ここまでの仕組みには一本の背骨が通っています。判定できないときは安全側に倒すという方針です。
CIの取得に失敗した。checkがまだpending(実行中)だ。該当するcheckがそもそも存在しない。こうした「緑とも赤とも言い切れない」状況では環境ノートを閉じません。据え置いて人間に残します。これを fail-open と呼びます。open は「自動で閉じない=人間に委ねる」という意味です。
つい「全部自動で判定しきりたい」と欲が出ますが、確信度が低い領域まで機械で閉じにいくと静かな誤判定が積み上がる。機械が自信を持って言えることだけを機械にやらせ、それ以外は人間への出口に流す(PRのマージ前に人間のレビューを残す設計にも書きました)。
fail-open は一見「詰めが甘い」ように見えますが、自動で閉じた誤判定は誰も気づかないのに対し、人間に残した判断は誰かの目に触れます。気づけない失敗より、気づける手間のほうがましという優先順位です。
原因を1つに潰さない
最後に、この一連の改修の底に流れている考え方を1つだけ。「同じ見た目の警告を、原因ごとに分ける」というものです。
環境要因とコード欠陥を分けたのもその一例でした。もう1例、評価時点とPRの最終状態がズレているときに出る「内容が古いかも」という警告があります。以前は単純な真偽値で「本当にズレている」のか「自動修正済み」なのかを区別できず、どちらも同じ警告になっていました。
これを原因を分けた列挙型に置き換えました。実装上はこう表現しています。
none/hash_mismatch/iterate_incomplete/
ズレなし・ハッシュ不一致・反復未収束・修正適用済みの4つに分けます。最後の「修正適用済み」は再レビューで担保済みなので、警告でなく情報として区別する。同じ「⚠️」に見えていたものを対応の要否で仕分けたわけです。
sandbox のビルド失敗に fallback 規約を用意したのも根は同じでした。環境制限でこけるなら別のビルド経路に切り替えて検証してよい、という規約を決めておくわけです。
警告やエラーをまとめて放り込むと、ノイズに埋もれて本物が見えなくなります。原因ごとに分けてそれぞれに合った扱いを与える。地味ですが、この仕分けの精度がそのまま「無駄に止まらない・無駄に直さない」に効いてきます。
まとめ
AIの自動開発ループで評価が赤くなったとき、その赤が「コードのせい」か「環境のせい」かをループ自身に仕分けさせる設計を紹介しました。
- 評価の指摘を決定論的ルールで環境要因とコード欠陥に分類し、環境要因は折りたたむ
- 環境要因は、本番CIが緑という証拠がある場合だけ許可リスト限定で機械的に閉じる
- 一般化するときは部分文字列マッチの誤爆に注意し、対象が見つからなければ据え置く
- CI未設定PRのような外部ツールの独特な挙動は終了コードだけでなく中身まで見て判定する
- 判定できないときは閉じずに人間へ残す(fail-open)
環境のノイズを欠陥として扱わないことは、AIをサボらせることではありません。ループが本当に見るべき問題に集中できるようにするための地ならしです。仕分けの基準を揺れない場所に置き、機械が自信を持てる範囲だけを機械に任せる。この線引きが、自動化を「速いけど信用できないもの」から「速くて任せられるもの」に変えていきます。



