「呼び出し元を全部書き換えてください」と頼んだ翌朝、一箇所だけ古いままの行が残っていました。
エージェントの作業ログを遡ると、書き換え自体は丁寧でした。問題はその手前です。エージェントが最初に投げた検索が、その行を拾っていなかった。手を動かす前の一手で、対象が静かに欠けていたわけです。
修正漏れは目で見つかります。検索漏れは、見つかりません。何も返ってこなかったのではなく、返ってきたものが全部だと信じてしまうからです。
個人開発でエージェントに夜間の作業を任せていると、この手の取りこぼしは翌朝まで誰にも見られません。本番のコードに古い呼び出しが残ったまま、ビルドは緑のまま通ります。
/codesearch 1.1.3 が置いた既定
Antigravity CLI 1.1.3(7/16)で /codesearch が追加されました。エイリアスは /cs と /search です。
仕様のうち、運用に効くのは次の3点です。
| 項目 | 挙動 |
| クエリの解釈 | 既定で正規表現として読まれる |
-F / --literal | 完全一致(リテラル)に切り替える |
f: / file: | グロブでパスを含める・除外する |
既定が正規表現である、という一行を軽く読み飛ばしていました。私自身、検索窓に打つ文字列は「探したい文字列そのもの」だという前提を、長いあいだ疑わずにいたのです。
けれど config.json も useState( も ja|en も、正規表現として読めば別の意味を持ちます。人間が投げるなら、返ってきた結果を見て「あ、違うな」と気づきます。エージェントは気づきません。返ってきた集合を、そのまま作業対象として受け取ります。
手元のリポジトリで、差を測る
推測で書きたくなかったので、測りました。
対象は Antigravity Lab のリポジトリそのものです。日本語記事の MDX が 979 本、src/ 配下が 73 ファイル。/codesearch と同じ意味論(既定が正規表現、-F でリテラル)を手元の grep -E と grep -F で再現し、同一クエリのヒット行数を突き合わせました。
| クエリ | 正規表現(既定) | リテラル(-F) | 差 |
config.json | 125 | 125 | 0 |
tsconfig.json | 86 | 86 | 0 |
package.json | 200 | 199 | +1 |
.mdx | 41 | 35 | +6 |
antigravity.google | 48 | 35 | +13 |
config.json は差がゼロでした。ドットが任意の一文字にマッチするとはいえ、configXjson のような文字列がリポジトリに無ければ、事故は起きません。ここが厄介なところです。**大半のクエリでは、正規表現既定は何も壊しません。**だから既定を疑う機会が来ない。
差が出たのは antigravity.google でした。48件中13件、約 27% が過剰ヒットです。実体を見ると、こうでした。
src/generated/articles.json:antigravity-google
content/articles/ja/integrations/antigravity-google-workspace-mcp-daily-workflow.mdx:antigravity-google
ドットがハイフンにマッチしています。ドメイン名を探したつもりが、スラッグを拾っていた。公式サイトへの外部リンクを棚卸ししようとして、記事のURLパスが混ざる形です。
.mdx の過剰ヒット6件も同じ構図で、実体は |mdx・ mdx・_mdx・"mdx でした。拡張子を数えたつもりが、その手前の一文字ごと拾っている。
どれも、結果を眺めれば人間なら気づきます。エージェントに「この一覧を全部直して」と渡した瞬間、気づく人がいなくなります。
ja|en が返した 4.5MB
差が最も大きく出たのは、区切り文字を含むクエリでした。
| クエリ | 正規表現(既定) | リテラル(-F) |
ja|en | 26,658行 / 約4.5MB | 0件 |
ja|en は、リテラルとしては「そういう文字列」です。パスの表記やロケール設定の書き方として、素直に打ちたくなる形でしょう。
正規表現として読むと、これは ja または en になります。日本語記事のMDX 979本に対して、26,658行、約 4,564,685 バイトが返りました。約 4.5MB です。
どのモデルの文脈にも収まりません。エージェントがこれを受け取ると、途中で切り詰めるか、要約するか、諦めるかのどれかになります。そしていずれの場合も、「検索は成功した」という体裁のまま次の工程へ進みます。
一方、リテラルなら 0 件です。0 件は正しい答えでした。このリポジトリに ja|en という文字列は存在しないからです。
同じ5文字が、片方では 4.5MB、片方では 0 件。この振れ幅を、既定の一行が握っています。
括弧は止まる、ドットは止まらない
もうひとつ、性質の違う失敗があります。
関数の呼び出し元を探すつもりで getArticleContent( を投げてみました。
grep: Unmatched ( or \(
正規表現として読むと、閉じていないグループになります。エラーで止まりました。
これは良い失敗です。止まるからです。気づけます。-F を付ければ 3 件が返り、それが正しい答えでした。
危ないのは、止まらない側です。ドットは止まりません。エラーも出さず、それらしい件数を返し、その中に 13 件の余計な行を混ぜてきます。
| 失敗の型 | 例 | 気づけるか |
| 構文エラーで停止 | ( [ の未閉じ | 気づける(エラーが出る) |
| 過剰ヒット | . が任意文字にマッチ | 気づけない |
| 意味の反転 | | が OR になる | 件数が異常なら気づける |
| 取りこぼし | $ ^ * を含む文字列 | 気づけない |
自動実行の設計では、この表の右列だけを見ています。エラーで落ちる経路には、リトライもログも用意してあります。静かにずれる経路には、何も置いていませんでした。
この落とし穴を回避する方法は、実のところ単純です。既定を変えるか、既定の外に出たことを検出するか。前者は -F を付けるだけ、後者は後述のハーネスです。私はこの二つを両方入れることを推奨します。片方だけだと、付け忘れた日に元へ戻るためです。
エラーは設計の対象になりやすく、沈黙は対象になりにくい。ここに手が届いていなかった、と認めるほかありません。
クエリを契約として書く
そこで、検索クエリを「その場の思いつき」ではなく契約として書くことにしました。エージェントに渡す指示文にも、同じ規約を置いています。
1. 既定はリテラル、正規表現は意図があるときだけ
探したいものが「文字列そのもの」なら、-F を付けます。付け忘れが怖いので、逆に考えました。正規表現を使うのは、正規表現でなければ書けない検索のときだけです。
呼び出し元を横断で拾いたいときは、正規表現が要ります。
# 2つの関数の呼び出し元を一度に拾う(正規表現が必要な場面)
/codesearch 'getArticleContent\(|getArticleBySlug\(' f:src/**/*.ts
# 文字列そのものを探す(リテラルで十分・こちらが既定であるべき)
/codesearch -F 'antigravity.google' f:content/**/*.mdx
上の例では、括弧を \( とエスケープしています。エスケープが要る時点で、それは正規表現を意図している証拠です。逆に、エスケープが一つも無いのに正規表現で投げているクエリは、たいてい -F の付け忘れです。
迷う場面もあります。関数名のように括弧を含む文字列を、括弧ごと厳密に探したい場合は、エスケープして正規表現で書くよりも -F の方が短く、読み違えの余地もありません。この場合は素直にリテラルを選ぶのが安全です。
この見分け方は、自分の手癖を点検するのに役立っています。私はこの規約を入れてから、クエリを打つ前に一拍置くようになりました。
2. f: で探索範囲を先に閉じる
範囲を絞ると、過剰ヒットの絶対数が減ります。減った分だけ、目視できる量に近づきます。
同じリポジトリで premium: を測ると、こうでした。
| 範囲 | ヒット行数 |
| リポジトリ全体 | 1,934 |
f:content/articles/ja/**/*.mdx | 955 |
約 51% が消えました。消えた側の大半は、ビルド生成物と英語版です。どちらも「フロントマターの premium を点検する」という目的には要りません。
範囲を閉じるのは、性能のためではありません。エージェントに渡す集合を、人間がまだ検算できる大きさに保つためです。1,934 行は読めませんが、955 行なら件数の妥当性は判断できます。
3. 件数の上限を先に決めておく
「この検索は何件くらい返るはずか」を、投げる前に見積もります。桁が違ったら、クエリが壊れています。
ja|en の 26,658 行は、この規約があれば即座に止まりました。ロケール表記を探して 5 桁が返るなら、探しているものが違います。
エージェントへの指示文には、こう書いています。「検索結果が想定の 10 倍を超えたら、作業に進まずクエリを報告してください」。上限は厳密でなくて構いません。桁で止まれば十分です。
ドリフトを測る小さなハーネス
規約を決めても、守られているかは別の話です。よく使うクエリについて、正規表現とリテラルの差を定期的に測るスクリプトを置きました。
#!/usr/bin/env python3
"""codesearch_drift.py — 正規表現既定とリテラルのヒット差を測る。
/codesearch は既定でクエリを正規表現として解釈する。
同じ意味論を ripgrep で再現し、両者の差分が閾値を超えるクエリを警告する。
使い方:
python3 codesearch_drift.py <repo_root> queries.txt
"""
import re
import subprocess
import sys
from pathlib import Path
# 差が出たら警告する閾値(過剰ヒットの割合)
DRIFT_THRESHOLD = 0.05
# この桁を超えたら、クエリが壊れている可能性が高い
VOLUME_CEILING = 5000
def count(root: Path, query: str, literal: bool) -> int:
"""ヒット行数を返す。マッチ0件は rg の exit 1 なので 0 を返す。"""
cmd = ["rg", "--count-matches", "--no-messages"]
if literal:
cmd.append("--fixed-strings")
cmd += [query, str(root)]
proc = subprocess.run(cmd, capture_output=True, text=True)
# 注意: exit code をパイプで潰さない。1 は「0件」、2 以上は構文エラー。
if proc.returncode == 1:
return 0
if proc.returncode >= 2:
return -1 # 正規表現の構文エラー(= 止まる失敗)
return sum(int(l.rsplit(":", 1)[1]) for l in proc.stdout.splitlines() if ":" in l)
def audit(root: Path, queries: list[str]) -> int:
problems = 0
for q in queries:
re_hits = count(root, q, literal=False)
fx_hits = count(root, q, literal=True)
if re_hits == -1:
print(f"⛔ SYNTAX {q!r}: 正規表現として不正(-F を付けるべき)")
problems += 1
continue
if re_hits > VOLUME_CEILING:
print(f"🚨 VOLUME {q!r}: {re_hits:,}行 — 上限 {VOLUME_CEILING:,} を超過")
problems += 1
if fx_hits == 0 and re_hits > 0:
print(f"🚨 INVERT {q!r}: リテラル0件 / 正規表現{re_hits:,}行 — 意味が反転している")
problems += 1
continue
if fx_hits > 0:
drift = (re_hits - fx_hits) / fx_hits
if drift > DRIFT_THRESHOLD:
pct = drift * 100
print(f"⚠️ DRIFT {q!r}: 正規表現{re_hits} / リテラル{fx_hits} (+{pct:.1f}%)")
problems += 1
return problems
def main() -> int:
if len(sys.argv) != 3:
print(__doc__)
return 2
root = Path(sys.argv[1])
queries = [
line.strip()
for line in Path(sys.argv[2]).read_text(encoding="utf-8").splitlines()
if line.strip() and not line.startswith("#")
]
problems = audit(root, queries)
print(f"\n{len(queries)}件を検査 / {problems}件の要注意クエリ")
return 1 if problems else 0
if __name__ == "__main__":
sys.exit(main())
queries.txt には、日常的に投げるクエリを並べます。
# 外部リンクの棚卸し
antigravity.google
# フロントマターの点検
premium:
# ロケール表記(これが INVERT で検出される)
ja|en
このリポジトリに対して走らせると、antigravity.google が DRIFT で、ja|en が INVERT で挙がります。どちらも、私が実際に投げていたクエリです。
ひとつ、実装で引っかかった点があります。当初 rg の出力を head へ渡していたところ、構文エラーの終了コードが握り潰されて、常に「正常」として扱われていました。判定に使うコマンドはパイプにつながない。ハーネス自身が静かに失敗するのでは、意味がありません。
エージェントへの指示文に何を残すか
規約とハーネスができたら、最後はエージェントに伝える番です。指示文に置いたのは、次の3行だけです。
- 文字列そのものを探すときは
-F を付ける。エスケープが一つも無い正規表現クエリは、付け忘れとみなす
- 検索は必ず
f: で範囲を閉じる。リポジトリ全体を対象にしない
- ヒット件数が想定の桁を超えたら、作業に進まず件数とクエリを報告する
長く書きたくなるのを堪えました。エージェントへの指示は、増やすほど守られる率が落ちます。これは 1.1.3 の別の修正——ヘッドレス実行(-p)が確認の要るツールを黙って自動承認していた不具合が、ソフト拒否と allow ルール名の提示に改められた件——を読んだときにも感じたことでした。
既定値が安全側に寄っていない場所は、指示文をいくら足しても埋まりません。逆に、既定が安全なら、指示は短くて済みます。
/codesearch の正規表現既定は、危険な既定ではありません。強力な既定です。ただ、強力な既定は、それを強力だと知っている人の手にあるときだけ強力なのだと思います。
次の一手
まず、自分が普段投げているクエリを 10 個ほど書き出してみてください。その中に、ドットを含むファイル名や、| を含む表記が混ざっていないか。混ざっていたら、上のハーネスを走らせると、差がそのまま数字で出ます。
私の場合、10 個のうち 2 個が該当しました。1年近く投げ続けて、気づいていませんでした。
エージェントに任せる範囲を広げるほど、任せる手前の一手が効いてきます。検索は、その最初の一手です。
既定を疑う時間は、任せる時間を増やすための投資だと思っています。長くお付き合いいただき、ありがとうございました。