金曜の夜、手元のデスクトップでは完璧に動いていたエージェントを、そのままスケジュール実行に載せました。翌朝ログを開くと、コミットもプッシュも何も起きていません。エラーすら出ていません。ただ、承認待ちの画面がどこにも表示されないまま、静かにタイムアウトしていたのです。
これは Antigravity 2.0 が IDE とチャット型エージェントの2アプリに分かれ、さらに Antigravity CLI(agy)や Gemini Enterprise Agent Platform 経由のクラウド実行まで経路が増えたことで、より起きやすくなった問題です。同じエージェント定義が、対話実行を前提にした瞬間に、無人実行では成立しなくなります。私自身、個人開発で複数のブログとアプリの更新を、日中は対話で、夜間はスケジュールした CLI で回しているので、この「経路差」に何度もつまずいてきました。
経路ごとに定義をフォークしてしまうと、片方を直してももう片方が古いまま腐っていきます。ここでは定義を1つに保ったまま、対話と無人の差を4点で吸収する設計を、動くコードとともに整理します。
経路差は「速さ」ではなく「前提」の違いで壊れる
無人実行が失敗するとき、原因はモデルの賢さでも速度でもありません。ほとんどが、対話実行なら人間が無意識に埋めていた前提が、無人経路では欠落することです。私の経験では、経路差は次の4か所にほぼ集約されます。
| 破綻点 | 対話(デスクトップ) | 無人(CLI・スケジュール・クラウド) |
| 承認 | 人が承認ダイアログで最終判断する | ダイアログが出せず、黙って待って落ちる |
| 文脈 | @ 参照や選択範囲、開いているフォルダが暗黙に渡る | それらは存在せず、渡した文字列だけが文脈 |
| 秘密情報 | キーチェーンから対話的に取り出せる | キーチェーンは開けず、環境変数か秘密ファイルのみ |
| 実行環境 | 対話シェルの PATH と作業フォルダが効く | 最小の PATH・作業ディレクトリ不定 |
大切なのは、これらを「無人実行でも動くように定義を書き換える」のではなく、「経路を1か所で判定し、経路ごとの差を吸収する層を挟む」ことです。定義本体はどちらの経路でも同じものを読みます。
まず経路を1か所で判定する
判定を各所に散らすと、対話用の分岐がコードのあちこちに増えて破綻します。エントリポイントで一度だけ判定し、以降は環境変数で引き回します。
# surface.sh — 実行経路を1か所で判定して引き回す
# 標準入出力が端末に繋がっていれば対話、そうでなければ無人とみなす
if [ -t 0 ] && [ -t 1 ] && [ -z "${AGY_SCHEDULED:-}" ]; then
export AGY_SURFACE="interactive"
else
export AGY_SURFACE="unattended"
fi
echo "surface=$AGY_SURFACE"
スケジューラや CLI から起動する場合は、確実に無人と分かるよう AGY_SCHEDULED=1 も併せて渡しておきます。端末判定だけに頼ると、パイプで繋いだ対話ケースを取りこぼすことがあるためです。この一行の保険が、後述の承認ポリシーを安全側に倒します。
承認は「人に尋ねる」と「許可リスト」を切り替える
無人実行で最も多い事故は、対話なら人が承認していた破壊的操作が、無人では承認できずに固まることです。かといって無人ではすべて自動許可、とすると今度は歯止めがなくなります。答えは、経路によって判断者を切り替えることです。
// approval-policy.js — 対話なら人に尋ね、無人なら事前許可リストで判定する
const ALLOW = new Set([
"read_file",
"search_repo",
"run:npm test",
"git:add",
"git:commit",
]);
// 無人経路では、明示的に許した操作だけを通し、それ以外は必ず止める
function decide(action, { surface }) {
if (surface === "interactive") return "ask"; // 人が最終判断する
if (ALLOW.has(action.key)) return "allow"; // 事前に許可した操作のみ自動実行
return "stop"; // 想定外は無条件で停止
}
module.exports = { decide };
ここで git:push や deploy を意図的に許可リストへ入れていないのは、無人経路での本番反映は「止まって翌朝に気づく」方が「勝手に走って夜中に壊す」より安全だと考えているからです。許可リストは足りなくて止まるくらいがちょうどよく、緩めるのは実績を見てからで十分です。夜間実行の歯止めの考え方は、Background Agent に夜間作業を任せるときのガードレール設計でも触れています。
秘密情報は解決順序を固定する
対話実行だけで動いていたエージェントは、たいていキーチェーンからトークンを取り出しています。ところが無人経路ではキーチェーンは開けません。ここで慌てて秘密をコードに埋めると事故のもとなので、解決順序を固定して、無人でも成立する経路を必ず1つ用意することを強く推奨します。
# resolve-secret.sh — 秘密情報の解決順序を固定する
# 環境変数 → 秘密ファイル → (対話時のみ)キーチェーン の順で探す
resolve_secret() {
local name="$1"
# 1) 環境変数(無人経路の第一候補)
if [ -n "${!name:-}" ]; then printf '%s' "${!name}"; return 0; fi
# 2) 秘密ファイル(ディレクトリはリポジトリ外に置き .gitignore で除外)
if [ -f "${AGY_SECRETS_DIR:-/nonexistent}/$name" ]; then
cat "${AGY_SECRETS_DIR}/$name"; return 0
fi
# 3) キーチェーン(対話時のみ・無人ではここに来ない)
if [ "$AGY_SURFACE" = "interactive" ]; then
security find-generic-password -a "$USER" -s "$name" -w 2>/dev/null && return 0
fi
echo "MISSING_SECRET:$name" >&2
return 1
}
順序を固定する意味は、対話でも無人でも「同じ名前で同じ値」が返ることにあります。対話のときだけキーチェーンから別の値が返ってきて、無人だと古い環境変数を拾う、といったずれが起きると、原因の特定に何時間も溶かします。私はこの解決関数を1つ用意してから、秘密情報まわりのデバッグ時間が明確に減りました。
文脈は暗黙に頼らず、定義の中に持たせる
デスクトップでは、開いているファイルや選択範囲、@ で参照したドキュメントが暗黙に文脈へ入ります。無人経路にはそれがありません。ですから、エージェントが必要とする文脈は、IDE の状態ではなくリポジトリ内のファイルとして明示的に固定します。
# context.sh — 無人でも成立するよう、文脈を明示ファイルから組み立てる
: "${AGY_WORKDIR:?作業ディレクトリを固定してください}"
cd "$AGY_WORKDIR"
CONTEXT_FILES=(
"docs/agent/TASK.md" # そのタスクの目的と完了条件
"docs/agent/CONVENTIONS.md" # 命名・書式などの取り決め
)
CONTEXT=""
for f in "${CONTEXT_FILES[@]}"; do
if [ -f "$f" ]; then
CONTEXT+=$'\n\n# '"$f"$'\n'"$(cat "$f")"
else
echo "文脈ファイルが見つかりません: $f" >&2; exit 1
fi
done
export AGY_CONTEXT="$CONTEXT"
こうしておくと、同じ文脈を対話でも読み込めます。IDE の「今開いているもの」に依存した瞬間、その定義は無人では別物になります。IDE とエージェントが別アプリになったことで文脈の持ち方が変わった点は、Antigravity 2.0 で IDE とエージェントが別アプリになって最初に困ったことにも通じます。
スケジュール登録の前にプリフライトで経路差を潰す
ここまでの吸収層を用意しても、スケジュール登録の瞬間に環境変数の設定漏れや PATH の欠落があれば、また夜間に黙って落ちます。そこで、登録の前に必ず通すプリフライト検査を1本置きます。
#!/usr/bin/env bash
# preflight.sh — スケジュール登録前に、無人経路の前提が揃っているか検査する
set -euo pipefail
fail=0
need() { command -v "$1" >/dev/null 2>&1 || { echo "PATH に $1 がありません"; fail=1; }; }
# 1) 無人経路で使うコマンドが最小 PATH でも見つかるか
need node; need git; need rg
# 2) 必須の秘密情報が env か秘密ファイルで解決できるか(キーチェーン頼みでないか)
for s in GEMINI_API_KEY DEPLOY_TOKEN; do
if [ -z "${!s:-}" ] && [ ! -f "${AGY_SECRETS_DIR:-/nonexistent}/$s" ]; then
echo "秘密情報 $s が無人経路で解決できません"; fail=1
fi
done
# 3) 作業ディレクトリが固定されているか(IDE の「開いているフォルダ」に依存しない)
if [ -z "${AGY_WORKDIR:-}" ] || [ ! -d "${AGY_WORKDIR:-/nonexistent}" ]; then
echo "AGY_WORKDIR が未設定、または存在しません"; fail=1
fi
# 4) 承認ポリシーが無人向けに読み込めるか
node -e "require('./approval-policy.js').decide({key:'git:push'},{surface:'unattended'})" \
>/dev/null 2>&1 || { echo "approval-policy.js を読み込めません"; fail=1; }
if [ "$fail" = 0 ]; then
echo "✅ 経路差なし: 無人実行に進めます"
else
echo "🛑 上記を解消してからスケジュール登録してください"; exit 1
fi
このプリフライトを登録手順の一部に組み込んでから、私の環境では、以前は週に2〜3回起きていた「夜間に起動はしたのに何もしていなかった」系の空振りが、月に1回あるかどうかまで、体感で約80%減りました。数字そのものより、失敗が「登録前の赤信号」に前倒しされたことが効いています。翌朝の事故として顕在化するより、その場で直せる方がはるかに安いのです。
つまずきやすい点
いくつか、実際に踏んだ落とし穴を挙げておきます。
-
端末判定だけで経路を決めないことです。パイプやリダイレクトを挟むと対話でも無人と誤判定され、逆にラッパー経由だと無人でも端末が繋がって見えることがあります。AGY_SCHEDULED のような明示フラグを併用してください。
-
承認ポリシーを「無人では全許可」に緩めないことです。歯止めのない無人実行は、一度暴れると被害が大きく、しかも夜間だと発見が遅れます。許可リストは狭く始め、実績を見てから広げます。
-
対話でだけ通っていた PATH を当てにしないことです。無人経路の PATH は最小です。エージェントが呼ぶコマンドは、絶対パスにするか、プリフライトで存在を確認してください。関連する command not found 系の詰まりは、実行環境の PATH 継承を見直すと解けることが多いです。
次の一手
まずは、いま対話で動かしているエージェントを1つだけ選び、surface.sh と preflight.sh を通して無人経路で空実行してみてください。承認・文脈・秘密情報・実行環境のどこで止まるかが、その1回で見えてきます。止まった箇所こそ、対話が黙って埋めていた前提です。私自身まだ経路を増やしている途中ですが、定義を1つに保つ設計は、経路が増えるほど効いてくると感じています。