エージェントに「生成した HTML を public/content 配下へ書き出してください」と頼み、アーティファクト・ビューアで差分を確認し、満足して commit しました。ところが本番には何も反映されていません。ログを追い直すと、git status にそのファイルは最初から一度も現れていませんでした。.gitignore に public/content/ が入っていたからです。
原因が分かるまで半日近く費やしました。エージェントは確かに書き込んでおり、ファイルはディスク上に存在し、差分 UI もそれを表示します。ただ Git だけが黙って無視していました。この「三者のうち一者だけが黙る」構造が、デバッグを難しくします。
エージェントの差分と Git の差分は別物です
Antigravity のアーティファクトは、エージェントが実際に触れたファイルの一覧と内容を提示します。これはファイルシステム上の事実です。一方 git status が示すのは、追跡対象かつ ignore 規則に一致しない変更だけです。
観点 アーティファクト・ビューア git status(既定)
対象 エージェントが書いたファイル全て 追跡中 + 未追跡かつ非 ignored
.gitignore 一致 そのまま表示 完全に沈黙
失敗の見え方 成功に見える 差分ゼロ = 何もしていないように見える
つまり「エージェントの作業は成功、Git は何も検出しない」という状態が、正常系と見分けがつきません。ビルド生成物を ignore しているリポジトリでは、生成物をあえて commit したい局面(静的アセットの事前生成、コンテンツ HTML の同梱など)で必ず踏みます。
ignored なファイルだけを列挙する
Git には ignored なパスを明示的に見せるオプションがあります。既定の --ignored はディレクトリ単位で丸めてしまうため、ファイル単位で欲しいときは matching を指定します。
# ignored なパスをファイル単位で列挙する(!! が ignored を示す)
git status --porcelain --ignored=matching | grep '^!!' | cut -c4-
# 特定のパスがどの規則で ignore されているかを逆引きする
git check-ignore -v public/content/articles/ja/agents/foo.html
# → .gitignore:12:public/content/ public/content/articles/ja/agents/foo.html
git check-ignore -v は「どのファイルの何行目の規則に当たったか」まで返します。私自身、.gitignore ではなく .git/info/exclude 側に古い除外が残っていたケースを、この出力で初めて特定できました。ファイル名まで表示されるので、原因の候補を一つずつ潰す必要がなくなります。
複数パスをまとめて確認するなら標準入力を使います。
# エージェントが触れたファイル一覧を渡して、ignored なものだけを抜き出す
printf '%s\n' "${ TOUCHED [ @ ]}" | git check-ignore --stdin --verbose
エージェント実行の前後でスナップショットを取る
「エージェントが触れたファイル」を Git 側から知る方法はありません。ファイルシステム側で捕まえます。私は実行前にタイムスタンプの基準ファイルを置き、実行後に find -newer で差分を拾う方法に落ち着きました。inotify を使う手もありますが、CLI をワンショットで回す用途では基準ファイル方式のほうが壊れにくいと感じています。
#!/usr/bin/env bash
# agent-run-with-guard.sh — エージェント実行を挟んで生成物を検査する
set -euo pipefail
REPO_ROOT = "$( git rev-parse --show-toplevel )"
cd " $REPO_ROOT "
STAMP = "$( mktemp )"
trap 'rm -f "$STAMP"' EXIT
touch " $STAMP "
# ここで実際のエージェントを走らせる(例)
" $@ "
# 基準ファイルより新しいファイルを収集(.git 配下は除外)
mapfile -t TOUCHED < <(
find . -type f -newer " $STAMP " \
-not -path './.git/*' \
-printf '%P\n' | sort
)
if [ "${ # TOUCHED [ @ ]}" -eq 0 ]; then
echo "変更されたファイルはありません"
exit 0
fi
echo "エージェントが書いたファイル: ${ # TOUCHED [ @ ]} 件"
printf ' %s\n' "${ TOUCHED [ @ ]}"
-printf '%P\n' で先頭の ./ を落としておくと、この後 git check-ignore にそのまま渡せます。ここを省いてパス表記が揺れると、規則に当たっているのに当たっていないと判定される厄介なバグになります。
ゲート本体 — ignored な生成物を検出して落とす
収集したパスのうち、ignore 規則に当たるものを列挙します。ただし「意図して ignore している一時ファイル」まで拾うと、毎回赤くなって誰も見なくなります。allowlist を用意して、そこに載っているものは黙って通します。
# .agent-ignored-ok — 意図的に ignore されたままでよいパターン(1行1パターン)
# 例:
# node_modules/
# .next/
# *.log
ALLOW_FILE = "${ REPO_ROOT }/.agent-ignored-ok"
is_allowed () {
local path = " $1 "
[ -f " $ALLOW_FILE " ] || return 1
while IFS = read -r pat ; do
case " $pat " in '' | '#' * ) continue ;; esac
# shellcheck disable=SC2254
case " $path " in $pat * ) return 0 ;; esac
done < " $ALLOW_FILE "
return 1
}
VIOLATIONS = 0
while IFS = read -r path ; do
is_allowed " $path " && continue
RULE = "$( git check-ignore -v " $path " 2> /dev/null || true )"
echo "🛑 ignored な生成物: $path "
echo " 規則: ${ RULE :- (不明)}"
VIOLATIONS = $(( VIOLATIONS + 1 ))
done < <( printf '%s\n' "${ TOUCHED [ @ ]}" | git check-ignore --stdin 2> /dev/null || true )
if [ " $VIOLATIONS " -gt 0 ]; then
echo "🛑 ${ VIOLATIONS } 件の生成物が commit に含まれません。.gitignore か allowlist を見直してください"
exit 1
fi
echo "✅ ignored な生成物はありません"
git check-ignore --stdin は、規則に当たったパスだけを標準出力に返し、当たらなかったものは黙って捨てます。この性質のおかげで、フィルタとしてそのまま使えます。終了コードが 1 になるのは「一件も当たらなかった」ときなので、|| true を添えないと set -e で落ちます。私は最初これで詰まりました。
Antigravity CLI のフックに載せる
Antigravity CLI は、コマンドの前後にスクリプトを差し込めます。ゲートは commit の直前ではなく、エージェントの実行直後 に置くのが実用的だと考えています。commit まで進んでしまうと「なぜ差分が空なのか」の文脈が失われ、原因の切り分けが一段難しくなるためです。
# .agy/hooks.toml
[[ hooks ]]
event = "post-agent-run"
command = "./scripts/agent-run-with-guard.sh"
fail_on_error = true
[[ hooks ]]
event = "pre-push"
command = "./scripts/agent-run-with-guard.sh --check-only"
fail_on_error = true
post-agent-run を一次防衛線、pre-push を保険とする二段構えにしています。ローカルのフックを外して作業する日があっても、push の手前でもう一度止まります。CI 側にも同じスクリプトを置けば三段になりますが、そこまでは要らないというのが今のところの私の判断です。
誤検知を減らす — .gitignore を直す順序
導入直後は、node_modules/ や .next/ が大量に引っかかります。ここで allowlist に片端から追加していくと、allowlist が第二の .gitignore になり、肝心の生成物を見逃す元に戻ってしまいます。順序を守ると落ち着きます。
1. まず「エージェントが触るはずのないディレクトリ」を find の段階で除外する
node_modules・.next・dist のような依存や中間生成物は、そもそも収集対象から外します。allowlist ではなく find の -not -path で切ります。ここを間違えると、ゲートの実行時間が数十倍に膨らみます。
2. 次に「commit したい生成物」を .gitignore から外す
public/content/ のように、生成物でありながらリポジトリに含めたいパスがあります。これは .gitignore 側を直すのが正解です。allowlist に逃がすと、次のエージェントが同じ場所に書いたときにまた黙って消えます。
3. 最後に残ったものだけを allowlist に入れる
ログ・キャッシュ・ロックファイルなど、エージェントが副次的に生成し、かつ commit したくないもの。ここまで絞ると、Dolice で運用している4サイトでは allowlist は 5〜7 行に収まりました。
導入して2週間ほど回した体感では、赤くなるケースのうち 10% 前後は allowlist に足すべき副産物、残りは本当に .gitignore 側の見直しが必要なものでした。この比率が逆転してきたら、allowlist の使い方を間違えている合図だと考えています。
本番運用で踏んだ落とし穴
このゲートを本番のパイプラインに載せてから、二つの罠に当たりました。どちらもスクリプトのバグではなく、前提の置き方の問題でした。
一つ目は、CI がリポジトリを浅く clone している環境では .git/info/exclude がホスト側と異なり、ローカルでは検出できた ignored 生成物が CI では素通りしたことです。対処として、ゲートの冒頭で git check-ignore -v の出力に含まれる規則ファイル名をログへ残すようにしました。どの環境のどの規則が効いているかが記録に残ると、環境差の切り分けが数分で終わります。
二つ目は、エージェントがファイルの内容を変えずに再書き込みしたときも find -newer が反応してしまい、実質ゼロ差分の実行で赤くなったことです。ここは内容ハッシュを比較して除外する案も検討しましたが、私は採用していません。ignored なパスへの書き込みは、内容が同じでも「commit されない場所に書いている」という設計上の問題を示しているためです。ノイズではなく、直すべき指摘だと解釈しています。
その上で、次の運用を推奨します。ゲートの出力は必ず規則ファイル名まで含めること、赤くなった実行のログを最低7日は保持すること、そして allowlist に追記した日付と理由をコメントで残すことです。理由を書かない allowlist は、半年後に誰も外せなくなります。
個人開発でこの仕組みが効く理由
チーム開発なら、レビュアーが「この PR、生成物が入っていませんね」と気づきます。個人開発にはその目がありません。エージェントに任せる範囲が広がるほど、「作業は成功したが結果は残っていない」という失敗の形が増えていきます。
このゲートが守っているのは、エージェントの賢さではなく、エージェントの成果が確かに残ったこと です。検証と証跡がエージェント運用の信頼性を支える、という考え方の、ごく小さな一実装だと捉えています。
まずは自分のリポジトリで git status --porcelain --ignored=matching | grep '^!!' | head を実行してみてください。そこに、本来 commit されているはずのファイルが混ざっていないでしょうか。私はそこで一本、記事の HTML を見つけました。
お読みいただきありがとうございました。同じ半日を誰かが節約できたなら嬉しく思います。