7月1日、Antigravity は v2.2.1・v2.1.4・v2.0.11 を同じ日に公開しました。安定版と新機能版を並行して選べる体制です。同じ時期に、手元の CLI は Gemini CLI から Antigravity CLI へ移り、既定モデルも静かに更新されます。
私自身、個人開発で4つの技術ブログの記事生成を毎日エージェントに任せています。ある朝、生成された記事のテーブルが崩れて出力されました。原因を探ろうとして、手が止まりました。
前回の正常な実行から今日までに、動いていた部品が多すぎたのです。IDE のビルドが上がり、CLI が別実装に替わり、モデルが更新され、依存パッケージも2つ上がっていました。どれが崩したのか、切り分ける手がかりがありません。
この記事は、その朝の反省から組み直した「変更予算」の設計です。派手な自動化ではなく、可動部が増えた環境で回帰の原因を最短で見つけるための、地味な土台の話をします。
可動部が増えると「どれが壊したか」が分からなくなる
エージェント開発のスタックは、いつのまにか独立に動く軸の集合になっています。少なくとも次の4つは、それぞれ別のタイミングで更新されます。
| 可動軸 | 更新の主体 | 更新頻度の体感 |
| Antigravity IDE ビルド | 公式リリース(安定版/新機能版が並行) | 週単位 |
| Antigravity CLI | ツール統合・移行に伴う更新 | 不定期・移行時に断続 |
| 既定モデル | プラットフォーム側の差し替え | 予告なく静かに |
| 依存パッケージ | 自分の npm update や自動更新 | 随時 |
問題は、これらが組み合わせで効いてくることです。軸が4つあれば、状態の組み合わせは指数的に増えます。1週間に各軸が1回ずつ動いただけでも、前回の正常な実行との差は4軸ぶん開きます。
回帰が起きたとき、私たちは「何が変わったか」を再構成しようとします。ところが4軸ぶんの変化が同時に積まれていると、二分探索の起点そのものが失われます。原因の候補が多すぎて、勘に頼るしかなくなるのです。
無人実行では、これがさらに重くなります。夜間に走らせたパイプラインが崩れた出力を出しても、その瞬間の環境スナップショットが残っていなければ、翌朝には「今の環境」しか手元にありません。壊れた時点の状態は、もう再現できません。
「既知の正常」を1枚のロックファイルに固定する
最初にやるべきは、最後に正常だった実行の環境を、1枚のファイルに書き留めることです。アプリケーションの package-lock.json が依存を固定するのと同じ発想を、スタック全体へ広げます。
私は stack.known-good.yml という名前の1枚を、リポジトリ直下に置いています。
# stack.known-good.yml
# 「この組み合わせなら正常に走り切った」最後の状態を記録する
known_good:
captured_at: "2026-07-01T09:00+09:00"
note: "4サイトの記事生成が JA=EN 一致・テーブル崩れなしで完走"
axes:
antigravity_ide: "2.0.11"
antigravity_cli: "0.4.2"
default_model: "gemini-3.5-flash-2026-06"
node: "22.14.0"
deps:
"@antigravity/sdk": "2.3.1"
"yaml": "2.6.1"
# 変更予算: 前回の正常状態から同時に動かしてよい軸の最大数
change_budget: 1
要点は3つあります。
- 各軸の版を、人間が読める形で残すこと
- 正常を宣言する条件を
note に書くこと
- 変更予算を、このファイルに同居させること
第一に、各軸の版を人間が読める形で残すこと。ハッシュだけでは、後から「どのモデルだったか」を思い出せません。日付と短いメモを添えます。
第二に、正常を宣言する条件を note に書くこと。何をもって「正常」とするかは運用ごとに違います。私の場合は「日英記事数が一致し、テーブルが崩れず完走した」ことです。この基準を言語化しておくと、次に更新するときの判断がぶれません。
第三に、変更予算をこのファイルに同居させること。予算は環境と一体で管理すると、レビュー時に一目で見えます。
変更予算 — 一度に動かす軸は1つだけ
変更予算とは、前回の正常状態から、一度の更新で同時に動かしてよい軸の数です。私は原則 1 に設定することを推奨します。
考え方はシンプルです。軸を1つずつしか動かさなければ、回帰が起きたときの容疑者は常に1つに絞られます。二分探索すら要りません。直前に動かした軸を戻せば済みます。
これを無人実行の前に機械で確認します。現在の環境を読み取り、既知の正常との差分軸を数え、予算を超えていたら実行を止める preflight です。
#!/usr/bin/env python3
# preflight.py — 無人実行の直前に走らせ、変更予算の超過なら停止する
import subprocess
import sys
import yaml
def read_current_axes() -> dict:
"""現在のスタックから各軸の版を取得する。取得できない軸は 'unknown'。"""
def sh(cmd: str) -> str:
try:
out = subprocess.run(
cmd, shell=True, capture_output=True, text=True, timeout=20
)
return out.stdout.strip() or "unknown"
except Exception:
return "unknown"
return {
"antigravity_ide": sh("agy --ide-version 2>/dev/null"),
"antigravity_cli": sh("agy --version 2>/dev/null | head -1"),
"default_model": sh("agy config get default_model 2>/dev/null"),
"node": sh("node --version 2>/dev/null | tr -d 'v'"),
}
def diff_axes(known: dict, current: dict) -> list:
"""既知の正常と現在で食い違う軸名を返す。"""
drifted = []
for axis, good_value in known.items():
now = current.get(axis, "unknown")
if now != good_value:
drifted.append((axis, good_value, now))
return drifted
def main() -> int:
with open("stack.known-good.yml", encoding="utf-8") as f:
manifest = yaml.safe_load(f)
budget = manifest.get("change_budget", 1)
known = manifest["known_good"]["axes"]
current = read_current_axes()
drifted = diff_axes(known, current)
if not drifted:
print("preflight: 既知の正常と一致。無人実行を許可します。")
return 0
print(f"preflight: {len(drifted)} 軸が既知の正常から動いています(予算 {budget})")
for axis, good, now in drifted:
print(f" - {axis}: {good} -> {now}")
if len(drifted) > budget:
print("preflight: 変更予算を超過。無人実行を中止します。")
print(" → 軸を1つずつ検証し、正常を確認できたら known-good を更新してください。")
return 1
print("preflight: 予算内。実行を許可しますが、この軸を今回の容疑者として記録します。")
return 0
if __name__ == "__main__":
sys.exit(main())
このスクリプトの狙いは、回帰を防ぐことではありません。回帰が起きたときに、容疑者を1つに保つことです。予算内で1軸だけ動かして実行し、結果が正常なら known-good を更新して次へ進みます。崩れたら、動かした1軸を戻すだけで元の正常へ帰れます。
agy --ide-version のような取得コマンドは、お使いの版で名称が異なる場合があります。取得できない軸は unknown として扱い、差分ありと見なす設計にしています。分からない軸を「たぶん同じ」と楽観するより、明示的に容疑者へ入れておくほうが安全です。取得できない軸をそのまま通すと、静かな回帰を見逃す落とし穴があります。動かした軸を戻せば回避できる問題も、原因が分からなければ回避のしようがありません。
回帰したときの二分切り分け手順
予算を守れていれば二分探索は不要ですが、移行のように複数軸が不可避で同時に動く局面もあります。7月の CLI 移行がまさにそれでした。そういうときのために、切り分けの手順を決めておきます。
| 手順 | 操作 | 目的 |
| 1 | 現在の全軸と known-good の差分を列挙する | 容疑者の集合を確定する |
| 2 | 差分軸を半分だけ known-good に戻す | 探索範囲を二分する |
| 3 | 最小の再現ケースで1回だけ実行する | 正常/回帰を判定する |
| 4 | 正常なら残り半分に、回帰なら戻した半分に容疑者を絞る | 範囲を半減させる |
| 5 | 軸が1つになるまで2〜4を繰り返す | 原因軸を確定する |
鍵は**手順3の「最小の再現ケース」**です。4サイト全体を回して判定していると、1回の試行に時間がかかりすぎて、二分探索が現実的な回数で終わりません。私は「1記事だけを生成してテーブルの崩れを見る」という30秒ほどの再現ケースを別に用意しています。原因の切り分けと、本番の重い実行は、分けて考えるのが要点です。
戻す操作を確実にするために、各軸の固定方法もメモに残しておきます。IDE ビルドは安定版のバージョンを明示指定、CLI は版ピン、モデルは設定での明示指定、依存は lockfile です。「戻せない軸」があると二分探索はそこで破綻するため、戻す手段の有無こそ、事前に確認すべき点です。
無人実行に組み込む — プリフライトで止める
preflight は、スケジュール実行の一番最初に置きます。予算超過なら本体を走らせずに終了し、失敗のみ通知します。無人運用では「黙って崩れた出力を出す」ことが最悪で、「今日は予算超過で見送った」と分かるほうが、はるかに扱いやすいのです。
#!/usr/bin/env bash
# nightly.sh — 無人パイプラインの入口
set -euo pipefail
if ! python3 preflight.py; then
echo "変更予算の超過により本日の無人実行を見送りました。" >&2
# 失敗のみ通知(正常時は無音)
exit 0
fi
# preflight を通過したときだけ本体を実行する
python3 generate_articles.py
結果として、無人実行の状態は次の3つに整理されます。
| 状態 | preflight の判定 | 翌朝に手元へ残るもの |
| 正常 | 差分なし、または予算内1軸 | 成果物+動かした容疑者軸の記録 |
| 見送り | 予算超過で中止 | 差分軸のリストと通知 |
| 要調査 | 通過したが成果物が異常 | 直前に動かした1軸=ほぼ確定した容疑者 |
「要調査」の欄が、この設計のいちばんの利得です。予算を守っていれば、異常が出ても容疑者は直前に動かした1軸に絞られています。原因探しの出発点が、勘ではなくログになります。
運用して見えたこと
この土台を入れる前、回帰の原因特定にはしばしば半日を溶かしていました。動いた軸が多すぎて、一つずつ戻しては試すしかなかったからです。
予算を1に絞り、known-good を毎回更新するようにしてからは、原因特定はほとんど「直前に動かした軸を戻す」だけで済むようになりました。私の記録では、切り分けにかかる時間はおおむね90%短縮し、数十分から数分の桁へ落ちています。派手さはありませんが、毎朝の安心感が変わりました。
一方で、代償もあります。更新が遅くなることです。軸を1つずつしか動かせないので、4軸をすべて追随させるには、正常確認を挟みながら数日かけることになります。新機能をすぐ試したい気持ちとは、正直なところ相性が良くありません。
そこで私は、実験用の環境と本番の無人環境を分けています。実験環境では予算を気にせず最新を試し、良ければ本番へ「1軸ずつ」昇格させます。可動部の探索と、収益に直結する無人運用の安定は、目的が違うからです。
もう一つの学びは、「戻せる軸」しか予算に載せないことでした。戻す手段のない軸は、二分探索の前提を壊します。version ピンや lockfile で確実に戻せる形にしておくことが、変更予算という考え方そのものを支えています。
可動部が同時に動く時代に、すべてを止めることはできません。できるのは、動かす順番を1つずつに整え、いつでも直前の正常へ帰れる道を残しておくことです。地味な準備ですが、無人でエージェントに任せる範囲を広げるほど、この土台の有無が効いてきます。私はこの一つずつ動かす順序を、個人開発の無人運用における生命線だと考えています。
実装の参考になれば幸いです。お読みいただき、ありがとうございました。