AI向けにコマンド出力を最適化するCLIプロキシ 後編 rtkの誤判定を減らす除外設定
rtkがlintスクリプトをESLint専用のフィルタへ書き換えた際、成功している実行を「失敗」と誤判定した例を取り上げます。原因の切り分け方と、削減の実測をもとに圧縮させる範囲を絞った設定を紹介します。
前回まで
前回は、コマンドの出力をAI向けに圧縮するrtk(Rust Token Killer)が、どれだけトークンを減らしているかを測りました。rtk gainが示すコマンド出力の削減率は50.2%で、公式が掲げる60〜90%には届きません。モデルに渡らない出力を差し引くと、入力全体に対する削減は20%ほどでした。
その20%も、圧縮による間違いが頻発すれば失われてしまいます。今回は、筆者の環境でrtkがコマンドの中身を取り違えて起きた事故と、それを予防するために圧縮させるコマンドを絞った設定を紹介します。
圧縮が成功を失敗に変える
rtkの圧縮は、コマンドごとのフィルタが担っています。出力のどこを残すかはフィルタの判断次第で、当たっていれば必要な部分だけが残ります。外れると情報が欠けるだけでなく、結果そのものを変えてしまいます。
筆者が遭遇したのは、記事の原稿を管理するリポジトリで、rtkが成功しているlintを失敗として判定する問題でした。このリポジトリのnpm run lintは、Markdownの校正のためにtextlintを実行します。ところがrtkのフックは、npm run lintだけをESLint専用フィルタのrtk lintへ書き換えていました。npm run testのようなほかのスクリプトが汎用のrtk npm run testに書き換わるのに対し、lintという名前からESLintと決め打ちされるわけです。
rtk lintは、プロジェクトのlintスクリプトではなくeslintを直接呼び出し、その出力をJSONとして解析します。このリポジトリにESLintはないため、出力が空になって解析に失敗します。その結果、本来は成功(exit 0)しているlintが、エラー(exit 1)として報告されていたのです。成功が失敗に見えるのですから、情報の欠落より深刻です。
これはtextlint固有の症状ではありません。ESLintからoxlintへ移行したプロジェクトでも同じJSON parse failedが報告されており(rtk-ai/rtk#2154)、執筆時点で修正はリリースされていません。lintという名前をESLintと決め打ちする限り、ESLint以外のリンターを選んでいれば同じことが起こります。
書き換えそのものが失敗するぶんには、まだ穏やかです。その記録はrtk gain --failuresで追えます。筆者の履歴に残る143件のうち95件は、grep -rやgrep -Eのようにrtkが解釈できないオプションによるものでした。これらは98.6%が生実行へのフォールバックに成功していて、結果は変わりません。危険なのは、失敗せずに誤った結果を返すrtk lintのほうです。
決め打ちフィルタへの対処
対処は、該当コマンドを書き換えの対象外にする設定です。ファイルの場所はrtk configを実行すると表示されます(macOSでは~/Library/Application Support/rtk/config.tomlでした)。この時点では、問題が出たコマンドだけを外しました。
問題が出たコマンドを対象外にする設定
[hooks]
exclude_commands = ["npm run lint", "pnpm lint", "pnpm run lint"]
そもそもnpm run lintの中身は、ESLintとは限りません。筆者のプロジェクトではoxlintやtextlintを使っていて、ESLint決め打ちの書き換えは誤判定しか生みません。逆に、ESLint中心に使っている人は除外を入れると、npm run lintからの削減が得られなくなります。
補足:別のエラー回避方法
エラーを回避する別の方法として、RTK_DISABLED=1 npm run lintのように環境変数を指定して特定のコマンドだけrtkを無効化したり、npm run textlintのようにスクリプト名を変えてrtkの書き換え対象から外したりする方法もあります。
設定後は、書き換えが起きないことをrtk hook checkで確かめられます。
除外後のrtk hook check
$ rtk hook check "npm run lint"
No rewrite for: npm run lint
除外したコマンドは、rtkから見れば取りこぼしになります。rtk discoverはClaude Codeの履歴を読み、rtkを通せば削減できたコマンドを一覧する機能です。実行すると、過去57セッション・1,362コマンドのうちnpm run lintが39回、この一覧に並びました。
フィルタ回避を常用しない
では、失敗と判定されたlintを前にして、AIはどう振る舞っていたのでしょうか。
rtkにはrtk proxy <コマンド>があります。フィルタを一切適用せず、生の出力をそのまま返し、利用の記録だけを残します。エラーの詳細を調べるために圧縮前の出力を見る、といった使い道があります。
筆者の環境のClaudeは、rtkが成功を失敗として判定するたびにrtk proxy npm run lintを実行していました。これは生の出力と比べて原因を切り分けるための、適切な使い方です。一方で、「このリポジトリのnpm run lintはrtk proxy経由で実行する」という運用を、セッションをまたいで残るメモリに保存してもいました。調査のための回避策が、そのまま恒久のルールになりかけていたわけです。AIエージェントはうまくいった手順を次回にも使おうとするので、原因が設定のずれにあっても、回避策だけが残ることがあります。
rtk proxyで実行したコマンドに圧縮はかかりません。原因を調べるために生の出力が必要なら、圧縮されないこと自体は問題ではありません。ただし同じコマンドで繰り返し使われるなら、設定を疑ったほうがよいでしょう。筆者がフィルタのずれに気づけたのも、エージェントのログにproxyが頻繁に現れていたからでした。rtk gain --historyの履歴で目立ち始めたら、生の出力と比べてずれを確かめ、exclude_commandsに追加します。
削減が出る範囲へ絞る
削減は公式の掲げる幅に届かず、設定の調整も必要でした。それでも筆者はrtkをやめず、圧縮させる範囲を絞りました。
前回同様にコマンド別の集計を見直すと、削減はVitest、ps、ls、cat、find、grepに集まっています。小さいほうは、npmスクリプトの6KB、ghの7KB、tscの0.8KBで、npxとcurlはほぼ0です。
この集計中のgitは23.9%ですが、内訳を見てみるとgit addとgit pushは差し引きでマイナス、git switchは0でした。git diffは249回の実行で19.8%と、率が低いままです。
削減が出ないコマンドでも、フィルタが実態とずれれば誤判定の危険だけは残ります。実際、成功のlintが失敗として報告されたのはnpm run経由で、圧縮で行が削除されてAIが取り直したのはgit diffでした。どちらも削減の小さい側です。
git diffを外すなら、gitを丸ごと対象から除いたほうが抜け漏れは起きませんが、ここではサブコマンド単位で外していくことにしました。理由は、gitの中で削減の大きいものと小さいものが分かれていたからです。git pullは95.0%、git commitは84.9%で、Vitestに次ぐ削減率です。git logは率では29.3%ですが、量では41KBあります。gitを一括で外すと、手放す削減は239KB、対象から外れる実行は2,551件になります。
そこで、削減がほとんど出ないコマンドと、実際に情報が欠けた2つを書き換えの対象から外しました。
削減が出る範囲に絞った設定
[hooks]
exclude_commands = [
"npm",
"pnpm",
"npx",
"gh",
"curl",
"tsc",
"git diff",
"git push",
"git add",
"git switch",
]
除外の指定は語の区切りでの前方一致です。git diffを入れるとgit diff --statも対象から外れますが、git difftoolは書き換わります。意図どおりに外れているかは、ここでもrtk hook checkで確かめられます。
前方一致の確認
$ rtk hook check "git diff --stat"
No rewrite for: git diff --stat
$ rtk hook check "git difftool"
rtk git difftool
$ rtk hook check "git status"
rtk git status
この10項目で失う削減は123KB、全体の3.6%です。大半はgit diffの106KBですが、それでも3%です。代わりに、rtkを通る実行は1,457件減ります。事故が起きた2つもそこに含まれます。
実行の数では3分の1を占める10項目が、削減量ではわずかです。削減の96%を保ったまま、誤判定の起きうる範囲を狭められます。
残したコマンドでは、--quietや| tailをエージェントへ頼む必要がありません。プロンプトでの指示は無視されることがありますが、フックは毎回同じように書き換えます。
Claude Codeは長い出力の中央を省略するので、欠けた部分を求めてAIが追加のコマンドを走らせることがあります。圧縮がかかっていれば同じ内容が短く収まり、上限に届きにくくなります。想定より大きな出力が返ってきたときの備えとしては、まだ利点があると考えています。
まとめ
人間向けのCLI出力をAI向けに変換するというrtkの発想は合理的です。ただ、公式が掲げる削減幅は筆者の環境では再現しませんでした。
手間もかかります。ESLintと決め打ちしたフィルタが、textlintやoxlintを使うプロジェクトで成功しているLintを失敗に変える例を紹介しました。生の出力と比較して原因を切り分け、exclude_commandsを書き、rtk hook checkで結果を確かめる作業が必要です。入れて放っておけるツールではありません。
そのため、誰にでもすすめられるツールだとは考えていません。それでも筆者は、削減が出ると確認できたコマンドだけを残して使い続けています。テストを繰り返し回す作業や、プロセスやファイル一覧の確認では、出力の大半が定型なので大きく減ります。Git操作中心のレビューやAPIの応答を読むときは、ほとんど変わりません。どれだけ得をするかは、扱うタスクの中身に左右されます。
出力が特に冗長なコマンドにAI向けの簡潔なモードが備われば、こうしたラッパーはいずれ不要になります。それまでは、有効な場面を見極めて使ってみるのもよいのではないでしょうか。