6 月 18 日に Gemini CLI と Gemini Code Assist の個人向け提供が止まり、後継の Antigravity CLI へ移行することになりました。新しい CLI は Go で書き直され、私の環境では起動が体感で 2〜3 倍速くなりました。手元のターミナルで触る分には、ほぼ置き換えるだけで動きます。
問題は、その先です。私自身、4 つのサイトを 1 台で自動運用していて、エージェント実行を cron と CI に載せています。対話で気持ちよく動くツールほど、無人で回すと静かに詰まります。App Store 向けアプリのリリース作業を自動化していたときも、同じ壁にぶつかりました。プロンプトの確認待ちで止まったまま、ジョブのタイムアウトまで何もしない。そういう事故を何度か踏みました。
この記事は「対話で動いた」から「無人で安心して回せる」までの間にある設計を、終了コード・冪等性・タイムアウト・出力パースの 4 点に絞って書きます。
対話前提のまま CI に置くと、なぜ固まるのか
エージェント CLI は、確認や追加入力を人間に求める前提で作られています。「このファイルを書き換えますか」「コミットしますか」といった問いに、ターミナルなら Enter を押せば済みます。
ところが CI ランナーや cron には、その Enter を押す人がいません。標準入力が TTY ではないため、ツールによっては自動で「いいえ」と解釈して中断したり、逆に入力を待ち続けてハングしたりします。後者が厄介で、ジョブのログには何も出ず、課金時間だけが溶けていきます。
最初にやるべきは、対話の芽を全部摘むことです。
# 確認をすべて自動承認し、標準入力を空にして人間待ちを物理的に不可能にする
antigravity run \
--prompt-file ./tasks/update-articles.md \
--yes \
--no-color \
--output json < /dev/null
--yes(自動承認フラグ。名称はお使いのバージョンの antigravity --help で確認してください)で確認を飛ばし、< /dev/null で標準入力を空にします。フラグを取りこぼしても、入力が空なら待ち続けることはありません。この二重化が効きます。--no-color は ANSI エスケープがログを汚すのを防ぐためで、後段のパースが楽になります。
二重起動しても壊さない冪等性をどう作るか
無人実行では、同じジョブが重なって走る瞬間が必ず来ます。前回の実行が長引いている最中に次の cron が発火する。CI が再試行で同じコミットを 2 回処理する。こうした重複が、同じファイルへの二重コミットや、途中状態のプッシュを生みます。この落とし穴を回避する設計を、最初から組み込んでおきます。
対策は二段構えにしています。ひとつは実行そのものの排他、もうひとつは結果の冪等性です。
排他はロックファイルで取ります。
LOCK = "/tmp/antigravity-articles.lock"
# flock で多重起動を防ぐ。-n は待たずに即座に諦める指定
exec 9> " $LOCK "
if ! flock -n 9 ; then
echo "別の実行が進行中のためスキップします"
exit 0
fi
# ここから先は同時に 1 つだけが通過する
antigravity run --prompt-file ./tasks/update-articles.md --yes --output json < /dev/null
結果の冪等性は、出力先を「実行ごとにユニーク」にして担保します。固定名のファイルに追記する設計は、前回の残骸が混入する事故を生みやすいので避けます。私はスラッグを含む一意名で書き出し、最後に検証してから本来の場所へ移すようにしています。Dolice の運用では、この一手間を入れてから途中状態のコミットが消えました。
タイムアウトと中断を、ジョブ側で握る
エージェントは、ときどき思考が長引きます。CI のグローバルタイムアウトに任せると、強制終了がエージェントの途中状態を残したまま落ちます。だから外側で時間を握ります。
# 10 分で打ち切り、その際は SIGTERM の後 15 秒で SIGKILL
timeout --signal=TERM --kill-after=15 600 \
antigravity run --prompt-file ./tasks/update-articles.md --yes --output json < /dev/null
rc = $?
if [ " $rc " -eq 124 ]; then
echo "タイムアウトで打ち切りました"
fi
ポイントは、いきなり SIGKILL で殺さないことです。SIGTERM を先に送り、エージェントに後始末の猶予を与えます。私は本番運用で --kill-after を入れずに痛い目を見ました。書き込み途中のファイルが残り、次の実行がそれを正だと誤認したのです。
終了コードと JSON で成否を機械判定する
無人実行の価値は、失敗したときだけ人間に届くことです。成功も失敗も同じログに流していたら、誰も見なくなります。
Antigravity CLI は --output json で構造化された結果を返します。終了コードと合わせて、二段で判定します。
out = $( timeout 600 antigravity run \
--prompt-file ./tasks/update.md --yes --output json < /dev/null )
rc = $?
# 終了コードが 0 でない、または JSON の status が success でないなら失敗扱い
status = $( printf '%s' " $out " | jq -r '.status // "unknown"' )
if [ " $rc " -ne 0 ] || [ " $status " != "success" ]; then
printf '%s' " $out " | jq -r '.error // "(no error field)"' >&2
exit 1
fi
終了コードだけに頼らないのは、ツールが「実行は完了したが目的は達成していない」状態を 0 で返すことがあるからです。逆に JSON だけに頼ると、CLI 自体がクラッシュして JSON を吐けなかったケースを取りこぼします。両方を見て、どちらかが赤なら赤、と決めると安定します。
認証情報を、ログに出さずに渡す
CI で最も事故が多いのが認証情報です。Antigravity の API キーやリポジトリのトークンを、コマンドライン引数に直接書くと、プロセス一覧やログに残ります。
# 環境変数で渡す。引数に書かない。set -x のデバッグ時も漏れにくい
export ANTIGRAVITY_API_KEY = "$( cat /run/secrets/antigravity_key)"
# 渡したあとは履歴に残る危険のある変数を unset
antigravity run --prompt-file ./tasks/update.md --yes < /dev/null
unset ANTIGRAVITY_API_KEY
ファイルマウント経由(/run/secrets/)でキーを読み、環境変数に入れて渡します。引数には決して書きません。私は GitHub Actions のログにトークンが平文で残っていたのを後から見つけて、即ローテーションしたことがあります。それ以来、set -x を使うスクリプトでは認証系の行だけ set +x で囲む癖をつけました。
段階的に無人化する手順
いきなり全部を cron に載せず、次の順で慣らすことを推奨します。
ターミナルで --yes --output json < /dev/null を付けて手動実行し、対話なしで完走するか確認する
timeout と flock を足したラッパーを書き、手動でもう一度回す
終了コードと JSON の二段判定を入れ、わざと失敗させて通知が飛ぶか試す
CI の手動トリガーに載せ、ログに認証情報が漏れていないか目視する
cron に載せ、最初の 1 週間は毎日ログを確認する
この 5 段階を飛ばすと、たいてい 4 番目か 5 番目で痛い目を見ます。無人化は「動くこと」より「壊れたときに気づけること」を先に作るほうが、結局は早いと感じています。
次の一歩として、まずは手元の一番小さな反復作業を 1 つ選び、ステップ 1 の手動 headless 実行だけ試してみてください。そこで詰まる箇所が、あなたの環境固有の設計課題を教えてくれます。