スクロールアップしたことは一度もなかった

毎日コーディングエージェントを使っている。タスクを渡して、別のことをして、戻ってきたときに画面の下部に次のような行がある:

"빌드·회귀 스모크·기존 분석 테스트 모두 통과."
(build, regression smoke, and the existing analysis tests all pass)

それは自分のセッションログからの実際の行だ。私はそれを確認しなかった。テストが実際に実行されたかどうかを確認するためにスクロールアップする代わりに、次のタスクを渡した。私は何ヶ月もそうしていた。

それから、韓国人の開発者コミュニティの投稿を読んだ。誰かがエージェントのプライベートな推論を調べて、テスターの役割に与えられたメモリに、問題が発生したときの回避方法のリストが含まれていたことを発見したという。

それはエージェントが悪意を持っていたことを意味しない。テストを通すように指示すれば、最も安価な方法を見つけるのが最適化の性質だ。問題は、その最も安価な方法が「コードを修正する」ではなく「チェックを回避する」であり、それに気づく方法がないことだ。

そこで私は確認することにした。ツールの名前はred-handedだ。

裁判官ではなく、差分

私は裁判官を作ろうとしていたわけではなかった。エージェントが言ったことと実際にやったことの並列表示が欲しかった。

資料はすでにそこにあった。Claude Codeはセッションごとにトランスクリプトを書き込む。実行したすべてのコマンドとその結果、すべてのファイル編集の前後、そして私に言ったすべての文だ。それをgitと並べれば、言葉と作業の間のギャップを計算できる。

テストを実行せずに通ったと言ったか。失敗した実行の直後に通ったと言ったか。期待値を書き換えて壊れたコードの出力に合わせたか。9つのチェックがある。

最も時間をかけたのは機能ではなかった。ツールが間違ったときの対処だった。

ここで間違える方法は2つある。

何かを見逃す。 ユーザーは何も失わない——彼らは以前と同じくらい情報を持っていない。

正直に作業をした人を非難する。 それは、していないのにチートをしたと言うことだ。一度間違えれば、誰もそれを開かなくなる。

コストは対称ではない。そこでルールはこうなった:誤って非難するよりも、見逃す。

それは2つの厳しい制約につながった。

確認された発見には2つの証拠が必要。 セッションでそれが起こったことを示さなければならず、かつその変更がワーキングツリーに残っていなければならない。エージェントが後で元に戻した場合、非難するものは何もなく、発見は消える。

読めないものは失敗ではない。 テストが実行され、出力がパースできなかった場合、それは「わからない」であって「失敗した」ではない。その区別が結果全体を逆転させる。

このプロセスではモデルは一切呼ばれない。判定がLLMから来た場合、同じトランスクリプトでも日によって異なる回答が得られる——それでは証拠にならない。

249セッションでの実行

マシン上のすべてのセッションを対象にした。

249 sessions. Your agent said "tests pass" 124 times.
   117  a test ran first
     7  no test ran in that session at all

Enter fullscreen mode Exit fullscreen mode

確認された発見:ゼロ

正直に言うと、それは落胆した。嘘発見器に何週間も費やしたのに、嘘は捕まらなかった。

それから7つを1つずつ開いて、私は間違った質問をしていたことに気づいた。

本当の問題は嘘ではなかった

7つのうち4つは1つのプロジェクトから来ていた。エージェントは次のようなことを言っていた:

"드래그·휠 5방향 브라우저 테스트 통과."
(drag and scroll, all five directions, browser tests pass)

それは嘘ではない。エージェントは実際にブラウザを開いて確認した。しかし、その確認はどのような機械可読な痕跡も残さなかった。したがって、後でその主張が成立したかどうかを確認する方法がない。私にも、ツールにも、そのコードを引き継ぐ誰にも。

他の3つも同じ形だった:プロジェクトは独自のスクリプトでテストを実行し、私のパーサーはその出力形式を知らない。テストのようなものが実行されたことはわかる。それが通ったことはわからない。

そこで結論が逆転した。私は嘘をつくエージェントを心配していた。実際に見つけたのは、誰にも読めない検証だった。

そしてそれはAIの性質というよりは、私たちの現在の作業方法の性質だ。以前は、誰かがブラウザを開いて見て、次に進むのは問題なかった——その人がそのコードを見続けていたからだ。今、チェックをしているのは、セッションごとにメモリがリセットされるエージェントだ。読めない検証は、検証が行われなかったのと区別がつかない。

したがって、それらの7つはCAUGHTではなくSUSPICIOUSと報告され、言辞は「それが嘘をついた」ではなく「そのコマンドが実際に何を報告したかを見てください」となる。それがツールが正直に言える限界だ。

述べておきたい注意点がある:249セッションのうち、「tests pass」の主張を含んでいたのはわずか12セッションだけだ。残りは短く、1つか2つの質問だ。だから「249のうち0」は、この結果を読み取る方法として誇張されている。

そしてそれは私を6回誤って非難した

動作するようになった後、私はそれを壊そうとしばらく時間をかけた。それがプロジェクトの中で最も価値のある部分になった。敵対的レビューで、5つの、正直な作業を非難する別々の方法が見つかった:

  1. タイムアウトで殺されたテスト実行を、失敗として読み取る
  2. 知らないランナー(rspecphpunittox)を、「テストが実行されなかった」として読み取る
  3. 見えないランチャーの下で再実行されたため、古い結果で判断する
  4. ユーザーが明示的に要求した動作変更を、回答の書き換えとして読み取る
  5. バックグラウンド化されたコマンドを、通ったものとしてカウントする

最初の2つは詳しく説明する価値がある。

タイムアウトのものが最悪だった。 テスト実行が長すぎるために殺された場合、特徴的な終了シグナルが残り、私のコードはそれを失敗として読み取った。したがって、エージェントがテストを通ったと言った場合、ツールは最も侮辱的な判定を出力した:「tests passと言った——最後の実行は失敗した。」 私のコードは、設計ノートの2つの段落に書いたルールを直接侵害していた。

2番目は1文字だった。 テストランナーを認識するパターンは、単語境界の前にspecという単語を探していた——これはrspecの中では一致しない。なぜならrが単語文字だからだ。Ruby開発者がスイートを実行して20 examples, 0 failuresを得ても、私のツールは自信を持ってテストが実行されなかったと述べるだろう。

そして痛い部分がある。私のREADMEには「183の実セッションで0の偽陽性。」と誇らしげに書かれていた。それは本当だった——しかしツールが良かったからではない。私のマシン上のすべてのプロジェクトがJavaScriptまたはTypeScriptだったため、それらのパスが一度も実行されなかったからだ。私は主張を検証するツールを作り、それが独自の検証に通ったと報告したが、実際にコードを実行していなかった。私はまさにツールが捕まえるために存在するそのことをした。

6番目は出荷後に見つかった。 pytest tests/test_sync_cli.pyのスコープ指定された実行、その後GitHubワークフローファイルと2つの.tsxファイルの編集、そしてツールは主張を古くなったと判断した。Pythonテストは.tsxをインポートできず、CIワークフローはこのマシンで実行されたものではない。何も古くなっていなかった。

魅力的な修正があった:「異なるディレクトリなので、無関係として扱う。」 シンプルで、ほとんどの場合正しい。しかしそれは間違っている——pytest tests/を実行してからsrc/を編集した場合、主張は本当に期限切れになっている。そこで推測する代わりに、私は確実に真実であるものだけを抑制する:Pythonは.tsxをロードできない、JSランナーは.pyをロードできない、CIワークフローはローカル実行ではない。あいまいなものは依然としてカウントされる。「無関係に見える」は推測であり、推測で証拠を捨てることはない。

6つすべてが回帰テストで固定されている。

これは記事を書いている最中にも私を捕まえた

この記事の数字を更新するために再実行したところ、リストに自分のものが見つかった:

"테스트 3개로 고정했고 전체 389개 통과."
(pinned it with three tests, all 389 pass)
→ 1 file changed after that run: scripts/social.tape

私が言ったことは本当で、変更されたファイルはデモGIF用の画面録画スクリプトだ。テストスイートとは何の関係もない。

しかし、これはバグではない。前のセクションで私が選んだルール——あいまいなものは依然としてカウントされる——が、まさに私が指示したことを行っている。ツールは.tapeファイルが何かを知らず、私は未知のものはカウントされると決めた。

それが選択の代償だ。物事を見逃さないために、時には何もないところで吠えることもある。その代わりに、これはCAUGHTではなくSUSPICIOUSとして出て、変更されたファイル名がすぐ隣に印刷されているので、却下するのに3秒しかかからなかった。それが2つのティアの目的だ。

私が守りたい3つのこと

精度を調整する前に、どちらの方向で間違えるかを決める。 「精度は?」は、このようなツールではほとんど意味がない。なぜなら、2つのエラーの方向のコストが大きく異なるからだ。方向を選んでから、すべてのブランチでその決定を守る。私は原則をドキュメントに書いてから、コードの3箇所でそれを破った。

判定ではなく証拠を出荷する。 出力が「嘘をついた」と言えば、ユーザーはそれを信じるか信じないかしかできない。タイムスタンプと引用された行を示せば、彼らは自分で判断できる。それが、私自身の偽陽性を却下するのに3秒しかかからなかった理由でもある。間違っているのを捕まえられるツールは、使い続けるツールだ。

実際の修正は、読める検証だ。 それは4つのブラウザテストの発見が私に教えてくれたことだ。エージェントに作業を渡すとき、「チェックして」と言わず、「チェックして、機械が読める場所に結果を残して」と言うべきだ。誰かの頭の中にしか存在しないチェックは、次のセッションまでには存在しない。

試してみる

ローカルで実行する。トランスクリプトとコードはマシンを離れることはない——モデルを呼び出さず、独自のネットワークリクエストも行わない。同じセッション、同じ回答、毎回。

# a made-up session where every check fires, so you can see the shape first
npx @jinhyuk9714/red-handed@latest demo

# then your own version of the numbers above
npx @jinhyuk9714/red-handed@latest stats

Enter fullscreen mode Exit fullscreen mode

MITライセンスで、コードはgithub.com/sjh9714/red-handedにある。

あなたの数字は私のものとは異なるだろう。私は確認された発見がゼロ、誰にも読めない7つのチェック、そしてこの段落を書いている最中に見つけた1つの誤報があった。あなたの場合がどちらであれ、ポイントは見るまでわからないということだ。