「昨日まで動いていた夜間バッチが、今朝だけ空振りしている」。Antigravity が 6 月に v2.2.1・v2.1.4・v2.0.11 と立て続けにポイントリリースを出した週、私自身もまさにこの状況に出くわしました。手元で対話的に使っているぶんには何も変わって見えないのに、agy をヘッドレスで呼んでいるスケジュール実行だけが、出力形式の微妙な変化で後段のパースに失敗していたのです。
公式の変更履歴を毎朝読むのは現実的ではありませんし、読んでも「自分のスクリプトに効くかどうか」は書いてありません。必要なのは、更新の前後で自分のユースケースの出力が変わったかどうかを、機械的に・短時間で判定する仕掛けです。以下では、ゴールデン出力との差分で回帰を検知し、壊れていたら前のバージョンへ自動で戻す、軽量なスモークテストを組み立てます。
なぜ「壊れ方」が静かなのか
ポイントリリースが怖いのは、メジャー更新と違って身構えないからです。バグ修正と性能改善が中心なので、まさか自分の自動処理が止まるとは思いません。ところが無人運用で効いてくるのは、機能の有無ではなく出力の細部です。
実際に私が踏んだのは次のような変化でした。ヘッドレス出力の先頭に進捗行が一行増えた、JSON の整形が変わって末尾の改行有無が揺れた、終了コードは 0 のままなのに本文が空で返ってきた。どれも対話 UI では誰も気づきません。けれど、出力を jq や正規表現で受けている後段にとっては致命的です。終了コードだけを見て成否を判定していると、空の本文をそのまま「成功」として次の工程へ流してしまいます。
つまり守るべき境界は「コマンドが成功したか」ではなく「いつもと同じ形の出力が返ってきたか」です。ここを押さえると、対策はぐっと具体的になります。
ゴールデン出力を一度だけ固定する
最初にやるのは、安定して動いている「いまのバージョン」で、代表的な入力に対する出力を一度だけ記録することです。完全一致を求めると日付や乱数で毎回壊れるので、揺れる部分を正規化してから比べます。
#!/usr/bin/env bash
# capture_golden.sh — いま動いているバージョンの基準出力を保存する
set -euo pipefail
GOLDEN_DIR="${HOME}/.agy_smoke"
mkdir -p "$GOLDEN_DIR"
# 実運用と同じヘッドレス呼び出しを再現する(自分のバッチに合わせて差し替える)
run_case() {
agy run --headless --no-tty \
--prompt-file "$1" \
--output json 2>/dev/null
}
# 揺れる値(日付・UUID・所要時間)を伏せ字にして比較を安定させる
normalize() {
sed -E \
-e 's/[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9:.]+Z?/<TS>/g' \
-e 's/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/<UUID>/g' \
-e 's/"elapsed_ms":[0-9]+/"elapsed_ms":<N>/g'
}
agy --version > "$GOLDEN_DIR/version.txt"
for f in cases/*.prompt; do
name="$(basename "$f" .prompt)"
run_case "$f" | normalize > "$GOLDEN_DIR/${name}.golden"
echo "captured: $name"
done
echo "golden version: $(cat "$GOLDEN_DIR/version.txt")"ここで大事なのは、スモーク用の入力 cases/*.prompt を本番のバッチと同じ種類にしておくことです。私は記事生成・要約・JSON 抽出の 3 つだけを代表ケースとして置いています。網羅は狙わず、壊れたら一番困る経路だけを薄く守る、という方針です。ケースが多すぎると毎回の確認が重くなり、結局回さなくなります。
更新前にスモークし、ダメなら戻す
基準ができたら、更新の直前にもう一度同じケースを流し、ゴールデンと比べます。構造的な差分が出たら、その更新は自分のユースケースを壊している可能性が高い、と判断して止めます。
#!/usr/bin/env bash
# smoke_check.sh — 現在の出力をゴールデンと突き合わせる。差分があれば exit 1
set -euo pipefail
GOLDEN_DIR="${HOME}/.agy_smoke"
source_funcs() { :; } # run_case / normalize は capture_golden.sh と共有する想定
run_case() { agy run --headless --no-tty --prompt-file "$1" --output json 2>/dev/null; }
normalize() {
sed -E \
-e 's/[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9:.]+Z?/<TS>/g' \
-e 's/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/<UUID>/g' \
-e 's/"elapsed_ms":[0-9]+/"elapsed_ms":<N>/g'
}
fail=0
for g in "$GOLDEN_DIR"/*.golden; do
name="$(basename "$g" .golden)"
current="$(run_case "cases/${name}.prompt" | normalize)"
# 1) 本文が空でないこと(終了コード 0 でも空返しを弾く)
if [ -z "$(echo "$current" | jq -r '.text // empty' 2>/dev/null)" ]; then
echo "✗ ${name}: empty body"
fail=1; continue
fi
# 2) 構造の差分(キー集合の変化を検知。値の中身までは見ない)
ref_keys="$(echo "$(cat "$g")" | jq -S 'keys' 2>/dev/null || echo '[]')"
cur_keys="$(echo "$current" | jq -S 'keys' 2>/dev/null || echo '[]')"
if [ "$ref_keys" != "$cur_keys" ]; then
echo "✗ ${name}: schema drift"
diff <(echo "$ref_keys") <(echo "$cur_keys") || true
fail=1; continue
fi
echo "✓ ${name}"
done
exit "$fail"差分の比較を「キー集合の変化」に絞っているのには理由があります。本文の文字列は更新ごとに自然に揺れるため、完全一致で見るとスモークが毎回赤くなって信用されなくなります。後段のパースが実際に依存しているのは出力の構造、つまりどのキーが存在するかなので、そこだけを厳しく見ます。空本文チェックを別に置いているのは、終了コード 0 のまま中身が空という、最も静かな壊れ方を取り逃さないためです。
更新と検証をひとつの流れにする
最後に、バージョン固定・更新・スモーク・自動ロールバックをひとつのスクリプトにまとめます。段階移行が無難だという経験則を、手順として固定してしまうわけです。
#!/usr/bin/env bash
# guarded_upgrade.sh — スモークが通った時だけ更新を確定し、ダメなら戻す
set -euo pipefail
GOLDEN_DIR="${HOME}/.agy_smoke"
prev="$(agy --version | awk '{print $NF}')"
echo "current: $prev"
# 1) 更新前のスモーク(今の環境が健全であることを先に保証)
if ! ./smoke_check.sh; then
echo "更新前から赤い。基準を取り直すまで更新しない。"; exit 1
fi
# 2) 更新(バージョンを明示固定。最新追従は避ける)
target="${1:?usage: guarded_upgrade.sh <version>}"
agy self update --version "$target"
# 3) 更新後のスモーク。落ちたら即ロールバック
if ./smoke_check.sh; then
echo "✅ $target はスモーク通過。確定します。"
agy --version > "$GOLDEN_DIR/version.txt"
else
echo "🚨 $target でスモーク失敗。$prev へ戻します。"
agy self update --version "$prev"
./smoke_check.sh && echo "ロールバック完了($prev で健全)"
exit 1
fiagy self update のオプション名やサブコマンドは環境やバージョンで差があるので、agy self update --help で実際の指定方法を確認してから組み込んでください。要は「最新へ自動追従させず、検証を通ったバージョンだけを明示的に固定する」という考え方が核で、コマンドの細部は読み替えればそのまま使えます。
この仕組みを入れてから、ポイントリリースに対する私の態度は変わりました。更新を恐れて先送りするのでも、無防備に最新へ飛びつくのでもなく、30 秒のスモークが通れば取り込み、落ちれば戻す。Dolice Labs の複数サイトを個人開発で一人で回していると、更新の判断に時間をかけられないので、この「通れば進む・落ちれば戻る」の自動化が効きました。
小さく守る発想に切り替える
ポイントリリースの不確実性に対して、変更履歴を読み込んで身構えるのは続きません。代わりに、自分のユースケースの出力という一点だけを基準化し、更新の前後で機械的に突き合わせる。守る対象を「コマンドの成否」から「いつもの出力の形」へずらすだけで、静かな回帰のほとんどは検知できます。
次の一歩として、まずは壊れたら一番困る経路をひとつだけ選び、capture_golden.sh で基準を取ってみてください。たった一ケースでも、次のポイントリリースが来たときに「これは取り込んでいいのか」を秒で判断できる足場になります。同じように無人運用と更新の板挟みになっている方の助けになれば嬉しいです。