AIエージェントの「確認しました」が嘘になる3箇所
型チェックが別のプロジェクトを見ている。終了コードを一度も読めていない。ステージから外したはずの変更が本番に出る。どれもエージェントは正常終了し、成功の形をした報告だけが残る。
AIコーディングエージェントに検証まで任せると、「型チェックが通りました」という報告が返ってくる。その報告が事実と食い違う経路を3つ踏んだ。いずれもエージェントは正常終了していて、嘘をついているつもりもない。測り方のほうが壊れている。
1. 型チェックが、別のプロジェクトを見ている
事象
エージェントに参考実装を見せようとして、別のリポジトリへ寄り道させた。戻ってきて型チェックを走らせ、「通りました」と報告する。実際にはそのプロジェクトの型チェックは走っていない。
原因
多くのエージェント実行基盤では、シェルの作業ディレクトリがツール呼び出しをまたいで持続する。cd ../other-project した状態が、次のコマンドにも残る。
一方で、ファイル編集ツールは絶対パスで動く。だから編集は正しく当たる。ズレるのは検証コマンドだけだ。
cd ../other-project # 参考実装を見に行った
# …数ターン後…
npx tsc --noEmit # ← other-project を型チェックしている
タチが悪いのは、結果がたまたま同じになること。どちらのプロジェクトも型チェックが通るなら、最後まで気づかない。壊れているコードを「通った」と報告したまま先へ進む。
解決策
検証コマンドは、今どこにいるかとセットで実行させる。
pwd && npx tsc --noEmit
出力の先頭にパスが出るので、違っていれば目で拾える。エージェントに任せるルールとしては「cd を挟んだら、以後の検証は対象ディレクトリを明示し直す」を書いておくのが確実だった。
2. 終了コードを、一度も読めていない
事象
エージェントが出力の長いコマンドを走らせるとき、末尾だけ切り出しつつ結果を確認する形をよく書く。
npm run build | tail -20; echo "exit: ${PIPESTATUS[0]}"
返ってくるのはこれだ。
exit:
数字が無い。それなのに報告には「終了コードを確認し、正常終了を確認しました」と書かれている。
原因
$PIPESTATUS(パイプでつないだ各コマンドの終了コードが入る配列)は bash の変数で、macOS の既定シェルである zsh には存在しない。zsh 側の名前は小文字の $pipestatus で、しかも添字が 1 始まりになる。
${PIPESTATUS[0]} は zsh でエラーにならない。未定義の変数を読んだ扱いになって、静かに空文字を返す。
ここで効いてくるのが | tail -20 のほうだ。ビルドが失敗していても、エラー本文は上のほうにあるので画面外に出ている。手元に残るのは「末尾20行の、それらしい出力」と「空の終了コード」だけ。人間が読んでもエージェントが読んでも、成功に見える。
解決策
パイプを挟まずに素の $? を取り直すのがいちばん短い。
npm run build > /tmp/build.log 2>&1; echo "exit: $?"; tail -20 /tmp/build.log
zsh のまま書くなら添字を合わせる。
npm run build | tail -20; echo "exit: ${pipestatus[1]}"
一般化するとこうなる。終了コードの表示が空だったのは、コマンドが成功した証拠ではなく、測定に失敗した証拠。空を見たら測り直す。エージェントへの指示にも同じことを書いておくと、次から自分で測り直すようになる。
3. ステージから外した変更が、他方の push に相乗りする
事象
1つのリポジトリで、エージェントのセッションを2つ並行させていた。片方で「無関係な変更は巻き込まない」と判断して、意図的にステージから外したファイルがある。
コミットして、確認のために少し離れて戻ってきたら、HEAD(今いる位置)が別のコミットまで進んでいて、すでにリモートと一致していた。自分のコミットは3つ下に埋まっている。外したはずのファイルも一緒に本番へ出ていた。
原因
作業コピーを共有している以上、ステージング領域(.git/index)はセッションごとに分かれていない。片方が「外す」と決めても、それはそのコミットまでしか効かない。次に誰かが git add -A すれば入る。
そして自分のコミットは、もう片方の push に相乗りして一緒に出ていく。「まだ push していない」つもりのまま、本番のデプロイが走る。
もう一段ややこしいのが確認方法のほうだ。
git log origin/main..HEAD # ← これを信用すると取り違える
origin/main はローカルに置かれたリモートの控えでしかない。もう片方のセッションが push したり fetch したりすると、この控えが勝手に更新される。「もう入っている」「まだ入っていない」の判定が裏返る。
解決策
push 前に、控えではなくリモート本体を見る。
git log --oneline origin/main..HEAD # ローカルの控えとの差
git ls-remote origin refs/heads/main # リモート本体の実際の位置
両方を突き合わせれば、取り違えは起きない。
運用としては、作業コピーを共有したまま並行させないほうが早い。git worktree で物理的に分ければ、index もブランチも分かれるので、この問題自体が消える。
git worktree add -b feature-b ../repo-feature-b
分けられない事情があるなら、せめて「自分が触っていないファイルが勝手にコミットされうる」前提で、push 直前に差分の中身を数える。
共通する形
3つを並べると、壊れ方が同じところに集まっている。対象が違う、測れていない、後から書き換わる。どれも検証の結果ではなく、検証の足場のほうが崩れている。だから出力を何回読み返しても見つからない。
対策も揃った。検証コマンドには、必ず「何を測ったか」を一緒に吐かせる。 pwd を前に付ける、終了コードを数字で出す、push 前にリモート本体を引く。全部これの言い換えだ。
エージェントに任せる範囲が広がるほど、こちらが読むのは実行結果ではなく報告文になる。報告文は、測れていないときも同じ顔で返ってくる。