ADR 0013: Antigravity CLI adapter contract¶
Status¶
Accepted — 2026-09-20 (proposed 2026-09-19; revised after spikes S1–S8, see docs/spikes/antigravity.md)
Context¶
The first real ImageProvider drives Google's Antigravity CLI (agy on PATH; written against 1.2.3, spiked against 1.2.6) in headless mode. Verified from the official headless docs and agy --help:
agy -p "<prompt>" --output-format jsonprints one JSON envelope to stdout; diagnostics go to stderr.- Envelope fields:
conversation_id,status,response,error?,duration_seconds,num_turns,usage{input_tokens, output_tokens, thinking_tokens, cache_read_tokens, total_tokens}. status∈SUCCESS | ERROR | CANCELED | INTERRUPTED | INVALID | WAITING | RUNNING. Exit0on success; non-zero (observed1) on failure. Unknown--model→ exit1with anERRORenvelope.- Flags:
--add-dir <abs>(repeatable),--print-timeout <dur>(default5m),--model <slug>(agy models),--effort low|medium|high,--dangerously-skip-permissions. - Headless mode uses cached credentials; an unauthenticated run exits with
authentication required. - Permission-gated tools are soft-denied in headless mode (exit 0, notice on stderr) unless allowed via
permissions.allowin~/.gemini/antigravity-cli/settings.jsonor--dangerously-skip-permissions.
Spikes S1–S8 have since been run against agy 1.2.6 (docs/spikes/antigravity.md). They confirmed viability and falsified four of this ADR's original design points:
generate_imageexists headlessly, with exactly two parameters:ImageNameandPrompt. There is no path, size, aspect-ratio, seed or reference parameter, andtool_info.outputisnull.- Output lands in
~/.gemini/antigravity-cli/brain/<conversation_id>/<ImageName>_<epoch_ms>.jpg— not in--add-dirand not inscratch/. The path is deterministic because the envelope returnsconversation_id. "16:9 widescreen"in the prompt reliably yields 1376×768 JPEG (5 of 5); omitting the ratio gives an unpredictable size. Exact dimensions are never controllable.- A reference image is read with
view_fileand described into the prompt; its bytes never reach the image model. generate_imagerequires no permission grant (S5), so--dangerously-skip-permissionsis unnecessary.--print-timeoutexpiry returns exit0withstatus: SUCCESS, an emptyresponseand zerousage(S6c). A timeout looks like a success that produced no file.- Two concurrent invocations succeed and are faster per run than sequential ones (S7); seven generations moved the five-hour quota by 1%.
usagecarries tokens only — no credit, price or currency field (S8).
Still unverified: the unauthenticated path (S6a). Isolating the profile via USERPROFILE/HOME does not work, and confirming it would require signing the maintainer out, so the authentication required behaviour remains documentation-only.
Decision¶
providers/antigravity.py is a subprocess adapter designed around the verified facts only:
- Build the prompt from
providers/antigravity_wrapper.j2, which (a) instructs the agent to callgenerate_imageexactly once, (b) states the aspect ratio in words ("16:9 widescreen") — measured as the only lever on output size, (c) lists reference image absolute paths so the agent canview_fileand describe them, (d) forbids running shell commands or other tools. It does not state an output path:generate_imagehas no path parameter, so the instruction is unachievable and the agent spends a turn apologising for it. - Run
[binary, "-p", prompt, "--output-format", "json", "--add-dir", str(workdir), *("--add-dir", d for d in reference_dirs), "--print-timeout", f"{timeout_s}s", *(["--dangerously-skip-permissions"] if skip_permissions else []), *(["--model", model] if model else []), *(["--effort", effort] if effort else [])]viaasyncio.create_subprocess_exec(..., cwd=workdir, stdout=PIPE, stderr=PIPE). - Locate the output by globbing
~/.gemini/antigravity-cli/brain/<conversation_id>/for an image, using theconversation_idfrom the envelope, then copy it into the asset store. Success ⇔ exit code0andstatus == "SUCCESS"and exactly one such image is found and Pillow opens it. -
Error mapping:
Order matters: the timeout row must be evaluated before the missing-output row, because a
--print-timeoutexpiry presents asSUCCESSwith no file (S6c). Reversing them turns every long generation into a permanent failure and abandons the batch item.Observation Exception Retryable SUCCESS, emptyresponse,total_tokens == 0, no output imageProviderTimeoutError(agy's own print timeout)yes SUCCESSbut no output image found inbrain/<conversation_id>/ProviderOutputMissingError(message names the brain directory)no status ∈ {CANCELED, INTERRUPTED}ProviderTransientErroryes ERRORanderrorcontainsauthentication requiredProviderAuthError(mapping is defensive; S6a unverified)no ERRORotherwise /INVALID/WAITING/RUNNINGProviderPermanentErrorno subprocess exceeds timeout_s + 30ProviderTimeoutError(process killed)yes binary not found ProviderPermanentErrorwith hint "install Antigravity CLI"no -
usagetokens andduration_secondsare written toiteration.cost_json; the whole envelope toprovider_response_json; stdout/stderr to per-iteration log files. - Never uses
--continue/--conversation; every image is a fresh conversation. - Settings
[providers.antigravity] binary="agy" model=null effort="low" timeout_s=600 skip_permissions=false. S5 superseded decision D4:generate_imageneeds neither the flag nor apermissions.allowrule, so the default no longer opts into--dangerously-skip-permissions. The flag stays configurable for users whosesettings.jsonis more restrictive. - Capabilities, as measured:
reference=False(S4 — the agent describes a reference rather than conditioning on it),seed=False,negative=True(prompt-only),aspect=Truebut influence only — the ratio words steer the result and the exact size is never guaranteed (S3),max_batch=1,max_concurrency=2(S7 measured two concurrent runs as safe and faster),formats={"jpeg"}(S3 — JPEG in 7 of 7). provider check antigravityreports the contents of~/.gemini/config/plugins/. Every headless run inherits the developer's globally installed plugins — the S4 run opened asuperpowersskill file unprompted — so generation is not a pure function of thumbforge's inputs, and a surprising result must be diagnosable.
Consequences¶
- P3.3 ran S1–S8 (
docs/spikes/antigravity.md); this ADR is nowAcceptedand P3.4 is unblocked. The "external image file" fallback provider is not needed. - Every iteration is a fresh process, so cost is one CLI invocation per image. Concurrency is 2 rather than 1, on measured evidence.
imaging/fit.pymust upscale 1376×768 → 1920×1080 (1.40×) and crop the 1.792 ratio to 1.778, per decision D2. Dropping the default to 1280×720 would avoid the upscale; D2 chose 1920×1080 knowingly.- Batch style consistency cannot rely on image-to-image conditioning (S4), so ADR 0008's "AI paints the background; Pillow renders the text" carries more weight than when it was written.
- Cost reporting (P8.3) is limited to tokens,
duration_secondsand the/usagepercentages: the CLI exposes no monetary figure (S8).
Alternatives considered¶
- Calling a Google image API directly (HTTP) — rejected for v1: requires API keys and billing setup, whereas
agyreuses the user's existing login; may become a second provider. - Driving
agyinteractively via a PTY — rejected: fragile parsing, no JSON envelope, not portable to Windows CI. --output-format stream-jsonas the primary mode — rejected: the single JSON envelope is sufficient for success/failure, and S3 measuredtool_info.outputasnull, so the stream does not even carry the produced path. Stream mode is used only by spikes for inspection.