@estebanforge/pi-antigravity-bridge 1.9.2 → 1.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -12,8 +12,8 @@
12
12
  // on disk and pi's inline diff review does not engage.
13
13
  //
14
14
  // /agy command: full runtime config surface (engine, mode, permissions,
15
- // bridge tools, model, thinking, digest, system prompt, acp binary) plus
16
- // doctor, auth, patch-cleanup, and session clear. Config persists to
15
+ // bridge tools, model, thinking, digest, system prompt) plus doctor,
16
+ // authentication instructions, patch-cleanup, and session clear. Config persists to
17
17
  // ~/.pi/agent/antigravity-bridge/config.json so toggles survive restarts.
18
18
 
19
19
  import os from "node:os";
@@ -59,15 +59,15 @@ import { detectPermissionGateExtensions, resolveGateMode } from "../src/approval
59
59
  import { hookScriptSource, removeGateHooks, stageGateHooks, sweepWorkspaceGateGroups } from "../src/approval-hook.js";
60
60
  import { StreamDriver } from "../src/driver.js";
61
61
  import { AcpDriver } from "../src/acp/driver.js";
62
- import { runAcpAuth } from "../src/acp/auth.js";
63
- import { setAuthInvalidatedHook } from "../src/acp/connection.js";
64
- import { setupAuthUrlCapture } from "../src/acp/browser-capture.js";
65
- import { clearAcpAuthVerified, ensureAcpReady, inspectAcpSetup } from "../src/acp/setup.js";
62
+ import { sweepStaleAcpHomes } from "../src/acp/home.js";
63
+ import { cleanupManagedAcpInstall } from "../src/acp/setup.js";
64
+ import { agyAuthenticationMessage } from "../src/agy-install.js";
65
+ import { createAgySetupOffer } from "../src/agy-setup.js";
66
66
  import { ensureQueuedMessagesDefaults } from "../src/queued-messages.js";
67
67
  import type { TurnDriver, TurnOutcome } from "../src/driver-types.js";
68
- import { CONFIG_PATH, loadConfig, logsDir, MAX_TURN_CAP_MIN, parseCapMinutes, saveConfig, type AgyMode, type BridgeTools, type Engine, type ThinkingTier } from "../src/config.js";
68
+ import { CONFIG_PATH, legacyAcpBinConfigured, loadConfig, logsDir, MAX_TURN_CAP_MIN, parseCapMinutes, saveConfig, type AgyMode, type BridgeTools, type Engine, type ThinkingTier } from "../src/config.js";
69
69
  import { agyMissingMessage, ENGINE_SETTINGS_ROW_ID, engineSettingsRow, isAgyInstalled, savedEngineMessage, showEnginePicker, shouldOfferEnginePicker } from "../src/engine-picker.js";
70
- import { checkAgyCliVersion, describeAgyVersionCheck, MIN_AGY_VERSION } from "../src/agy-version.js";
70
+ import { agyVersionVerdict, checkAgyCliVersion, describeAgyVersionCheck, MIN_AGY_ACP_VERSION, MIN_AGY_VERSION } from "../src/agy-version.js";
71
71
  import { isValidAgyAgentName, listAgyAgents } from "../src/agents.js";
72
72
  import { formatSubagentRoster, SubagentRoster } from "../src/subagent-roster.js";
73
73
  import { spawn } from "node:child_process";
@@ -80,7 +80,7 @@ import { agyConversationDir } from "../src/agy-paths.js";
80
80
  import { createDailyLogger, type DailyLogger } from "../src/daily-log.js";
81
81
  import { registerAskAntigravityTool, toolModelsFromRaw } from "../src/ask-tool.js";
82
82
  import { registerWebTools } from "../src/web-tools.js";
83
- import { bridgeMcpConfigDir, startMcpServer, TOKEN_HEADER, type McpServerHandle } from "../src/mcp-server.js";
83
+ import { bridgeMcpConfigDir, startMcpServer, sweepStaleBridgeDirs, TOKEN_HEADER, type McpServerHandle } from "../src/mcp-server.js";
84
84
  import {
85
85
  registerBridgeServer,
86
86
  healBridgeSuppression,
@@ -102,15 +102,8 @@ import { Type } from "typebox";
102
102
  import { patchStatus, restorePatch } from "../src/patch-cleanup.js";
103
103
  import { withDialogLock } from "../src/dialog-lock.js";
104
104
 
105
- // The server disowning our verified marker (-32000 anywhere: turn, session
106
- // new/load, cancel) must drop it, or ensureAcpReady keeps reporting a login
107
- // that no longer exists. Wire once at load; the hook is a no-op until set.
108
- setAuthInvalidatedHook(() => clearAcpAuthVerified());
109
-
110
- // Last UI seen (session_start / /agy commands). The ACP login URL arrives
111
- // via the driver log sink, which has no command context; the stash lets that
112
- // sink toast instead of only logging to stderr. Module scope: both the
113
- // default export (session_start, log sink) and registerAgyCommand assign it.
105
+ // Last UI seen (session_start / /agy commands). Module scope: both the
106
+ // default export and registerAgyCommand assign it.
114
107
  let activeUi: ExtensionUIContext | null = null;
115
108
 
116
109
  function resolveAgyBinary(): string {
@@ -211,26 +204,13 @@ export default async function (pi: ExtensionAPI): Promise<void> {
211
204
  // Approval-gate hook script (per-pid, token embedded). Written at session
212
205
  // start when the gate is active; removed at session_shutdown.
213
206
  let gateScriptPath: string | null = null;
214
- // ACP self-heal runs once per process (session_start re-fires on /reload;
215
- // a ready setup is two file stats, so re-running is harmless anyway).
216
- let acpSelfHealRan = false;
217
- // Warn-once-per-process flags for the missing-agy-CLI toast and the
218
- // too-old-agy version gate (both stream-json).
207
+ let managedAcpCleanupRan = false;
208
+ let legacyAcpWarningShown = false;
209
+ let agySetupOffer: (() => Promise<void>) | null = null;
219
210
  let agyMissingWarned = false;
220
211
  let agyVersionWarned = false;
221
- // OAuth URL capture: the server hands the login URL only to the
222
- // browser-open call (nothing on stdio), so a BROWSER wrapper records it
223
- // and the driver logs it as "auth-url". Local users keep the automatic
224
- // browser open; over SSH the URL surfaces for copy-paste with the
225
- // port-forward command.
226
- const authCapture = setupAuthUrlCapture();
227
- if (!authCapture && process.platform !== "win32") {
228
- // Rare (unwritable data dir). Surfacing the diagnostic: without it,
229
- // login URLs would silently stop appearing on headless boxes.
230
- console.error("[antigravity-bridge] OAuth URL capture unavailable; login URL surfacing is off (setup failed).");
231
- }
232
- // Two turn engines behind one contract (plan §9): the official ACP
233
- // server (default for new installs) and the stream-json `agy` CLI
212
+ // Two turn engines behind one contract: `agy acp` (default for new
213
+ // installs) and the stream-json `agy` CLI
234
214
  // (fallback). Neither spawns anything until its first turn.
235
215
  // ACP log routing: only genuine failures reach stderr. Routine lifecycle
236
216
  // (driver-created, spawn, session-new, ...) stays in the driver's
@@ -260,59 +240,17 @@ export default async function (pi: ExtensionAPI): Promise<void> {
260
240
  const level = failed ? "warn" : msg === "turn-start" ? "info" : "debug";
261
241
  fileLog.log(msg, data, level);
262
242
  };
263
- // Shared ACP log sink (driver turns AND /agy auth): the login URL event
264
- // toasts so SSH users can copy it; genuine failures reach stderr.
265
243
  const acpLog = (msg: string, data?: unknown): void => {
266
- // The daily file log gets EVERY driver event (auth-url stripped of its
267
- // query string - it carries one-time login state); the filters below
268
- // only decide what reaches the user.
269
- const fileData =
270
- msg === "auth-url"
271
- ? { port: (data as { port?: number | null } | undefined)?.port ?? null, url: String((data as { url?: string } | undefined)?.url ?? "").split("?")[0] }
272
- : data;
273
- // Failures warn; turn-start + auth-url are the always-on skeleton;
274
- // routine per-event lifecycle (spawn, session-load, unparked, ...) is
275
- // verbose-only. Deliberate teardown exits (Esc abort kill, shutdown,
276
- // idle recycle) demote to debug: routine, and the warn tier stays the
277
- // "what broke" grep.
278
244
  const expectedExit =
279
245
  msg === "connection-exited" &&
280
246
  (data as { expected?: boolean } | undefined)?.expected === true;
281
- const level = expectedExit
282
- ? "debug"
283
- : acpFailures.has(msg)
284
- ? "warn"
285
- : msg === "turn-start" || msg === "auth-url"
286
- ? "info"
287
- : "debug";
288
- fileLog.log(msg, fileData, level);
289
- if (msg === "auth-url") {
290
- const { url, port } = (data ?? {}) as { url?: string; port?: number | null };
291
- if (!url) return;
292
- // Blank lines fence the URL off from the rest of the text: it is long
293
- // and wraps, so separation keeps it readable and copyable.
294
- const parts = ["Google sign-in URL for the ACP engine:", "", url];
295
- if (port) {
296
- parts.push(
297
- "",
298
- "SSH session? Forward the port on your machine first:",
299
- ` ssh -N -L ${port}:127.0.0.1:${port} <user@host>`,
300
- );
301
- }
302
- const text = parts.join("\n");
303
- if (activeUi) activeUi.notify(text, "warning");
304
- else console.error(`[antigravity-bridge acp] ${text}`);
305
- return;
306
- }
307
- if (!acpFailures.has(msg)) return;
308
- // Console noise is gone: warns toast through the fileLog wrapper (with
309
- // a stderr fallback when no UI exists), and raw tails stay in the file
310
- // log behind AGY_DEBUG.
247
+ const level = expectedExit ? "debug" : acpFailures.has(msg) ? "warn" : msg === "turn-start" ? "info" : "debug";
248
+ fileLog.log(msg, data, level);
311
249
  };
312
250
  const acpDriver = new AcpDriver({
313
- // Resolved per connection: the setup flow can install the binary and
314
- // update acp.bin mid-session; the next turn picks it up (no restart).
315
- bin: () => loadConfig().acp.bin,
251
+ bin: binary,
252
+ binArgs: ["acp"],
253
+ isolateHome: true,
316
254
  // Resolved per turn: /agy and AGY_USAGE_ESTIMATE changes apply without
317
255
  // a restart. Without this the driver defaults to "estimate" and the
318
256
  // config knob (incl. "off") is dead.
@@ -334,7 +272,6 @@ export default async function (pi: ExtensionAPI): Promise<void> {
334
272
  return idx >= 0 ? (req.options[idx]?.optionId ?? undefined) : undefined;
335
273
  });
336
274
  },
337
- ...(authCapture ? { extraEnv: authCapture.browserEnv, authUrlFile: authCapture.file } : {}),
338
275
  log: acpLog,
339
276
  mcpServers: () => {
340
277
  const handle = mcpHandle;
@@ -441,7 +378,7 @@ export default async function (pi: ExtensionAPI): Promise<void> {
441
378
  return;
442
379
  }
443
380
  closePollRun();
444
- if (event.mcpServer === "pi-bridge") return;
381
+ if (/^pi-bridge(?:-\d+)?$/.test(event.mcpServer ?? "")) return;
445
382
  // MCP calls through our own pi-bridge already have genuine Pi tool cards;
446
383
  // only native Antigravity / other MCP events need display-only entries.
447
384
  if (event.status === "started") {
@@ -563,11 +500,9 @@ export default async function (pi: ExtensionAPI): Promise<void> {
563
500
  getMcpPort: () => mcpHandle?.port ?? null,
564
501
  bridgeTools: getBridgeTools,
565
502
  hiddenBridgeTools,
566
- acpLog,
567
503
  fileLog,
568
504
  roster,
569
- authCapture: authCapture ?? null,
570
- runAcpPickSetup,
505
+ checkAgySetup,
571
506
  });
572
507
 
573
508
  // AskAntigravity tool: one-shot delegation to agy (ported from
@@ -603,73 +538,48 @@ export default async function (pi: ExtensionAPI): Promise<void> {
603
538
  },
604
539
  });
605
540
 
606
- /** ACP pick follow-through (first-run wizard): the same self-service
607
- * setup the /agy engine acp command runs, but immediately - the 1.5 GB
608
- * download starts while the toast is still on screen, progress rides the
609
- * footer status, and the Google sign-in opens when the install lands.
610
- * Restart still applies the engine (drivers wire at load); this only
611
- * removes the wait. Fire-and-forget: the caller already toasted the
612
- * promise, failures land in the daily log + a warning toast. */
613
- // eslint-disable-next-line @typescript-eslint/no-inner-declarations -- hoisted: registerAgyCommand below injects it
614
- async function runAcpPickSetup(ctx: { ui: ExtensionUIContext }): Promise<void> {
615
- acpSelfHealRan = true;
616
- let lastPhase = "";
617
- ctx.ui.setStatus("agy-acp", "downloading ACP server…");
541
+ function offerSetup(ui: ExtensionUIContext, explicit = false): void {
542
+ if (agySetupOffer && explicit) {
543
+ ui.notify(`CLI setup was already offered in this session. ${agyMissingMessage()}`, "info");
544
+ }
545
+ agySetupOffer ??= createAgySetupOffer({
546
+ ui,
547
+ binaryOverride: process.env.AGY_BIN || undefined,
548
+ missingMessage: agyMissingMessage,
549
+ log: (event, data, level) => fileLog.log(event, data, level),
550
+ });
551
+ void agySetupOffer();
552
+ }
553
+
554
+ // Availability checks never block bridge startup. The setup offer owns
555
+ // prompting, deduplication, and failure containment.
556
+ async function checkAgySetup(ctx: { ui: ExtensionUIContext; mode: string }, selectedEngine: Engine, explicit = false): Promise<void> {
618
557
  try {
619
- const status = await ensureAcpReady({
620
- configBin: loadConfig().acp.bin,
621
- onProgress: (m) => {
622
- // Dual surface: the status bar carries the live percent (cleared
623
- // on completion, zero footprint); the chat window gets phase
624
- // milestones only (download start, unpacking, installed) - same
625
- // line-in-chat feel as other extensions' notify() notices. The
626
- // percent variant updates every chunk and would spam the chat.
627
- ctx.ui.setStatus("agy-acp", m);
628
- if (m !== lastPhase && !/\d+%/.test(m)) {
629
- ctx.ui.notify(m, "info");
630
- lastPhase = m;
631
- }
632
- },
633
- });
634
- ctx.ui.setStatus("agy-acp", undefined);
635
- fileLog.log(
636
- "acp-setup",
637
- status.ok
638
- ? { ok: true, binarySource: status.binarySource, needsLogin: status.needsLogin }
639
- : { ok: false, error: status.error },
640
- status.ok ? "info" : "warn",
641
- );
642
- if (!status.ok) {
643
- ctx.ui.notify(`ACP auto-setup failed (${status.error}).\n${status.manual}`, "warning");
558
+ if (!isAgyInstalled(binary)) {
559
+ if (ctx.mode === "tui") {
560
+ offerSetup(ctx.ui, explicit);
561
+ } else if (!agyMissingWarned) {
562
+ agyMissingWarned = true;
563
+ console.error(`[antigravity-bridge] ${agyMissingMessage()}`);
564
+ }
644
565
  return;
645
566
  }
646
- // Spread, not a bare acp patch: a bare {bin} patch would drop
647
- // sibling keys (usageEstimate) from the file.
648
- saveConfig({ acp: { ...loadConfig().acp, bin: status.bin } });
649
- if (!status.needsLogin) {
650
- ctx.ui.notify(`ACP server ready (auth: ${status.auth}). Restart applies the engine.`, "info");
567
+ if (agyVersionWarned) {
568
+ if (explicit && ctx.mode === "tui") offerSetup(ctx.ui, true);
651
569
  return;
652
570
  }
653
- ctx.ui.notify(
654
- "ACP server ready. Signing in: the Google sign-in opens in your browser and completes when you finish it.",
655
- "info",
656
- );
657
- const r = await runAcpAuth({
658
- bin: status.bin,
659
- ...(authCapture ? { extraEnv: authCapture.browserEnv, authUrlFile: authCapture.file } : {}),
660
- log: acpLog,
661
- });
662
- fileLog.log("acp-auth", r.ok ? { ok: true } : { ok: false, error: r.error }, r.ok ? "info" : "warn");
663
- if (r.ok) {
664
- ctx.ui.notify(
665
- r.note ? `Signed in (${r.note}). The ACP engine is ready; restart applies it.` : "Signed in. The ACP engine is ready; restart applies it.",
666
- "info",
667
- );
668
- } else ctx.ui.notify(`ACP sign-in failed (${r.error}).\nRun /agy auth to retry; /agy auth-manual has manual steps.`, "warning");
571
+ const rawCheck = await checkAgyCliVersion(binary);
572
+ const floor = selectedEngine === "acp" ? MIN_AGY_ACP_VERSION : MIN_AGY_VERSION;
573
+ const check = agyVersionVerdict(rawCheck.raw, floor);
574
+ if (check.status !== "unsupported") return;
575
+ agyVersionWarned = true;
576
+ const msg = `agy ${check.version} is too old for ${selectedEngine}; install ${floor} or newer.`;
577
+ if (ctx.mode === "tui") {
578
+ ctx.ui.notify(msg, "warning");
579
+ offerSetup(ctx.ui, explicit);
580
+ } else console.error(`[antigravity-bridge] ${msg}`);
669
581
  } catch (err) {
670
- ctx.ui.setStatus("agy-acp", undefined);
671
- fileLog.log("acp-setup", { error: String(err) }, "warn");
672
- ctx.ui.notify(`ACP setup failed (${String(err)}). /agy auth retries; /agy doctor inspects.`, "warning");
582
+ fileLog.log("agy-version-check", { error: String(err) }, "warn");
673
583
  }
674
584
  }
675
585
 
@@ -688,6 +598,7 @@ export default async function (pi: ExtensionAPI): Promise<void> {
688
598
  // (drivers wire at load). The await intentionally runs before the
689
599
  // bridge startup below: on a genuine first run the modal blocks input
690
600
  // anyway, so the delay is invisible.
601
+ let setupEngine = engine;
691
602
  if (event.reason === "startup" && ctx.mode === "tui" && shouldOfferEnginePicker(CONFIG_PATH)) {
692
603
  // Best-effort, like the legacy-patch notice below: a picker failure
693
604
  // (mid-prompt TUI teardown, resize races) must never take down the
@@ -696,40 +607,31 @@ export default async function (pi: ExtensionAPI): Promise<void> {
696
607
  try {
697
608
  const picked = await showEnginePicker(ctx.ui);
698
609
  if (picked) {
610
+ setupEngine = picked;
699
611
  saveConfig({ engine: picked });
700
612
  ctx.ui.notify(savedEngineMessage(picked), "info");
701
- if (picked === "acp") void runAcpPickSetup(ctx);
702
613
  }
703
614
  } catch (err) {
704
615
  fileLog.log("engine-picker", { error: String(err) }, "warn");
705
616
  console.error(`[antigravity-bridge] engine picker failed: ${String(err)}`);
706
617
  }
707
618
  }
708
- // agy presence check (stream-json engine): the CLI is the whole engine,
709
- // so a missing binary means every Antigravity turn would fail. Warn on
710
- // every process start until it is installed (per-process flag so /new,
711
- // /resume and /reload re-fires do not nag mid-session). Runs after the
712
- // picker above, so a first-run stream-json pick warns immediately.
713
- if (engine === "stream-json" && !agyMissingWarned && !isAgyInstalled(binary)) {
714
- agyMissingWarned = true;
715
- const msg = agyMissingMessage();
619
+ if (!legacyAcpWarningShown && legacyAcpBinConfigured()) {
620
+ legacyAcpWarningShown = true;
621
+ const msg = "Legacy AGY_ACP_BIN/acp.bin settings are ignored. Turns use `agy acp`; remove the stale override.";
716
622
  if (ctx.hasUI) ctx.ui.notify(msg, "warning");
717
623
  else console.error(`[antigravity-bridge] ${msg}`);
718
624
  }
719
- // Version gate (stream-json): an agy older than MIN_AGY_VERSION fails
720
- // turns with raw stream errors that look like bridge bugs. Warn once
721
- // with the fix in the message; doctor carries the full verdict.
722
- // Development builds and unreadable output stay silent here (an agy
723
- // from source works; doctor shows what we could not parse).
724
- if (engine === "stream-json" && !agyVersionWarned && isAgyInstalled(binary)) {
725
- void checkAgyCliVersion(binary).then((check) => {
726
- if (agyVersionWarned || check.status !== "unsupported") return;
727
- agyVersionWarned = true;
728
- const msg = `agy ${check.version} is too old; install ${MIN_AGY_VERSION} or newer. Stream-json turns may fail in confusing ways until then (/agy doctor inspects).`;
729
- if (ctx.hasUI) ctx.ui.notify(msg, "warning");
730
- else console.error(`[antigravity-bridge] ${msg}`);
731
- });
625
+ if (!managedAcpCleanupRan) {
626
+ managedAcpCleanupRan = true;
627
+ try {
628
+ const cleanup = cleanupManagedAcpInstall();
629
+ if (cleanup.removedBuilds.length > 0) fileLog.log("legacy-acp-cleanup", cleanup, "info");
630
+ } catch (err) {
631
+ fileLog.log("legacy-acp-cleanup", { error: String(err) }, "warn");
632
+ }
732
633
  }
634
+ void checkAgySetup(ctx, setupEngine);
733
635
  // Legacy cleanup: users who ran the old consent-gated patcher still
734
636
  // carry pi.invokeTool in their installed pi. Inert, but tell them once
735
637
  // and offer /agy patch-cleanup. Never auto-edits the install.
@@ -747,36 +649,6 @@ export default async function (pi: ExtensionAPI): Promise<void> {
747
649
  } catch {
748
650
  /* detection is best-effort */
749
651
  }
750
- // ACP self-heal: engine=acp needs a server binary + auth. Silent when
751
- // everything is ready; installs from the registry and bootstraps auth
752
- // otherwise; manual instructions only on failure. Fire-and-forget: it
753
- // must not delay session start (and nothing spawns until the first turn).
754
- if (engine === "acp" && !acpSelfHealRan) {
755
- acpSelfHealRan = true;
756
- void ensureAcpReady({ configBin: loadConfig().acp.bin }).then((status) => {
757
- fileLog.log(
758
- "acp-self-heal",
759
- status.ok
760
- ? { ok: true, binarySource: status.binarySource, needsLogin: status.needsLogin }
761
- : { ok: false, error: status.error },
762
- status.ok ? "info" : "warn",
763
- );
764
- if (status.ok) {
765
- if (status.binarySource === "installed" || status.binarySource === "existing") {
766
- saveConfig({ acp: { ...loadConfig().acp, bin: status.bin } });
767
- }
768
- if (status.needsLogin) {
769
- const msg = acpLoginPending();
770
- if (ctx.hasUI) ctx.ui.notify(msg, "warning");
771
- else console.error(`[antigravity-bridge] ${msg}`);
772
- }
773
- return;
774
- }
775
- const msg = `ACP auto-setup failed (${status.error}).\n${status.manual}`;
776
- if (ctx.hasUI) ctx.ui.notify(msg, "warning");
777
- else console.error(`[antigravity-bridge] ${msg}`);
778
- });
779
- }
780
652
  // Bridge failure logger. Routine lifecycle (listening,
781
653
  // bridge-config-written/removed, closed) is normal startup/teardown
782
654
  // traffic: toasting it every session, or pinning it via stderr in
@@ -820,6 +692,10 @@ export default async function (pi: ExtensionAPI): Promise<void> {
820
692
  if (ctx.hasUI) ctx.ui.notify(msg, "warning");
821
693
  else console.error(msg);
822
694
  };
695
+ // Sweep crashed sessions even when the bridge is disabled.
696
+ sweepStaleBridgeServers();
697
+ sweepStaleBridgeDirs();
698
+ sweepStaleAcpHomes();
823
699
  // Start the bridge unless the user turned it off. No patch gate, no
824
700
  // consent flow: calls route through pi's normal toolUse loop.
825
701
  const bridgeMode: BridgeTools = loadConfig().bridgeTools;
@@ -922,14 +798,13 @@ export default async function (pi: ExtensionAPI): Promise<void> {
922
798
  );
923
799
  if (r.ok && r.handle) {
924
800
  mcpHandle = r.handle;
925
- // Stale entries swept at start; entries a crashed delegation left
926
- // suppressed are healed here - but only when no live delegation is in
801
+ // Entries a crashed delegation left suppressed are healed here,
802
+ // but only when no live delegation is in
927
803
  // flight anywhere (marker-aware heal). A blind re-enable used to
928
804
  // un-hide the bridge during another session's active delegation.
929
805
  // Global-config hygiene runs on BOTH engines: a crashed stream-json
930
806
  // session must not leave dead-pid entries or stuck suppression flags
931
807
  // for the next one.
932
- sweepStaleBridgeServers();
933
808
  healBridgeSuppression();
934
809
  // ACP supplies the bridge per-session (session/new | session/load);
935
810
  // only the stream-json CLI reads the global MCP config.
@@ -1053,8 +928,6 @@ export default async function (pi: ExtensionAPI): Promise<void> {
1053
928
  activeUi = null;
1054
929
  const h = mcpHandle;
1055
930
  mcpHandle = null;
1056
- await h?.close();
1057
- roundTrips.failAll(FAIL_REASON_SHUTDOWN);
1058
931
  // "recycle", NOT "shutdown": pi fires session_shutdown on /new, /resume
1059
932
  // and /fork (docs/extensions.md session lifecycle), not only on process
1060
933
  // exit. The drivers are process-lifetime singletons; closing them with
@@ -1065,6 +938,8 @@ export default async function (pi: ExtensionAPI): Promise<void> {
1065
938
  // ("quit") - the connection kill is identical and nothing runs after.
1066
939
  await streamDriver.close("recycle", "session shutdown");
1067
940
  await acpDriver.close("recycle", "session shutdown");
941
+ roundTrips.failAll(FAIL_REASON_SHUTDOWN);
942
+ await h?.close();
1068
943
  if (engine === "stream-json") unregisterBridgeServer(process.pid);
1069
944
  hiddenBridgeTools.clear();
1070
945
  // Approval gate: unstage hooks and remove the per-pid script. Pending
@@ -1084,14 +959,6 @@ export default async function (pi: ExtensionAPI): Promise<void> {
1084
959
 
1085
960
  // --- /agy command -----------------------------------------------------------
1086
961
 
1087
- /** Shared body for every ACP-login-pending moment (session_start self-heal,
1088
- * /agy engine acp). End-user simple: what to run, what happens, which
1089
- * account. Sign-in is explicit (/agy auth); it never rides the first
1090
- * message. */
1091
- function acpLoginPending(): string {
1092
- return `One-time sign-in needed to finish ACP setup. Run /agy auth: the Google sign-in opens in your browser. Use the Google account of your Antigravity subscription (the same account as your agy CLI login). If no browser opens, pi shows the sign-in URL to copy. The token stays on your machine; this extension never sees it.`;
1093
- }
1094
-
1095
962
  interface AgyCommandCtx {
1096
963
  entries: AgyModelEntry[];
1097
964
  store: SessionStore;
@@ -1103,18 +970,11 @@ interface AgyCommandCtx {
1103
970
  getMcpPort: () => number | null;
1104
971
  bridgeTools: () => Array<{ name: string }>;
1105
972
  hiddenBridgeTools: Set<string>;
1106
- /** Shared ACP log sink (login URL surfacing + failure events). */
1107
- acpLog: (msg: string, data?: unknown) => void;
1108
973
  /** Daily file logger (src/daily-log.ts); command + doctor surfacing. */
1109
974
  fileLog: DailyLogger;
1110
975
  /** Live subagent roster (folded in the provider); /agy subagents. */
1111
976
  roster: SubagentRoster;
1112
- /** Wizard-pick follow-through (download now + chained sign-in); reused
1113
- * by /agy engine's no-args modal so both entry points behave alike. */
1114
- runAcpPickSetup: (cmdCtx: { ui: ExtensionUIContext }) => Promise<void>;
1115
- /** BROWSER-capture handles; null when unavailable (Windows, unwritable
1116
- * data dir). /agy auth passes them to the sign-in process. */
1117
- authCapture: { browserEnv: Record<string, string>; file: string } | null;
977
+ checkAgySetup: (ctx: { ui: ExtensionUIContext; mode: string }, engine: Engine, explicit?: boolean) => Promise<void>;
1118
978
  }
1119
979
 
1120
980
  interface PendingConfig {
@@ -1161,13 +1021,13 @@ function statusText(ctx: AgyCommandCtx): string {
1161
1021
  const source = ctx.usingFallback ? "fallback (agy models failed)" : "discovered";
1162
1022
  return [
1163
1023
  "Antigravity bridge",
1164
- ` engine: ${config.engine}${config.engine === "stream-json" ? " (agy CLI fallback)" : " (official server)"}`,
1024
+ ` engine: ${config.engine}${config.engine === "stream-json" ? " (agy CLI fallback)" : " (agy acp)"}`,
1165
1025
  ` models: ${ctx.entries.length} ${source}`,
1166
1026
  ...settingsRows(ctx),
1167
1027
  ` sessions: ${ctx.store.size} bound`,
1168
1028
  ` config: ${CONFIG_PATH}`,
1169
1029
  "",
1170
- "Subcommands: /agy auth, /agy auth-manual, /agy engine stream-json|acp, /agy mode plan|accept-edits, /agy readonly on|off, /agy permissions on|off, /agy ask on|off, /agy model <alias>, /agy thinking low|medium|high, /agy agent <name|off>, /agy subagents, /agy quota, /agy artifacts [open <n|name>], /agy tasks [tail <id>], /agy bridge all|mcp|none, /agy tools [hide|show <name>|reset], /agy web on|off, /agy digest on|off, /agy system-prompt on|off, /agy timeout <1-1440|off>, /agy acp-bin <path|auto>, /agy patch-cleanup, /agy clear, /agy doctor",
1030
+ "Subcommands: /agy auth, /agy auth-manual, /agy engine stream-json|acp, /agy mode plan|accept-edits, /agy readonly on|off, /agy permissions on|off, /agy ask on|off, /agy model <alias>, /agy thinking low|medium|high, /agy agent <name|off>, /agy subagents, /agy quota, /agy artifacts [open <n|name>], /agy tasks [tail <id>], /agy bridge all|mcp|none, /agy tools [hide|show <name>|reset], /agy web on|off, /agy digest on|off, /agy system-prompt on|off, /agy timeout <1-1440|off>, /agy patch-cleanup, /agy clear, /agy doctor",
1171
1031
  ].join("\n");
1172
1032
  }
1173
1033
 
@@ -1175,7 +1035,7 @@ function statusText(ctx: AgyCommandCtx): string {
1175
1035
  function registerAgyCommand(pi: ExtensionAPI, ctx: AgyCommandCtx): void {
1176
1036
  pi.registerCommand("agy", {
1177
1037
  description:
1178
- "Antigravity provider: doctor (health + settings), settings picker, clear sessions. Usage: /agy [doctor|auth|auth-manual|engine stream-json|acp|mode plan|accept-edits|readonly on|off|permissions on|off|ask on|off|model <alias>|thinking low|medium|high|agent <name|off>|subagents|quota|artifacts [open <n|name>]|tasks [tail <id>]|bridge all|mcp|none|tools [hide|show <name>|reset]|web on|off|digest on|off|system-prompt on|off|timeout <1-1440|off>|acp-bin <path|auto>|patch-cleanup|clear]",
1038
+ "Antigravity provider: doctor (health + settings), settings picker, clear sessions. Usage: /agy [doctor|auth|auth-manual|engine stream-json|acp|mode plan|accept-edits|readonly on|off|permissions on|off|ask on|off|model <alias>|thinking low|medium|high|agent <name|off>|subagents|quota|artifacts [open <n|name>]|tasks [tail <id>]|bridge all|mcp|none|tools [hide|show <name>|reset]|web on|off|digest on|off|system-prompt on|off|timeout <1-1440|off>|patch-cleanup|clear]",
1179
1039
  handler: async (args, cmdCtx: ExtensionCommandContext) => {
1180
1040
  const ui = cmdCtx.ui;
1181
1041
  if (ui) activeUi = ui;
@@ -1240,7 +1100,7 @@ function registerAgyCommand(pi: ExtensionAPI, ctx: AgyCommandCtx): void {
1240
1100
  if (sub === "engine") {
1241
1101
  if (val === "acp" || val === "stream-json") {
1242
1102
  if (val === "acp" && loadConfig().mode === "plan") {
1243
- ui?.notify("mode is plan; the ACP engine has no plan mode. /agy mode accept-edits first.", "warning");
1103
+ ui?.notify("mode is plan; provider plan turns are disabled on ACP. /agy mode accept-edits first.", "warning");
1244
1104
  return;
1245
1105
  }
1246
1106
  const next = saveConfig({ engine: val });
@@ -1248,39 +1108,12 @@ function registerAgyCommand(pi: ExtensionAPI, ctx: AgyCommandCtx): void {
1248
1108
  ui?.notify("engine set to stream-json. Takes effect on the next pi start (or /reload).", "info");
1249
1109
  return;
1250
1110
  }
1251
- // Self-service setup: install the server from the official
1252
- // registry and bootstrap auth now, so the restart just works.
1253
- // Manual instructions only when a step fails.
1254
- ui?.notify("engine set to acp. Preparing the server (binary + auth)…", "info");
1255
- const status = await ensureAcpReady({
1256
- configBin: loadConfig().acp.bin,
1257
- onProgress: (m) => ui?.notify(m, "info"),
1258
- });
1259
- ctx.fileLog.log(
1260
- "acp-setup",
1261
- status.ok
1262
- ? { ok: true, binarySource: status.binarySource, needsLogin: status.needsLogin }
1263
- : { ok: false, error: status.error },
1264
- status.ok ? "info" : "warn",
1265
- );
1266
- if (!status.ok) {
1267
- ui?.notify(`ACP auto-setup failed (${status.error}).\n${status.manual}`, "warning");
1268
- return;
1269
- }
1270
- saveConfig({ acp: { ...loadConfig().acp, bin: status.bin } });
1271
- if (status.needsLogin) {
1272
- ui?.notify(
1273
- `ACP engine set. ${acpLoginPending()}`,
1274
- "warning",
1275
- );
1276
- } else {
1277
- ui?.notify(`ACP engine ready (auth: ${status.auth}). Takes effect on the next pi start (or /reload).`, "info");
1278
- }
1111
+ ui?.notify("engine set to acp. Turns will use `agy acp` and the CLI's existing authentication. Takes effect on the next pi start (or /reload).", "info");
1112
+ void ctx.checkAgySetup(cmdCtx, "acp", true);
1279
1113
  } else if (!val && mode === "tui" && ui) {
1280
1114
  // Same modal as the first-run wizard: switching engines deserves
1281
1115
  // the explanations, not a bare usage line. Semantics match the
1282
- // direct path above: plan blocks acp, an acp pick chains setup
1283
- // + sign-in immediately, restart applies the switch.
1116
+ // direct path above: plan blocks acp and restart applies the switch.
1284
1117
  await runEngineModal(ui, ctx);
1285
1118
  } else {
1286
1119
  ui?.notify(`current engine: ${loadConfig().engine}\nusage: /agy engine stream-json|acp`, "info");
@@ -1288,98 +1121,20 @@ function registerAgyCommand(pi: ExtensionAPI, ctx: AgyCommandCtx): void {
1288
1121
  return;
1289
1122
  }
1290
1123
  if (sub === "auth") {
1291
- // Gate on the CONFIGURED engine (disk + env), not the latched one:
1292
- // /agy engine acp followed by /agy auth in the same session works,
1293
- // no restart needed before signing in.
1294
- if (loadConfig().engine !== "acp") {
1295
- ui?.notify(`the selected engine is ${loadConfig().engine}. /agy engine acp first, then /agy auth.`, "warning");
1296
- return;
1297
- }
1298
- ui?.notify("Preparing the ACP server (binary + auth settings)…", "info");
1299
- const status = await ensureAcpReady({ configBin: loadConfig().acp.bin, onProgress: (m) => ui?.notify(m, "info") });
1300
- ctx.fileLog.log(
1301
- "acp-setup",
1302
- status.ok
1303
- ? { ok: true, binarySource: status.binarySource, needsLogin: status.needsLogin }
1304
- : { ok: false, error: status.error },
1305
- status.ok ? "info" : "warn",
1306
- );
1307
- if (!status.ok) {
1308
- ui?.notify(`ACP auto-setup failed (${status.error}).\n${status.manual}`, "warning");
1309
- return;
1310
- }
1311
- saveConfig({ acp: { ...loadConfig().acp, bin: status.bin } });
1312
- if (!status.needsLogin) {
1313
- ui?.notify(`Already signed in (auth: ${status.auth}). Nothing to do.`, "info");
1314
- return;
1315
- }
1316
- ui?.notify(
1317
- "Signing in: the Google sign-in opens in your browser and completes when you finish it (minutes-scale). If no browser opens, pi shows the sign-in URL to copy.",
1318
- "info",
1319
- );
1320
- const r = await runAcpAuth({
1321
- bin: status.bin,
1322
- extraEnv: ctx.authCapture?.browserEnv,
1323
- authUrlFile: ctx.authCapture?.file,
1324
- log: ctx.acpLog,
1325
- });
1326
- ctx.fileLog.log("acp-auth", r.ok ? { ok: true } : { ok: false, error: r.error }, r.ok ? "info" : "warn");
1327
- if (r.ok) {
1328
- ui?.notify(
1329
- r.note ? `Signed in (${r.note}). The ACP engine is ready; takes effect on the next pi start (or /reload).` : "Signed in. The ACP engine is ready; takes effect on the next pi start (or /reload).",
1330
- "info",
1331
- );
1332
- } else {
1333
- ui?.notify(`ACP sign-in failed (${r.error}).\nRun /agy auth to retry; /agy auth-manual has manual steps.`, "warning");
1334
- }
1335
- return;
1336
- }
1337
- if (sub === "acp-bin") {
1338
- const rest = (args ?? "").trim().split(/\s+/).slice(1).join(" ");
1339
- if (rest.length > 0) {
1340
- // Only the keyword compares case-insensitively; the path keeps its case.
1341
- const bin = rest.toLowerCase() === "auto" ? "" : rest.replace(/^~(?=\/|$)/, os.homedir());
1342
- // Spread, not a bare acp patch: a bare {bin} object
1343
- // would drop sibling keys (usageEstimate) from the file.
1344
- saveConfig({ acp: { ...loadConfig().acp, bin } });
1345
- ui?.notify(
1346
- bin
1347
- ? `acp.bin set to ${bin}. The next ACP turn (re)connects with it.`
1348
- : "acp.bin cleared. Auto-setup (or AGY_ACP_BIN) picks the binary on the next ACP turn.",
1349
- "info",
1350
- );
1351
- } else {
1352
- const cur = loadConfig().acp.bin;
1353
- ui?.notify(`acp.bin: ${cur || "(auto: setup installs, or AGY_ACP_BIN)"}\nusage: /agy acp-bin <path|auto>`, "info");
1354
- }
1124
+ ui?.notify(agyAuthenticationMessage(), "info");
1355
1125
  return;
1356
1126
  }
1357
1127
  if (sub === "auth-manual") {
1358
1128
  ui?.notify(
1359
1129
  [
1360
- "ACP engine authentication (one-time; usually automatic -",
1361
- "/agy engine acp and session start set this up for you):",
1130
+ "Both engines use the agy CLI and its cached Google credentials.",
1362
1131
  "",
1363
- "The server is Google's official Antigravity ACP, installed from Google's",
1364
- "own registry. Logging in uses your Antigravity subscription: the same",
1365
- "Google account and plan as the Antigravity CLI (agy). It is no different",
1366
- "from logging into the CLI; the server just keeps its own token file on",
1367
- "your machine, like any Google tool. This extension never sees your",
1368
- "credentials.",
1132
+ "Install on Linux or macOS:",
1133
+ " curl -fsSL https://antigravity.google/cli/install.sh | bash",
1134
+ "Install on Windows PowerShell:",
1135
+ " irm https://antigravity.google/cli/install.ps1 | iex",
1369
1136
  "",
1370
- "1. Server binary: auto-setup installs it. Manual: agy_acp_server.par from",
1371
- " the antigravity-acp registry; point acp.bin or AGY_ACP_BIN at it.",
1372
- '2. Default: put {"auth":{"type":"oauth-personal"}} in',
1373
- " ~/.gemini/antigravity-acp/settings.json, run /agy auth, and complete",
1374
- " the Google login that opens in your browser. No browser (SSH",
1375
- " session)? pi shows the sign-in URL to copy; forward the redirect",
1376
- " port over ssh (ssh -N -L <port>:127.0.0.1:<port> <user@host>), then",
1377
- " open the URL on your machine.",
1378
- ' Headless alternative: GEMINI_API_KEY + {"auth":{"type":"gemini-api-key"}}',
1379
- " (metered paid API - not your Antigravity plan). The key is used only",
1380
- " when that type is selected; with the default oauth-personal in place,",
1381
- " an exported key is ignored.",
1382
- "3. /agy doctor shows the server version when auth is OK.",
1137
+ agyAuthenticationMessage(),
1383
1138
  ].join("\n"),
1384
1139
  "info",
1385
1140
  );
@@ -1390,15 +1145,11 @@ function registerAgyCommand(pi: ExtensionAPI, ctx: AgyCommandCtx): void {
1390
1145
  const engine = ctx.engine;
1391
1146
  const snap = (engine === "acp" ? ctx.acpDriver : ctx.driver).snapshot();
1392
1147
  const port = ctx.getMcpPort();
1393
- // The CLI matters on both engines (AskAntigravity shells out to
1394
- // `agy -p` even when turns ride ACP), but on an ACP session a
1395
- // missing CLI is a supported shape, so label it optional there
1396
- // instead of showing a bare failure line.
1397
- const cliCheck = await checkAgyCliVersion(resolveAgyBinary());
1398
- const cliSuffix =
1399
- cliCheck.status === "unavailable" && engine === "acp"
1400
- ? " (optional here; AskAntigravity delegations use the CLI)"
1401
- : "";
1148
+ const rawCliCheck = await checkAgyCliVersion(resolveAgyBinary());
1149
+ const cliFloor = engine === "acp" ? MIN_AGY_ACP_VERSION : MIN_AGY_VERSION;
1150
+ const cliCheck = rawCliCheck.status === "ok" || rawCliCheck.status === "unsupported"
1151
+ ? agyVersionVerdict(rawCliCheck.raw, cliFloor)
1152
+ : rawCliCheck;
1402
1153
  const lines = [
1403
1154
  "Antigravity doctor (no tokens spent)",
1404
1155
  ` engine: ${engine}`,
@@ -1407,7 +1158,7 @@ function registerAgyCommand(pi: ExtensionAPI, ctx: AgyCommandCtx): void {
1407
1158
  ` driver stats: spawns=${snap.stats.spawns} turns=${snap.stats.turns} reused=${snap.stats.reused} recycles=${snap.stats.recycles}${snap.stats.lastRecycleReason ? ` (last: ${snap.stats.lastRecycleReason})` : ""}`,
1408
1159
  ` sessions: ${ctx.store.size} bound`,
1409
1160
  ` models: ${ctx.entries.length} ${ctx.usingFallback ? "FALLBACK (agy models failed)" : "discovered"}`,
1410
- ` agy cli: ${describeAgyVersionCheck(cliCheck)} (min ${MIN_AGY_VERSION})${cliSuffix}`,
1161
+ ` agy cli: ${describeAgyVersionCheck(cliCheck)} (min ${cliFloor})`,
1411
1162
  ` config: ${CONFIG_PATH}`,
1412
1163
  ` logs: ${logsDir()} (attach recent days' files when reporting issues; set AGY_DEBUG=1 to capture details)`,
1413
1164
  ];
@@ -1415,12 +1166,12 @@ function registerAgyCommand(pi: ExtensionAPI, ctx: AgyCommandCtx): void {
1415
1166
  lines.push(
1416
1167
  ` acp session: ${snap.acp.sessionId ?? "(none)"}`,
1417
1168
  ` acp server: ${snap.acp.serverVersion ?? "unknown"}${snap.acp.agentTitle ? ` (${snap.acp.agentTitle})` : ""}`,
1418
- ` acp stats: prompts=${snap.acp.prompts} created=${snap.acp.sessionsCreated} loaded=${snap.acp.sessionsLoaded} kills=${snap.acp.kills} reconnects=${snap.acp.reconnects} cancel=${snap.acp.cancelSupported === null ? "unprobed" : snap.acp.cancelSupported ? "supported" : "unsupported (kill+reload)"}`,
1169
+ ` acp stats: prompts=${snap.acp.prompts} created=${snap.acp.sessionsCreated} loaded=${snap.acp.sessionsLoaded} kills=${snap.acp.kills} reconnects=${snap.acp.reconnects}`,
1419
1170
  );
1420
1171
  // Gate B watch: silent while the server offers no token counts; one
1421
1172
  // line the day it starts (then real usage mapping is worth wiring).
1422
1173
  if (snap.acp.usageSeen) {
1423
- lines.push(" acp tokens: AVAILABLE in server payloads (wire real usage mapping next)");
1174
+ lines.push(" acp tokens: NATIVE exact usage in PromptResponse");
1424
1175
  } else if (config.acp.usageEstimate !== "off") {
1425
1176
  lines.push(` acp tokens: ESTIMATED client-side (mode: ${config.acp.usageEstimate}; auto-off once the server sends real usage)`);
1426
1177
  }
@@ -1429,13 +1180,9 @@ function registerAgyCommand(pi: ExtensionAPI, ctx: AgyCommandCtx): void {
1429
1180
  lines.push(" lifecycle (last 5):");
1430
1181
  for (const entry of snap.lifecycle.slice(-5)) lines.push(` ${entry}`);
1431
1182
  }
1432
- if (engine === "acp") {
1433
- const setup = inspectAcpSetup({ configBin: config.acp.bin });
1434
- lines.push(
1435
- ` acp binary: ${setup.bin ?? "not found (auto-setup offers install)"}${setup.source ? ` (${setup.source})` : ""}`,
1436
- ` acp auth: ${setup.auth ?? "not configured (auto-setup bootstraps)"}`,
1437
- );
1438
- }
1183
+ if (engine === "acp") lines.push(" acp transport: agy acp (shared CLI authentication, isolated MCP home)");
1184
+ const retiredAuth = path.join(os.homedir(), ".gemini", "antigravity-acp");
1185
+ if (fs.existsSync(retiredAuth)) lines.push(` retired auth: ${retiredAuth} is unused; remove it manually when no longer needed`);
1439
1186
  // The config surface lives here too, so doctor is the ONE status
1440
1187
  // surface (former /agy status is gone).
1441
1188
  lines.push("", "settings:", ...settingsRows(ctx));
@@ -1445,7 +1192,7 @@ function registerAgyCommand(pi: ExtensionAPI, ctx: AgyCommandCtx): void {
1445
1192
  if (sub === "mode") {
1446
1193
  if (val === "plan" || val === "accept-edits") {
1447
1194
  if (val === "plan" && ctx.engine === "acp") {
1448
- ui?.notify("plan cannot be enforced on provider turns on either engine: the ACP engine has no plan mode (RC01), and the CLI does not gate writes under plan (google-antigravity/antigravity-cli#1181). /agy mode accept-edits.", "warning");
1195
+ ui?.notify("plan cannot be enforced on provider turns: the CLI does not gate writes under plan (google-antigravity/antigravity-cli#1181), and provider plan turns are disabled on ACP. /agy mode accept-edits.", "warning");
1449
1196
  return;
1450
1197
  }
1451
1198
  const next = saveConfig({ mode: val as AgyMode });
@@ -1672,10 +1419,10 @@ function registerAgyCommand(pi: ExtensionAPI, ctx: AgyCommandCtx): void {
1672
1419
  return;
1673
1420
  }
1674
1421
  if (sub === "agent") {
1675
- // ACP has no agent slot in the protocol (RC01): refuse instead of
1422
+ // ACP has no per-turn agent slot in the protocol: refuse instead of
1676
1423
  // silently ignoring a configured agent.
1677
1424
  if (ctx.engine === "acp") {
1678
- ui?.notify("the ACP engine has no agent selection (RC01). /agy engine stream-json first.", "warning");
1425
+ ui?.notify("the ACP engine has no per-turn agent selection. /agy engine stream-json first.", "warning");
1679
1426
  return;
1680
1427
  }
1681
1428
  const name = (args ?? "").trim().split(/\s+/).slice(1).join(" ").trim();
@@ -2000,11 +1747,11 @@ async function openAgyPicker(ui: ExtensionUIContext, ctx: AgyCommandCtx): Promis
2000
1747
  // mode row can still produce plan while the engine is acp - either the
2001
1748
  // session-latched engine or the on-disk one, which the Switch engine row
2002
1749
  // can change mid-session without a restart. Plan cannot be enforced on
2003
- // provider turns on either engine (#1181 + RC01); refuse the combination.
1750
+ // provider turns on either engine; refuse the combination.
2004
1751
  const nextMode = pending.mode ?? config.mode;
2005
1752
  if (nextMode === "plan" && (ctx.engine === "acp" || config.engine === "acp")) {
2006
1753
  ui.notify(
2007
- "plan cannot be enforced on provider turns on either engine (google-antigravity/antigravity-cli#1181; ACP additionally has no plan mode, RC01). /agy mode accept-edits.",
1754
+ "plan cannot be enforced on provider turns (google-antigravity/antigravity-cli#1181); provider plan turns are disabled on ACP too. /agy mode accept-edits.",
2008
1755
  "warning",
2009
1756
  );
2010
1757
  return;
@@ -2042,8 +1789,7 @@ async function openAgyPicker(ui: ExtensionUIContext, ctx: AgyCommandCtx): Promis
2042
1789
 
2043
1790
  /** The engine-pick modal shared by /agy engine (no args) and the settings
2044
1791
  * picker's engine row: the same explanations as the first-run wizard and the
2045
- * same semantics — plan blocks acp, an acp pick chains setup + sign-in
2046
- * immediately, restart applies the switch. */
1792
+ * same semantics: plan blocks acp and restart applies the switch. */
2047
1793
  async function runEngineModal(ui: ExtensionUIContext, ctx: AgyCommandCtx): Promise<void> {
2048
1794
  const current = loadConfig().engine;
2049
1795
  const picked = await showEnginePicker(ui);
@@ -2056,10 +1802,10 @@ async function runEngineModal(ui: ExtensionUIContext, ctx: AgyCommandCtx): Promi
2056
1802
  return;
2057
1803
  }
2058
1804
  if (picked === "acp" && loadConfig().mode === "plan") {
2059
- ui.notify("mode is plan; the ACP engine has no plan mode. /agy mode accept-edits first.", "warning");
1805
+ ui.notify("mode is plan; provider plan turns are disabled on ACP. /agy mode accept-edits first.", "warning");
2060
1806
  return;
2061
1807
  }
2062
1808
  saveConfig({ engine: picked });
2063
1809
  ui.notify(savedEngineMessage(picked), "info");
2064
- if (picked === "acp") void ctx.runAcpPickSetup({ ui });
1810
+ if (picked === "acp") void ctx.checkAgySetup({ ui, mode: "tui" }, "acp", true);
2065
1811
  }