Ask Antigravity CLI Once Whether It Actually Answers, Right Before a Scheduled Run
When Gemini CLI shuts down on June 18 and you move to Antigravity CLI, an expired token or a bad first day can let an unattended job fail silently. Here is a preflight that probes the CLI once, classifies the failure, and decides whether the real job should start at all.
On June 18, Gemini CLI and the Gemini Code Assist IDE extension stop serving requests for AI Pro, Ultra, and free individual users. The successor is Antigravity CLI, rewritten in Go. For interactive use, switching is mostly a matter of reading gemini as antigravity on the day.
The hard part is the hours when no one is watching the screen. As an indie developer running several apps' and sites' update jobs on a nightly schedule, the first day on a freshly rewritten binary is exactly when small environmental hiccups show up: a token format that changed, a config search path that moved, a model that briefly refuses under launch-day load. The awkward part is that an agentic CLI can hit any of these and still exit with code 0, having produced nothing. Since the morning I found a job marked "successful" with no new output to show for it, I have put a small gate in front of every schedule: probe the CLI for life exactly once before the real work begins.
"Nothing Happened" Is Worse Than "It Failed"
A non-zero exit is the friendly case. You retry, and after N attempts you alert. The genuinely hard case is the run that returns as a clean success while never reaching the model at all.
On the first day of a new binary, this is a real possibility. Auth resolves to an empty token, the config path shifts so the default model cannot be resolved, or a launch-day spike makes the gateway briefly refuse. Each tends to look the same from the outside: the command starts, returns almost immediately, and exits cleanly. The downstream schedule treats that as success and moves on, leaving a hole — the one night when nothing got updated.
What helps is a stage that does not do the work, but asks whether the work is allowed to start. The idea is plain: send one tiny prompt, and check whether the expected string comes back. If it does not, the real job does not run tonight. Spend a cheap canary before paying for an expensive miss.
The Four Layers a Preflight Should Check
"Is it alive" sounds like one question, but there are degrees of being down. A preflight wants to separate four layers.
First, can the binary even be resolved. Right after migration you get the basic absences: the path is not set, or antigravity simply does not exist in another account's environment. command -v settles this instantly.
Second, is auth alive. An expired token or a wrong reference is the layer most likely to bite on day one. We infer this from error wording in the response body.
Third, does a request actually reach the model and come back. An unresolved default model, or a launch-day refusal, lives here. We send a short canary and check for the expected string.
Fourth, is the response non-empty. Exit code 0 with an empty body is the exact pattern this whole exercise exists to kill. Below a character threshold, we treat it as "never arrived."
The preflight checks these in order and returns a distinct exit code for wherever it stopped. Collapsing everything into a single "failed" pushes root-cause work back onto your hands the next morning.
✦
Thank you for reading this far.
Continue Reading
What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.
WHAT YOU'LL LEARN
✦Preflight logic that separates expired auth, a missing binary, exhausted quota, and an unreachable model, then decides mechanically whether to launch the real job
✦A complete wrapper script that probes liveness with a one-token canary prompt and writes a single JST log line
✦How to start in observe-only mode and set thresholds so a noisy probe does not stop healthy nights
Secure payment via Stripe · Cancel anytime
✦
Unlock This Article
Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.
The core of the implementation is just sending a fixed short prompt and checking for a fixed string. No heavy work at all. The script below lets you swap the CLI name with an environment variable, because during the migration window you want to flip between gemini and antigravity in one place. Adjust the non-interactive flag to match your build.
#!/usr/bin/env bash# preflight.sh — probe the CLI before launching the real job# exit codes: 0=OK / 10=binary missing / 11=timeout / 12=auth / 13=quota / 14=empty or unreachableset -uCLI_BIN="${AGENT_CLI:-antigravity}" # set AGENT_CLI=gemini to fall back to the old CLI in one placeCANARY_PROMPT='Reply with exactly this token and nothing else: PREFLIGHT_OK'EXPECT='PREFLIGHT_OK'TIMEOUT_SEC="${PREFLIGHT_TIMEOUT:-25}"LOG="${PREFLIGHT_LOG:-$HOME/preflight.log}"log() { echo "$(TZ=Asia/Tokyo date '+%F %T') [$CLI_BIN] $1" >> "$LOG"; }# Layer 1: resolve the binaryif ! command -v "$CLI_BIN" >/dev/null 2>&1; then log "NG binary-not-found"; exit 10fi# Layers 2-4: send the canary exactly onceOUT="$(timeout "$TIMEOUT_SEC" "$CLI_BIN" -p "$CANARY_PROMPT" 2>&1)"RC=$?if [ "$RC" -eq 124 ]; then log "NG timeout(${TIMEOUT_SEC}s)"; exit 11fi# infer auth and quota from the response body (case-insensitive)LOWER="$(printf '%s' "$OUT" | tr '[:upper:]' '[:lower:]')"case "$LOWER" in *unauthorized*|*authentication*|*credential*|*token*expired*|*not*logged*in*) log "NG auth"; exit 12 ;; *quota*|*rate*limit*|*resource*exhausted*|*429*) log "NG quota"; exit 13 ;;esac# Layer 4: presence of the expected string, and empty responseif printf '%s' "$OUT" | grep -q "$EXPECT"; then log "OK"; exit 0else CHARS=$(printf '%s' "$OUT" | wc -c | tr -d ' ') log "NG empty-or-unreachable chars=${CHARS}"; exit 14fi
Three things matter here. First, always cap with timeout — a preflight that hangs on a non-responsive model defeats its own purpose. Second, auth and quota are inferred from the body, not from a strict API status, so treat the wording as version-dependent and grow the case patterns as you operate. Third, put the final verdict on "is the expected string present." Not trusting the exit code is the whole point.
Don't Swallow Failures — Route Them by Type
The exit code the preflight returns is directly usable for routing downstream. "Won't start" is not one thing: expired auth needs a human to log back in, an exhausted quota recovers with time, and a timeout may be transient. Because each calls for a different next move, branch instead of retrying uniformly.
#!/usr/bin/env bash# run_job.sh — start the real job only after preflight passesset -uHERE="$(cd "$(dirname "$0")" && pwd)"LOG="${PREFLIGHT_LOG:-$HOME/preflight.log}"jlog() { echo "$(TZ=Asia/Tokyo date '+%F %T') $1" >> "$LOG"; }bash "$HERE/preflight.sh"case $? in 0) jlog "preflight passed -> start job" bash "$HERE/the_real_job.sh" jlog "job done rc=$?" ;; 10) jlog "ABORT binary missing — notify only; no auto-recovery" ;; 12) jlog "ABORT auth — needs re-login; do not start the job" ;; 13) jlog "SKIP quota — pass this time; re-evaluate next slot" ;; 11|14) jlog "RETRY-ONCE — re-preflight once after 60s" sleep 60 if bash "$HERE/preflight.sh"; then bash "$HERE/the_real_job.sh"; jlog "job done after retry rc=$?" else jlog "ABORT — re-preflight failed too; do not start" ; fi ;;esac
The discipline here is not to retry the failures that do not recover (missing binary, expired auth). Hammering something that cannot fix itself only dirties the log and burdens the morning version of you. Timeouts and empty responses, on the other hand, may be transient, so we wait 60 seconds and re-evaluate exactly once. Retries go only to the layers that have a chance of recovering.
Wire It in Front of the Schedule
To retrofit an existing cron or scheduler, replace the real command with run_job.sh. The real job script stays untouched. Keeping the preflight as a front gate that never edits the work itself is what makes this easy to maintain later.
It is worth folding in some double-run protection too. The preflight is light, but if a previous job runs long and the next overlaps, two canaries fire and burn quota for nothing. One line of mutual exclusion handles it.
# crontab example (run hourly, with a lock)0 * * * * /usr/bin/flock -n /tmp/agent_job.lock /home/me/jobs/run_job.sh
flock -n gives up immediately if a run already holds the lock. For scheduled work, stepping aside without waiting is the safer choice; a queue just means something runs twice somewhere later.
Start in Observe Mode, Then Set the Stop Criteria
Switching the preflight on in "stop the job on failure" mode from day one risks halting healthy nights over a misread wording. For the first few days, have the preflight only log its verdict while the real job runs as before — temporarily skew the case in run_job.sh toward "log only, always start."
After a few days of logs, once you have seen how the auth and quota wording actually appears, enable the stopping branches. My own bar for moving to enforcing mode is a stretch with zero false stops and at least one genuine hiccup caught correctly. A preflight that stops too often is worse than none. Grow the wording patterns in case as you go, and it will keep up across versions.
A First Step
Drop in preflight.sh alone, run bash preflight.sh; echo $? a few times, and confirm it returns 0 when things are fine. Then route just one nightly job — the one whose silent failure would hurt most — through run_job.sh, and watch the log only for a few days. It is the smallest insurance against waking up to a hole where last night's run should have been.
Share
Thank You for Reading
Antigravity Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.