ある朝、サイトを開いて記事が一本も増えていないことに気づきました。
前夜のバックグラウンドエージェントの実行ログには、はっきりと「完了しました」と書かれています。エラーは一件もありません。それなのに、本来追加されているはずの記事が、どこにも存在しないのです。
ログは緑、結果は空。私自身、個人開発で Dolice Labs の複数サイトを自動運用するなかで、Antigravity 2.0 のバックグラウンドや予約実行のエージェントを本格的に回し始めてから、この食い違いを何度か味わってきました。今日はその正体と、対処として落ち着いた運用について書きます。
「完了」は作業の成功を意味しない
まず受け入れるべき前提があります。エージェントの「完了しました」は、「自分が指示された手順の最後まで到達した」という意味であって、「成果物が意図した場所に残った」という意味ではありません。
この二つは、対話しながら使っているときには区別する必要がほとんどありません。画面の前にいれば、ファイルが書き換わったか、コミットが積まれたかを、その場で目で確認できるからです。
ところがバックグラウンド実行では、両者が静かに分離します。エージェントは手順を実行し、各コマンドが非ゼロ終了を返さなかったことをもって「成功」と判断します。途中のコマンドが「エラーではないが、何もしなかった」場合、その空振りは成功として記録されるのです。
人が見ていない時間帯にこそ、この隙間が開きます。
静かにすり抜ける三つの経路
実際に私が踏んだ落とし穴は、いずれも「例外を投げない失敗」でした。代表的な三つを挙げます。
ひとつ目は、Git のコミットがコミットされない経路です。新しく clone した直後のリポジトリでは user.email と user.name が未設定で、その状態で git commit を実行するとコミットが作られないまま処理が進みます。続く git push は「送るものがない」ので終了コード 0 を返します。エージェントから見れば、commit も push も成功です。けれどリモートには何も増えていません。
ふたつ目は、固定の一時ファイルパスを使い回す経路です。/tmp/insert.txt のような決め打ちの名前に途中成果を書き出していると、書き込みが失敗したときに前回実行の残骸がそのまま読み込まれます。エラーは出ません。ただ、今回作ったはずのない古い内容が、しれっと混ざります。
みっつ目は、パッチの当たり外れが握りつぶされる経路です。エージェントがファイルへの差分適用に失敗しても、ハンクの不一致を警告に留めて先へ進むことがあります。本人は「編集した」つもりで完了し、ファイルは編集前のままです。
三つに共通するのは、どれも「やったつもり」と「実際の状態」がずれていて、しかもその場では何も赤くならない点です。
自己申告ではなく、地上の事実を見る
対処の方針は単純です。エージェントの報告を信用するのをやめ、最後に独立した確認を一つ挟みます。報告と確認は別の主体でなければ意味がありません。
Git の場合、いちばん確実なのはローカルとリモートのコミットハッシュを突き合わせることです。push のあとで、両者が一致しているかを機械的に検査します。
# commit する前に identity を必ず固める
git -c user.email="you@example.com" \
-c user.name="Your Name" \
commit -m "Add: 記事を追加"
LOCAL=$(git rev-parse HEAD)
git push origin main
REMOTE=$(git ls-remote origin -h refs/heads/main | cut -f1)
if [ "$LOCAL" != "$REMOTE" ]; then
echo "❌ push が反映されていません(local=$LOCAL remote=$REMOTE)"
exit 1
fi
echo "✅ リモートに反映されました: $LOCAL"ここで git push の終了コードを見ていない点が肝心です。終了コードは「コマンドがエラーを返さなかったか」しか教えてくれません。私たちが知りたいのは「リモートの先端が、いま手元で作ったコミットになっているか」であって、それはハッシュの一致でしか確かめられません。
ファイル成果物の場合は、生成物そのものを覗いて、今回だけの目印が入っているかを確認します。
# 一時ファイルは固定名を避け、毎回ユニークにする
TMP="$HOME/work/insert-${SLUG}-$(date +%s).txt"
# 生成後、その記事固有の目印が本当に入っているか検査する
if ! grep -q "$SLUG" "$TARGET_FILE"; then
echo "❌ $SLUG が出力に見当たりません。残骸混入か書き込み失敗の疑い"
exit 1
fi固定名 /tmp/insert.txt をユニーク名に変えるだけで、前回の残骸が混ざる経路は閉じます。そのうえで目印を grep すれば、書き込み自体の成否も同時に取れます。
公式ドキュメントには、エージェントが各ステップの終了コードで成否を判断するとは明記されていません。けれど無人で長く回していると、終了コードという薄い証拠だけでは「やったつもり」を見抜けない場面が確実に出てきます。
バックグラウンドだからこそ検証を自動化する
対話のときは人間が最後の検証者でした。バックグラウンドでは、その役割を担う人がいません。だから検証ステップ自体を、エージェントの手順の中に組み込んでおく必要があります。
個人開発では、私自身が唯一の運用者であり、最後の確認者です。そこで私は「作る指示」と「確かめる指示」を必ず分けて書くようにしました。同じ一回の実行のなかで、生成のあとに独立した検査コマンドを置き、検査が落ちたらそこで止める。こうしておくと、緑のログと空の結果という食い違いは、少なくとも「赤いログと空の結果」に変わります。失敗が失敗として見えるだけで、原因の特定は格段に楽になります。
検証は重い仕組みである必要はありません。ハッシュの一致を見る三行、目印を grep する一行で、無人運用の信頼性はかなり変わります。むしろ凝った監視基盤を組むより、各タスクの末尾に小さな確認を一つ足すほうが、私の手元では長く効いています。
次の一歩
いま動いているバックグラウンドのタスクをひとつ選び、その末尾に「成果物が実在するか」を見る確認を一行だけ足してみてください。Git なら push 後のハッシュ一致、ファイルなら固有の目印の grep です。
それだけで、「完了しました」の重みが変わります。エージェントの報告は手順の話、検証は結果の話。この二つを分けて持っておくことが、無人で長く回すための土台になると考えています。
同じように自動運用で静かなすり抜けに悩んでいる方の、確認の入口になれば幸いです。