From 0af98c3a007c81b0f15fef1e86fb98362701b2a4 Mon Sep 17 00:00:00 2001 From: 4ndreello <4ndreello@users.noreply.github.com> Date: Tue, 8 Sep 2026 15:37:59 -0300 Subject: [PATCH 1/3] fix(antigravity): Attach prompt to -p flag agy -p takes the prompt as its value, so a bare -p up front swallowed --output-format as the prompt and left the real prompt as a stray positional. Every antigravity session failed at spawn with '-p took --output-format as its prompt'. Pass -p= last instead, which also stays unambiguous when the prompt starts with a dash. --- src/drivers/antigravity/driver.ts | 8 ++++++-- tests/antigravity-driver.test.ts | 9 +++++++-- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/src/drivers/antigravity/driver.ts b/src/drivers/antigravity/driver.ts index 5febaa7..0b7e588 100644 --- a/src/drivers/antigravity/driver.ts +++ b/src/drivers/antigravity/driver.ts @@ -10,7 +10,7 @@ import { parseAntigravityLine } from "./parser.js"; // Pure so the flag spellings and effort clamping are testable without spawning agy. export function buildAntigravityArgs(options: StartOptions): string[] { - const args: string[] = ["-p", "--output-format", "stream-json", "--dangerously-skip-permissions"]; + const args: string[] = ["--output-format", "stream-json", "--dangerously-skip-permissions"]; if (options.resumeSessionId) { args.push("--conversation", options.resumeSessionId); @@ -27,7 +27,11 @@ export function buildAntigravityArgs(options: StartOptions): string[] { args.push("--effort", clampedEffort); } - args.push(options.prompt); + // -p/--print takes the prompt as its value, so a bare -p up front swallows + // the next flag as the prompt ("-p took --output-format as its prompt"). + // Attach with `=` so the prompt is unambiguous anywhere on the line, even + // when it starts with a dash. + args.push(`-p=${options.prompt}`); return args; } diff --git a/tests/antigravity-driver.test.ts b/tests/antigravity-driver.test.ts index a6c37ff..50ed519 100644 --- a/tests/antigravity-driver.test.ts +++ b/tests/antigravity-driver.test.ts @@ -10,14 +10,19 @@ describe("buildAntigravityArgs", () => { it("builds default headless flags with auto-permissions and prompt", () => { const args = buildAntigravityArgs(base); expect(args).toEqual([ - "-p", "--output-format", "stream-json", "--dangerously-skip-permissions", - "do something", + "-p=do something", ]); }); + it("attaches the prompt to -p so it cannot swallow the next flag", () => { + const args = buildAntigravityArgs(base); + expect(args).not.toContain("-p"); + expect(args[args.length - 1]).toBe("-p=do something"); + }); + it("adds --conversation on resume", () => { const args = buildAntigravityArgs({ ...base, resumeSessionId: "conv-1234" }); expect(args).toContain("--conversation"); From 8c5527144041db89aa78479af7196b2d590dce8f Mon Sep 17 00:00:00 2001 From: 4ndreello <4ndreello@users.noreply.github.com> Date: Tue, 8 Sep 2026 15:47:45 -0300 Subject: [PATCH 2/3] fix(antigravity): Parse tab-separated models list agy models separates id and display name with a single TAB, but the parser split on two-or-more spaces, so every catalog id carried the display name along (id\tname). Those dirty ids were stored in role bindings and agy rejected them at spawn with 'invalid model selection'. Split on TAB first, fall back to column spacing, and drop the banner line that also leaked into the catalog. --- src/drivers/antigravity/driver.ts | 88 +++++++++++++++++-------------- tests/antigravity-driver.test.ts | 35 +++++++++++- 2 files changed, 83 insertions(+), 40 deletions(-) diff --git a/src/drivers/antigravity/driver.ts b/src/drivers/antigravity/driver.ts index 0b7e588..e33694f 100644 --- a/src/drivers/antigravity/driver.ts +++ b/src/drivers/antigravity/driver.ts @@ -35,6 +35,54 @@ export function buildAntigravityArgs(options: StartOptions): string[] { return args; } +// Pure so the TAB-separated `agy models` table is testable without the binary. +export function parseAntigravityModelsList(stdout: string): ProviderModels[] { + const lines = stdout.split("\n"); + const providerMap = new Map(); + + for (const rawLine of lines) { + // Strip spinner prefix if present (e.g. ⠋ Fetching available models...) + const clean = rawLine.replace(/^[⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏\s]+Fetching available models\.\.\./g, "").trim(); + if (!clean) continue; + // Banner without spinner prefix is not a model row either. + if (/^fetching available models\.\.\./i.test(clean)) continue; + + // `agy models` separates id and display name with a TAB; fall back to + // column spacing for anything else. + const tab = clean.indexOf("\t"); + const parts = tab >= 0 ? [clean.slice(0, tab), clean.slice(tab + 1)] : clean.split(/\s{2,}/); + const id = parts[0]?.trim(); + const name = parts[1]?.trim() || id; + if (!id || id.startsWith("#") || id.startsWith("Usage")) continue; + + let provider = "google"; + if (id.startsWith("claude-")) provider = "anthropic"; + else if (id.startsWith("gpt-")) provider = "openai"; + + if (!providerMap.has(provider)) { + providerMap.set(provider, []); + } + + providerMap.get(provider)!.push({ + id, + name, + provider, + supportsThinking: id.includes("thinking") || id.includes("high") || id.includes("medium"), + }); + } + + const result: ProviderModels[] = []; + for (const [provider, models] of providerMap.entries()) { + result.push({ + provider, + displayName: provider.charAt(0).toUpperCase() + provider.slice(1), + models, + }); + } + + return result; +} + export class AntigravityDriver extends SessionDriver { readonly id = "antigravity" as const; @@ -136,45 +184,7 @@ export class AntigravityDriver extends SessionDriver { try { const stdout = await runCommandWithTimeout(install.path, ["models"], { timeoutMs: 10000 }); - const lines = stdout.split("\n"); - const providerMap = new Map(); - - for (const rawLine of lines) { - // Strip spinner prefix if present (e.g. ⠋ Fetching available models...) - const clean = rawLine.replace(/^[⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏\s]+Fetching available models\.\.\./g, "").trim(); - if (!clean) continue; - - const parts = clean.split(/\s{2,}/); - const id = parts[0]?.trim(); - const name = parts[1]?.trim() || id; - if (!id || id.startsWith("#") || id.startsWith("Usage")) continue; - - let provider = "google"; - if (id.startsWith("claude-")) provider = "anthropic"; - else if (id.startsWith("gpt-")) provider = "openai"; - - if (!providerMap.has(provider)) { - providerMap.set(provider, []); - } - - providerMap.get(provider)!.push({ - id, - name, - provider, - supportsThinking: id.includes("thinking") || id.includes("high") || id.includes("medium"), - }); - } - - const result: ProviderModels[] = []; - for (const [provider, models] of providerMap.entries()) { - result.push({ - provider, - displayName: provider.charAt(0).toUpperCase() + provider.slice(1), - models, - }); - } - - return result; + return parseAntigravityModelsList(stdout); } catch { return []; } diff --git a/tests/antigravity-driver.test.ts b/tests/antigravity-driver.test.ts index 50ed519..54dcfa3 100644 --- a/tests/antigravity-driver.test.ts +++ b/tests/antigravity-driver.test.ts @@ -1,5 +1,9 @@ import { describe, it, expect } from "vitest"; -import { buildAntigravityArgs, AntigravityDriver } from "../src/drivers/antigravity/driver.js"; +import { + buildAntigravityArgs, + parseAntigravityModelsList, + AntigravityDriver, +} from "../src/drivers/antigravity/driver.js"; import { parseAntigravityLine } from "../src/drivers/antigravity/parser.js"; import type { AgentEvent } from "../src/core/events.js"; @@ -225,6 +229,35 @@ describe("parseAntigravityLine", () => { }); }); +describe("parseAntigravityModelsList", () => { + // Mirrors real `agy models` output: a banner line plus TAB-separated rows. + const stdout = [ + "Fetching available models...", + "gemini-3.8-flash-high\tGemini 3.8 Flash (High)", + "claude-sonnet-4-6\tClaude Sonnet 4.6 (Thinking)", + "gpt-oss-120b-medium\tGPT-OSS 120B (Medium)", + "", + ].join("\n"); + + it("splits TAB-separated id and display name", () => { + const providers = parseAntigravityModelsList(stdout); + const ids = providers.flatMap((p) => p.models.map((m) => m.id)); + expect(ids).toEqual(["gemini-3.8-flash-high", "claude-sonnet-4-6", "gpt-oss-120b-medium"]); + const names = providers.flatMap((p) => p.models.map((m) => m.name)); + expect(names).toEqual([ + "Gemini 3.8 Flash (High)", + "Claude Sonnet 4.6 (Thinking)", + "GPT-OSS 120B (Medium)", + ]); + }); + + it("drops the banner line instead of listing it as a model", () => { + const providers = parseAntigravityModelsList(stdout); + const ids = providers.flatMap((p) => p.models.map((m) => m.id)); + expect(ids.every((id) => !/fetching/i.test(id))).toBe(true); + }); +}); + describe("AntigravityDriver", () => { it("declares expected capabilities", () => { const driver = new AntigravityDriver(); From 6f4d2376c8113af56e4176b695c845b850d3ca62 Mon Sep 17 00:00:00 2001 From: 4ndreello <4ndreello@users.noreply.github.com> Date: Tue, 8 Sep 2026 15:54:36 -0300 Subject: [PATCH 3/3] test(antigravity): Cover dash prompt and spinner banner Review feedback on PR #69: the dash-leading prompt and the spinner banner branch had verification only via throwaway probes. Pin both in the scoped suite. --- tests/antigravity-driver.test.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/antigravity-driver.test.ts b/tests/antigravity-driver.test.ts index 54dcfa3..4642afc 100644 --- a/tests/antigravity-driver.test.ts +++ b/tests/antigravity-driver.test.ts @@ -27,6 +27,11 @@ describe("buildAntigravityArgs", () => { expect(args[args.length - 1]).toBe("-p=do something"); }); + it("keeps a dash-leading prompt attached to -p", () => { + const args = buildAntigravityArgs({ ...base, prompt: "--help is a flag" }); + expect(args).toContain("-p=--help is a flag"); + }); + it("adds --conversation on resume", () => { const args = buildAntigravityArgs({ ...base, resumeSessionId: "conv-1234" }); expect(args).toContain("--conversation"); @@ -256,6 +261,14 @@ describe("parseAntigravityModelsList", () => { const ids = providers.flatMap((p) => p.models.map((m) => m.id)); expect(ids.every((id) => !/fetching/i.test(id))).toBe(true); }); + + it("strips spinner-prefixed banner output", () => { + const providers = parseAntigravityModelsList( + "⠋ Fetching available models...\ngemini-3.8-flash-high\tGemini 3.8 Flash (High)\n", + ); + const ids = providers.flatMap((p) => p.models.map((m) => m.id)); + expect(ids).toEqual(["gemini-3.8-flash-high"]); + }); }); describe("AntigravityDriver", () => {