先日の大型更新を取り込んだ翌朝、いつもなら静かに終わっているはずの無人実行のログに、見慣れない失敗が並んでいました。夜のうちに走るタスクのうち三割ほどが途中で止まっていて、原因を追う朝になったのです。
調べてみると、コードのバグではありませんでした。前日まで問題なく動いていた構成が、更新によって静かに置き換わっていたことが理由でした。私は個人開発で作った壁紙アプリや癒し系アプリを Google Play と App Store に出しながら、その更新作業や記事運用の多くを Antigravity のエージェントに任せています。だからこそ、足場そのものが一晩で変わると、寝ている間に積み上がるはずだった成果が静かに崩れます。
新機能を早く使いたい気持ちと、無人で回している土台を壊したくない気持ちは、いつも綱引きをしています。その綱引きに毎回悩まないための「段階導入カナリアゲート」という仕組みを、実際に組んだ形で以下に共有します。
一晩で半分しか通らなくなった朝
最初に起きたことを正確に書きます。更新を取り込んだのは前日の夜で、その時点では手元で一つ二つコマンドを試して、問題なく動いたので安心して寝ました。ところが翌朝、無人実行の初回成功率が普段の約98%から約63%まで落ちていました。
厄介だったのは、失敗が一様ではなかった点です。あるタスクは出力の形式が変わったために後段の処理が読み取れず、別のタスクは応答の傾向が変わって品質ゲートに弾かれていました。手元で叩いた数コマンドが「たまたま壊れていない経路」だったために、寝る前の確認をすり抜けていたのです。
この一件で私が痛感したのは、大型更新は単一の変更ではないということです。一つの更新が、同時に複数の前提を書き換えます。
更新が同時に動かす三つの変数
更新で何が壊れるのかを、私は三つの変数に分けて考えるようにしました。ひとつずつ別の壊れ方をするので、まとめて「更新」とだけ捉えると原因の切り分けに時間がかかります。
更新で動く変数 典型的な壊れ方 凍結して守る対象
CLI 本体のバージョン サブコマンドや出力フォーマットが変わり、後段のパースが崩れる antigravity のバージョン番号
拡張・プラグイン 自動更新で API が非互換になり、無言で挙動が変わる 拡張一覧のハッシュ
既定モデル 既定が差し替わり、同じ指示でも出力傾向が変化する model.default の値
私の失敗の原因は、三つ目の「既定モデルの差し替え」と一つ目の「出力フォーマットの変化」が同時に起きていたことでした。どちらか片方だけなら気づけたかもしれませんが、重なると症状が混ざり、切り分けが難しくなります。
だからこそ、復旧を運任せにせず、更新の前後で「何が動いたか」を機械が説明できる状態にしておく価値があります。
設計の柱は「凍結してから更新する」
ここでの設計思想はとても素朴です。動いている構成をまず凍結し、その凍結点をいつでも復元できるようにしてから、新しいバージョンを別の場所で試す。試した結果が以前と同じように振る舞うと確認できて初めて、本番の無人実行に採用します。
言い換えると、更新を「不可逆な一発勝負」から「いつでも戻せる、合否のある手続き」に変える、ということです。個人開発では検証環境に潤沢な人手を割けません。だからこそ、戻せることと、合否を機械が判定できることの二つを最初に用意しておくと、夜の自動実行を安心して任せられます。
手順 — 段階導入カナリアゲート
実際の流れは四段階です。番号順に淡々と進めれば、判断のたびに迷う必要はありません。
動作中の構成を構成マニフェストに凍結し、一世代前を退避として残します。
新しいバージョンを、本番とは隔離したプロファイルに入れます。本番の設定には触れません。
代表的なタスクを隔離プロファイルで一度走らせ、ゴールデン出力と照合します。
照合スコアがしきい値以上なら本採用、未満なら凍結版へ自動で戻します。
この四段階の肝は、三番目の「ゴールデン照合」です。新バージョンが速くなったか、賢くなったかではなく、これまでと同じように振る舞うかどうかだけを見ます。無人実行で大切なのは尖った賢さよりも、昨日と同じ結果が今日も返ることだからです。
実装1: 動作中の構成を凍結する
まず、いま動いている構成をひとつのファイルに固めます。バージョン番号だけでなく、拡張一覧のハッシュと既定モデルも一緒に記録しておくのが要点です。三つの変数をまとめて押さえておくことで、後から「何が動いたのか」を差分で説明できます。
#!/usr/bin/env bash
# capture-env.sh — 動作確認済みの構成を凍結する
set -euo pipefail
OUT = "env.lock.json"
ag_version = "$( antigravity --version 2> /dev/null | head -1 )"
node_version = "$( node -v )"
default_model = "$( antigravity config get model.default 2> /dev/null || echo unknown)"
ext_hash = "$( antigravity ext list --json | sha256sum | cut -c1-16 )"
cat > " $OUT " << JSON
{
"captured_at": "$( date -u +%FT%TZ)",
"antigravity": "${ ag_version }",
"node": "${ node_version }",
"default_model": "${ default_model }",
"extensions_sha": "${ ext_hash }"
}
JSON
echo "frozen -> $OUT "
このファイルをリポジトリに含めておくと、更新の履歴が git の差分として残ります。私は env.lock.json をコミットする運用にしてから、「いつ何が変わったか」を後追いできるようになり、原因調査の朝がずいぶん短くなりました。
実装2: カナリア検証ランナー
次に、新しいバージョンを隔離プロファイルで一度だけ走らせ、以前の出力と照合します。ここで完璧な一致を求めると、ささいな揺らぎで毎回失格になってしまうので、類似度のしきい値で「振る舞いが変わっていないか」を判定します。
# canary.py — 新バージョンを隔離プロファイルで試し、合否を終了コードで返す
import json, subprocess, sys, difflib
GOLDEN = "canary/golden_output.txt"
TASK = "canary/task_prompt.txt"
def run_on (profile: str ) -> str :
out = subprocess.run(
[ "antigravity" , "run" , "--profile" , profile, "--prompt-file" , TASK ],
capture_output = True , text = True , timeout = 900 ,
)
if out.returncode != 0 :
raise RuntimeError ( f "run failed: { out.stderr[: 200 ] } " )
return out.stdout.strip()
def similarity (a: str , b: str ) -> float :
return difflib.SequenceMatcher( None , a, b).ratio()
def main () -> int :
golden = open ( GOLDEN , encoding = "utf-8" ).read().strip()
actual = run_on( "canary-next" )
score = similarity(golden, actual)
print (json.dumps({ "similarity" : round (score, 3 )}))
# 0.92 未満は「振る舞いが変わった」とみなして本採用を止める
return 0 if score >= 0.92 else 1
if __name__ == "__main__" :
sys.exit(main())
しきい値の 0.92 は私の手元での値で、扱うタスクの揺らぎの大きさによって調整します。出力に日付や乱数が混ざる場合は、照合の前にそうした部分を伏せ字に置き換えてから比較すると、誤検知が減ります。終了コードでだけ合否を返すので、後段のシェルからそのまま分岐に使えるのも利点です。
実装3: 健全性を見て自動でピン留めへ戻す
最後に、カナリアの合否で採用と復帰を分けます。合格したときだけ自己更新を実行し、新しい構成を凍結し直します。失格なら、凍結しておいたバージョンへピン留めし直すだけで元に戻ります。
#!/usr/bin/env bash
# adopt-or-rollback.sh — カナリア合格時のみ本採用、失格なら凍結版へ戻す
set -euo pipefail
if python3 canary.py ; then
cp env.lock.json env.lock.prev.json # 退避を一世代残す
antigravity self-update --channel stable
./capture-env.sh # 新しい構成を凍結し直す
echo "adopted"
else
pinned = "$( jq -r .antigravity env.lock.json)"
antigravity self-update --pin " $pinned "
echo "rolled back to ${ pinned }"
fi
この三つのスクリプトを、無人実行が始まる前の時間帯に一度だけ走らせるようにしました。更新があってもなくても、毎回「凍結 → カナリア → 採用か復帰か」を通るので、人が起きていなくても足場の入れ替えが安全に進みます。私はこの仕組みを、夜のタスクが始まる二時間前に置くことを推奨します。検証で問題が出ても、本番が動き出すまでに余裕があるからです。
運用して見えた数字と落とし穴
導入してからしばらく回した結果が、次の表です。数字は私の運用環境での実測で、扱うタスクの数や種類によって変わります。
指標 更新直後・無策のとき カナリアゲート導入後
無人実行の初回成功率 約63% 約98%
異常に気づくまでの時間 翌朝まで気づかず約8時間 採用前に検知し0分
凍結版への復帰時間 手作業で約40分 自動で約12分
成功率そのものより、私にとって大きかったのは「異常に気づくまでの時間」が翌朝からゼロになったことでした。壊れた構成のまま夜を一晩過ごす、という最悪の経路を断てたからです。
運用の落とし穴も書いておきます。ひとつ目は、拡張の自動更新を止めずにいると、CLI を凍結しても拡張だけが裏で動いてしまう点です。私は拡張の自動更新を切り、更新は env.lock.json の更新と一緒に明示的に行うようにしました。ふたつ目は、ゴールデン出力を一度作って放置すると、正当な仕様変更にまで失格を出し続けてしまう点です。本採用が決まったら、その出力で必ずゴールデンを更新する手順を adopt の側に入れておくと、検証が陳腐化しません。三つ目は、隔離プロファイルが本番の設定ファイルを読みに行ってしまう取り違えです。プロファイルの設定ディレクトリを環境変数で完全に分け、本番の認証情報を渡さないようにしておくと安全です。
AdMob の収益集計や Google Play 向けのリリース準備のように、結果が翌日の数字に直結する作業ほど、この「戻せること」と「合否が出ること」の二つが効いてきます。
使い始めの一手
最初から三つのスクリプトを揃える必要はありません。私が勧める最初の一手は、capture-env.sh だけを今日から走らせて、env.lock.json を git にコミットしておくことです。これだけで、次の大型更新が来たときに「更新前は何だったか」を差分で取り出せます。カナリアと自動復帰は、その土台ができてから足せば十分です。
足場の安定は、派手な成果ではありませんが、無人で回す規模では何より効いてきます。同じように一人で複数のものを動かしている方の、夜の安心につながれば幸いです。