@estebanforge/pi-antigravity-bridge 1.9.3 → 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, resolveInstalledArgs } 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,63 +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,
316
- // Registry-declared argv for the managed binary (e.g. linux --uid=).
317
- // Empty for env/config overrides, which own their argv. Resolved per
318
- // connection alongside bin so a mid-session install picks them up.
319
- binArgs: () => resolveInstalledArgs({ configBin: loadConfig().acp.bin }),
251
+ bin: binary,
252
+ binArgs: ["acp"],
253
+ isolateHome: true,
320
254
  // Resolved per turn: /agy and AGY_USAGE_ESTIMATE changes apply without
321
255
  // a restart. Without this the driver defaults to "estimate" and the
322
256
  // config knob (incl. "off") is dead.
@@ -338,7 +272,6 @@ export default async function (pi: ExtensionAPI): Promise<void> {
338
272
  return idx >= 0 ? (req.options[idx]?.optionId ?? undefined) : undefined;
339
273
  });
340
274
  },
341
- ...(authCapture ? { extraEnv: authCapture.browserEnv, authUrlFile: authCapture.file } : {}),
342
275
  log: acpLog,
343
276
  mcpServers: () => {
344
277
  const handle = mcpHandle;
@@ -445,7 +378,7 @@ export default async function (pi: ExtensionAPI): Promise<void> {
445
378
  return;
446
379
  }
447
380
  closePollRun();
448
- if (event.mcpServer === "pi-bridge") return;
381
+ if (/^pi-bridge(?:-\d+)?$/.test(event.mcpServer ?? "")) return;
449
382
  // MCP calls through our own pi-bridge already have genuine Pi tool cards;
450
383
  // only native Antigravity / other MCP events need display-only entries.
451
384
  if (event.status === "started") {
@@ -567,11 +500,9 @@ export default async function (pi: ExtensionAPI): Promise<void> {
567
500
  getMcpPort: () => mcpHandle?.port ?? null,
568
501
  bridgeTools: getBridgeTools,
569
502
  hiddenBridgeTools,
570
- acpLog,
571
503
  fileLog,
572
504
  roster,
573
- authCapture: authCapture ?? null,
574
- runAcpPickSetup,
505
+ checkAgySetup,
575
506
  });
576
507
 
577
508
  // AskAntigravity tool: one-shot delegation to agy (ported from
@@ -607,74 +538,48 @@ export default async function (pi: ExtensionAPI): Promise<void> {
607
538
  },
608
539
  });
609
540
 
610
- /** ACP pick follow-through (first-run wizard): the same self-service
611
- * setup the /agy engine acp command runs, but immediately - the 1.5 GB
612
- * download starts while the toast is still on screen, progress rides the
613
- * footer status, and the Google sign-in opens when the install lands.
614
- * Restart still applies the engine (drivers wire at load); this only
615
- * removes the wait. Fire-and-forget: the caller already toasted the
616
- * promise, failures land in the daily log + a warning toast. */
617
- // eslint-disable-next-line @typescript-eslint/no-inner-declarations -- hoisted: registerAgyCommand below injects it
618
- async function runAcpPickSetup(ctx: { ui: ExtensionUIContext }): Promise<void> {
619
- acpSelfHealRan = true;
620
- let lastPhase = "";
621
- 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> {
622
557
  try {
623
- const status = await ensureAcpReady({
624
- configBin: loadConfig().acp.bin,
625
- onProgress: (m) => {
626
- // Dual surface: the status bar carries the live percent (cleared
627
- // on completion, zero footprint); the chat window gets phase
628
- // milestones only (download start, unpacking, installed) - same
629
- // line-in-chat feel as other extensions' notify() notices. The
630
- // percent variant updates every chunk and would spam the chat.
631
- ctx.ui.setStatus("agy-acp", m);
632
- if (m !== lastPhase && !/\d+%/.test(m)) {
633
- ctx.ui.notify(m, "info");
634
- lastPhase = m;
635
- }
636
- },
637
- });
638
- ctx.ui.setStatus("agy-acp", undefined);
639
- fileLog.log(
640
- "acp-setup",
641
- status.ok
642
- ? { ok: true, binarySource: status.binarySource, needsLogin: status.needsLogin }
643
- : { ok: false, error: status.error },
644
- status.ok ? "info" : "warn",
645
- );
646
- if (!status.ok) {
647
- 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
+ }
648
565
  return;
649
566
  }
650
- // Spread, not a bare acp patch: a bare {bin} patch would drop
651
- // sibling keys (usageEstimate) from the file.
652
- saveConfig({ acp: { ...loadConfig().acp, bin: status.bin } });
653
- if (!status.needsLogin) {
654
- 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);
655
569
  return;
656
570
  }
657
- ctx.ui.notify(
658
- "ACP server ready. Signing in: the Google sign-in opens in your browser and completes when you finish it.",
659
- "info",
660
- );
661
- const r = await runAcpAuth({
662
- bin: status.bin,
663
- binArgs: status.args,
664
- ...(authCapture ? { extraEnv: authCapture.browserEnv, authUrlFile: authCapture.file } : {}),
665
- log: acpLog,
666
- });
667
- fileLog.log("acp-auth", r.ok ? { ok: true } : { ok: false, error: r.error }, r.ok ? "info" : "warn");
668
- if (r.ok) {
669
- ctx.ui.notify(
670
- r.note ? `Signed in (${r.note}). The ACP engine is ready; restart applies it.` : "Signed in. The ACP engine is ready; restart applies it.",
671
- "info",
672
- );
673
- } 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}`);
674
581
  } catch (err) {
675
- ctx.ui.setStatus("agy-acp", undefined);
676
- fileLog.log("acp-setup", { error: String(err) }, "warn");
677
- 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");
678
583
  }
679
584
  }
680
585
 
@@ -693,6 +598,7 @@ export default async function (pi: ExtensionAPI): Promise<void> {
693
598
  // (drivers wire at load). The await intentionally runs before the
694
599
  // bridge startup below: on a genuine first run the modal blocks input
695
600
  // anyway, so the delay is invisible.
601
+ let setupEngine = engine;
696
602
  if (event.reason === "startup" && ctx.mode === "tui" && shouldOfferEnginePicker(CONFIG_PATH)) {
697
603
  // Best-effort, like the legacy-patch notice below: a picker failure
698
604
  // (mid-prompt TUI teardown, resize races) must never take down the
@@ -701,40 +607,31 @@ export default async function (pi: ExtensionAPI): Promise<void> {
701
607
  try {
702
608
  const picked = await showEnginePicker(ctx.ui);
703
609
  if (picked) {
610
+ setupEngine = picked;
704
611
  saveConfig({ engine: picked });
705
612
  ctx.ui.notify(savedEngineMessage(picked), "info");
706
- if (picked === "acp") void runAcpPickSetup(ctx);
707
613
  }
708
614
  } catch (err) {
709
615
  fileLog.log("engine-picker", { error: String(err) }, "warn");
710
616
  console.error(`[antigravity-bridge] engine picker failed: ${String(err)}`);
711
617
  }
712
618
  }
713
- // agy presence check (stream-json engine): the CLI is the whole engine,
714
- // so a missing binary means every Antigravity turn would fail. Warn on
715
- // every process start until it is installed (per-process flag so /new,
716
- // /resume and /reload re-fires do not nag mid-session). Runs after the
717
- // picker above, so a first-run stream-json pick warns immediately.
718
- if (engine === "stream-json" && !agyMissingWarned && !isAgyInstalled(binary)) {
719
- agyMissingWarned = true;
720
- 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.";
721
622
  if (ctx.hasUI) ctx.ui.notify(msg, "warning");
722
623
  else console.error(`[antigravity-bridge] ${msg}`);
723
624
  }
724
- // Version gate (stream-json): an agy older than MIN_AGY_VERSION fails
725
- // turns with raw stream errors that look like bridge bugs. Warn once
726
- // with the fix in the message; doctor carries the full verdict.
727
- // Development builds and unreadable output stay silent here (an agy
728
- // from source works; doctor shows what we could not parse).
729
- if (engine === "stream-json" && !agyVersionWarned && isAgyInstalled(binary)) {
730
- void checkAgyCliVersion(binary).then((check) => {
731
- if (agyVersionWarned || check.status !== "unsupported") return;
732
- agyVersionWarned = true;
733
- 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).`;
734
- if (ctx.hasUI) ctx.ui.notify(msg, "warning");
735
- else console.error(`[antigravity-bridge] ${msg}`);
736
- });
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
+ }
737
633
  }
634
+ void checkAgySetup(ctx, setupEngine);
738
635
  // Legacy cleanup: users who ran the old consent-gated patcher still
739
636
  // carry pi.invokeTool in their installed pi. Inert, but tell them once
740
637
  // and offer /agy patch-cleanup. Never auto-edits the install.
@@ -752,36 +649,6 @@ export default async function (pi: ExtensionAPI): Promise<void> {
752
649
  } catch {
753
650
  /* detection is best-effort */
754
651
  }
755
- // ACP self-heal: engine=acp needs a server binary + auth. Silent when
756
- // everything is ready; installs from the registry and bootstraps auth
757
- // otherwise; manual instructions only on failure. Fire-and-forget: it
758
- // must not delay session start (and nothing spawns until the first turn).
759
- if (engine === "acp" && !acpSelfHealRan) {
760
- acpSelfHealRan = true;
761
- void ensureAcpReady({ configBin: loadConfig().acp.bin }).then((status) => {
762
- fileLog.log(
763
- "acp-self-heal",
764
- status.ok
765
- ? { ok: true, binarySource: status.binarySource, needsLogin: status.needsLogin }
766
- : { ok: false, error: status.error },
767
- status.ok ? "info" : "warn",
768
- );
769
- if (status.ok) {
770
- if (status.binarySource === "installed" || status.binarySource === "existing") {
771
- saveConfig({ acp: { ...loadConfig().acp, bin: status.bin } });
772
- }
773
- if (status.needsLogin) {
774
- const msg = acpLoginPending();
775
- if (ctx.hasUI) ctx.ui.notify(msg, "warning");
776
- else console.error(`[antigravity-bridge] ${msg}`);
777
- }
778
- return;
779
- }
780
- const msg = `ACP auto-setup failed (${status.error}).\n${status.manual}`;
781
- if (ctx.hasUI) ctx.ui.notify(msg, "warning");
782
- else console.error(`[antigravity-bridge] ${msg}`);
783
- });
784
- }
785
652
  // Bridge failure logger. Routine lifecycle (listening,
786
653
  // bridge-config-written/removed, closed) is normal startup/teardown
787
654
  // traffic: toasting it every session, or pinning it via stderr in
@@ -825,6 +692,10 @@ export default async function (pi: ExtensionAPI): Promise<void> {
825
692
  if (ctx.hasUI) ctx.ui.notify(msg, "warning");
826
693
  else console.error(msg);
827
694
  };
695
+ // Sweep crashed sessions even when the bridge is disabled.
696
+ sweepStaleBridgeServers();
697
+ sweepStaleBridgeDirs();
698
+ sweepStaleAcpHomes();
828
699
  // Start the bridge unless the user turned it off. No patch gate, no
829
700
  // consent flow: calls route through pi's normal toolUse loop.
830
701
  const bridgeMode: BridgeTools = loadConfig().bridgeTools;
@@ -927,14 +798,13 @@ export default async function (pi: ExtensionAPI): Promise<void> {
927
798
  );
928
799
  if (r.ok && r.handle) {
929
800
  mcpHandle = r.handle;
930
- // Stale entries swept at start; entries a crashed delegation left
931
- // 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
932
803
  // flight anywhere (marker-aware heal). A blind re-enable used to
933
804
  // un-hide the bridge during another session's active delegation.
934
805
  // Global-config hygiene runs on BOTH engines: a crashed stream-json
935
806
  // session must not leave dead-pid entries or stuck suppression flags
936
807
  // for the next one.
937
- sweepStaleBridgeServers();
938
808
  healBridgeSuppression();
939
809
  // ACP supplies the bridge per-session (session/new | session/load);
940
810
  // only the stream-json CLI reads the global MCP config.
@@ -1058,8 +928,6 @@ export default async function (pi: ExtensionAPI): Promise<void> {
1058
928
  activeUi = null;
1059
929
  const h = mcpHandle;
1060
930
  mcpHandle = null;
1061
- await h?.close();
1062
- roundTrips.failAll(FAIL_REASON_SHUTDOWN);
1063
931
  // "recycle", NOT "shutdown": pi fires session_shutdown on /new, /resume
1064
932
  // and /fork (docs/extensions.md session lifecycle), not only on process
1065
933
  // exit. The drivers are process-lifetime singletons; closing them with
@@ -1070,6 +938,8 @@ export default async function (pi: ExtensionAPI): Promise<void> {
1070
938
  // ("quit") - the connection kill is identical and nothing runs after.
1071
939
  await streamDriver.close("recycle", "session shutdown");
1072
940
  await acpDriver.close("recycle", "session shutdown");
941
+ roundTrips.failAll(FAIL_REASON_SHUTDOWN);
942
+ await h?.close();
1073
943
  if (engine === "stream-json") unregisterBridgeServer(process.pid);
1074
944
  hiddenBridgeTools.clear();
1075
945
  // Approval gate: unstage hooks and remove the per-pid script. Pending
@@ -1089,14 +959,6 @@ export default async function (pi: ExtensionAPI): Promise<void> {
1089
959
 
1090
960
  // --- /agy command -----------------------------------------------------------
1091
961
 
1092
- /** Shared body for every ACP-login-pending moment (session_start self-heal,
1093
- * /agy engine acp). End-user simple: what to run, what happens, which
1094
- * account. Sign-in is explicit (/agy auth); it never rides the first
1095
- * message. */
1096
- function acpLoginPending(): string {
1097
- 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.`;
1098
- }
1099
-
1100
962
  interface AgyCommandCtx {
1101
963
  entries: AgyModelEntry[];
1102
964
  store: SessionStore;
@@ -1108,18 +970,11 @@ interface AgyCommandCtx {
1108
970
  getMcpPort: () => number | null;
1109
971
  bridgeTools: () => Array<{ name: string }>;
1110
972
  hiddenBridgeTools: Set<string>;
1111
- /** Shared ACP log sink (login URL surfacing + failure events). */
1112
- acpLog: (msg: string, data?: unknown) => void;
1113
973
  /** Daily file logger (src/daily-log.ts); command + doctor surfacing. */
1114
974
  fileLog: DailyLogger;
1115
975
  /** Live subagent roster (folded in the provider); /agy subagents. */
1116
976
  roster: SubagentRoster;
1117
- /** Wizard-pick follow-through (download now + chained sign-in); reused
1118
- * by /agy engine's no-args modal so both entry points behave alike. */
1119
- runAcpPickSetup: (cmdCtx: { ui: ExtensionUIContext }) => Promise<void>;
1120
- /** BROWSER-capture handles; null when unavailable (Windows, unwritable
1121
- * data dir). /agy auth passes them to the sign-in process. */
1122
- authCapture: { browserEnv: Record<string, string>; file: string } | null;
977
+ checkAgySetup: (ctx: { ui: ExtensionUIContext; mode: string }, engine: Engine, explicit?: boolean) => Promise<void>;
1123
978
  }
1124
979
 
1125
980
  interface PendingConfig {
@@ -1166,13 +1021,13 @@ function statusText(ctx: AgyCommandCtx): string {
1166
1021
  const source = ctx.usingFallback ? "fallback (agy models failed)" : "discovered";
1167
1022
  return [
1168
1023
  "Antigravity bridge",
1169
- ` 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)"}`,
1170
1025
  ` models: ${ctx.entries.length} ${source}`,
1171
1026
  ...settingsRows(ctx),
1172
1027
  ` sessions: ${ctx.store.size} bound`,
1173
1028
  ` config: ${CONFIG_PATH}`,
1174
1029
  "",
1175
- "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",
1176
1031
  ].join("\n");
1177
1032
  }
1178
1033
 
@@ -1180,7 +1035,7 @@ function statusText(ctx: AgyCommandCtx): string {
1180
1035
  function registerAgyCommand(pi: ExtensionAPI, ctx: AgyCommandCtx): void {
1181
1036
  pi.registerCommand("agy", {
1182
1037
  description:
1183
- "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]",
1184
1039
  handler: async (args, cmdCtx: ExtensionCommandContext) => {
1185
1040
  const ui = cmdCtx.ui;
1186
1041
  if (ui) activeUi = ui;
@@ -1245,7 +1100,7 @@ function registerAgyCommand(pi: ExtensionAPI, ctx: AgyCommandCtx): void {
1245
1100
  if (sub === "engine") {
1246
1101
  if (val === "acp" || val === "stream-json") {
1247
1102
  if (val === "acp" && loadConfig().mode === "plan") {
1248
- 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");
1249
1104
  return;
1250
1105
  }
1251
1106
  const next = saveConfig({ engine: val });
@@ -1253,39 +1108,12 @@ function registerAgyCommand(pi: ExtensionAPI, ctx: AgyCommandCtx): void {
1253
1108
  ui?.notify("engine set to stream-json. Takes effect on the next pi start (or /reload).", "info");
1254
1109
  return;
1255
1110
  }
1256
- // Self-service setup: install the server from the official
1257
- // registry and bootstrap auth now, so the restart just works.
1258
- // Manual instructions only when a step fails.
1259
- ui?.notify("engine set to acp. Preparing the server (binary + auth)…", "info");
1260
- const status = await ensureAcpReady({
1261
- configBin: loadConfig().acp.bin,
1262
- onProgress: (m) => ui?.notify(m, "info"),
1263
- });
1264
- ctx.fileLog.log(
1265
- "acp-setup",
1266
- status.ok
1267
- ? { ok: true, binarySource: status.binarySource, needsLogin: status.needsLogin }
1268
- : { ok: false, error: status.error },
1269
- status.ok ? "info" : "warn",
1270
- );
1271
- if (!status.ok) {
1272
- ui?.notify(`ACP auto-setup failed (${status.error}).\n${status.manual}`, "warning");
1273
- return;
1274
- }
1275
- saveConfig({ acp: { ...loadConfig().acp, bin: status.bin } });
1276
- if (status.needsLogin) {
1277
- ui?.notify(
1278
- `ACP engine set. ${acpLoginPending()}`,
1279
- "warning",
1280
- );
1281
- } else {
1282
- ui?.notify(`ACP engine ready (auth: ${status.auth}). Takes effect on the next pi start (or /reload).`, "info");
1283
- }
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);
1284
1113
  } else if (!val && mode === "tui" && ui) {
1285
1114
  // Same modal as the first-run wizard: switching engines deserves
1286
1115
  // the explanations, not a bare usage line. Semantics match the
1287
- // direct path above: plan blocks acp, an acp pick chains setup
1288
- // + sign-in immediately, restart applies the switch.
1116
+ // direct path above: plan blocks acp and restart applies the switch.
1289
1117
  await runEngineModal(ui, ctx);
1290
1118
  } else {
1291
1119
  ui?.notify(`current engine: ${loadConfig().engine}\nusage: /agy engine stream-json|acp`, "info");
@@ -1293,99 +1121,20 @@ function registerAgyCommand(pi: ExtensionAPI, ctx: AgyCommandCtx): void {
1293
1121
  return;
1294
1122
  }
1295
1123
  if (sub === "auth") {
1296
- // Gate on the CONFIGURED engine (disk + env), not the latched one:
1297
- // /agy engine acp followed by /agy auth in the same session works,
1298
- // no restart needed before signing in.
1299
- if (loadConfig().engine !== "acp") {
1300
- ui?.notify(`the selected engine is ${loadConfig().engine}. /agy engine acp first, then /agy auth.`, "warning");
1301
- return;
1302
- }
1303
- ui?.notify("Preparing the ACP server (binary + auth settings)…", "info");
1304
- const status = await ensureAcpReady({ configBin: loadConfig().acp.bin, onProgress: (m) => ui?.notify(m, "info") });
1305
- ctx.fileLog.log(
1306
- "acp-setup",
1307
- status.ok
1308
- ? { ok: true, binarySource: status.binarySource, needsLogin: status.needsLogin }
1309
- : { ok: false, error: status.error },
1310
- status.ok ? "info" : "warn",
1311
- );
1312
- if (!status.ok) {
1313
- ui?.notify(`ACP auto-setup failed (${status.error}).\n${status.manual}`, "warning");
1314
- return;
1315
- }
1316
- saveConfig({ acp: { ...loadConfig().acp, bin: status.bin } });
1317
- if (!status.needsLogin) {
1318
- ui?.notify(`Already signed in (auth: ${status.auth}). Nothing to do.`, "info");
1319
- return;
1320
- }
1321
- ui?.notify(
1322
- "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.",
1323
- "info",
1324
- );
1325
- const r = await runAcpAuth({
1326
- bin: status.bin,
1327
- binArgs: status.args,
1328
- extraEnv: ctx.authCapture?.browserEnv,
1329
- authUrlFile: ctx.authCapture?.file,
1330
- log: ctx.acpLog,
1331
- });
1332
- ctx.fileLog.log("acp-auth", r.ok ? { ok: true } : { ok: false, error: r.error }, r.ok ? "info" : "warn");
1333
- if (r.ok) {
1334
- ui?.notify(
1335
- 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).",
1336
- "info",
1337
- );
1338
- } else {
1339
- ui?.notify(`ACP sign-in failed (${r.error}).\nRun /agy auth to retry; /agy auth-manual has manual steps.`, "warning");
1340
- }
1341
- return;
1342
- }
1343
- if (sub === "acp-bin") {
1344
- const rest = (args ?? "").trim().split(/\s+/).slice(1).join(" ");
1345
- if (rest.length > 0) {
1346
- // Only the keyword compares case-insensitively; the path keeps its case.
1347
- const bin = rest.toLowerCase() === "auto" ? "" : rest.replace(/^~(?=\/|$)/, os.homedir());
1348
- // Spread, not a bare acp patch: a bare {bin} object
1349
- // would drop sibling keys (usageEstimate) from the file.
1350
- saveConfig({ acp: { ...loadConfig().acp, bin } });
1351
- ui?.notify(
1352
- bin
1353
- ? `acp.bin set to ${bin}. The next ACP turn (re)connects with it.`
1354
- : "acp.bin cleared. Auto-setup (or AGY_ACP_BIN) picks the binary on the next ACP turn.",
1355
- "info",
1356
- );
1357
- } else {
1358
- const cur = loadConfig().acp.bin;
1359
- ui?.notify(`acp.bin: ${cur || "(auto: setup installs, or AGY_ACP_BIN)"}\nusage: /agy acp-bin <path|auto>`, "info");
1360
- }
1124
+ ui?.notify(agyAuthenticationMessage(), "info");
1361
1125
  return;
1362
1126
  }
1363
1127
  if (sub === "auth-manual") {
1364
1128
  ui?.notify(
1365
1129
  [
1366
- "ACP engine authentication (one-time; usually automatic -",
1367
- "/agy engine acp and session start set this up for you):",
1130
+ "Both engines use the agy CLI and its cached Google credentials.",
1368
1131
  "",
1369
- "The server is Google's official Antigravity ACP, installed from Google's",
1370
- "own registry. Logging in uses your Antigravity subscription: the same",
1371
- "Google account and plan as the Antigravity CLI (agy). It is no different",
1372
- "from logging into the CLI; the server just keeps its own token file on",
1373
- "your machine, like any Google tool. This extension never sees your",
1374
- "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",
1375
1136
  "",
1376
- "1. Server binary: auto-setup installs it. Manual: agy_acp_server.par from",
1377
- " the antigravity-acp registry; point acp.bin or AGY_ACP_BIN at it.",
1378
- '2. Default: put {"auth":{"type":"oauth-personal"}} in',
1379
- " ~/.gemini/antigravity-acp/settings.json, run /agy auth, and complete",
1380
- " the Google login that opens in your browser. No browser (SSH",
1381
- " session)? pi shows the sign-in URL to copy; forward the redirect",
1382
- " port over ssh (ssh -N -L <port>:127.0.0.1:<port> <user@host>), then",
1383
- " open the URL on your machine.",
1384
- ' Headless alternative: GEMINI_API_KEY + {"auth":{"type":"gemini-api-key"}}',
1385
- " (metered paid API - not your Antigravity plan). The key is used only",
1386
- " when that type is selected; with the default oauth-personal in place,",
1387
- " an exported key is ignored.",
1388
- "3. /agy doctor shows the server version when auth is OK.",
1137
+ agyAuthenticationMessage(),
1389
1138
  ].join("\n"),
1390
1139
  "info",
1391
1140
  );
@@ -1396,15 +1145,11 @@ function registerAgyCommand(pi: ExtensionAPI, ctx: AgyCommandCtx): void {
1396
1145
  const engine = ctx.engine;
1397
1146
  const snap = (engine === "acp" ? ctx.acpDriver : ctx.driver).snapshot();
1398
1147
  const port = ctx.getMcpPort();
1399
- // The CLI matters on both engines (AskAntigravity shells out to
1400
- // `agy -p` even when turns ride ACP), but on an ACP session a
1401
- // missing CLI is a supported shape, so label it optional there
1402
- // instead of showing a bare failure line.
1403
- const cliCheck = await checkAgyCliVersion(resolveAgyBinary());
1404
- const cliSuffix =
1405
- cliCheck.status === "unavailable" && engine === "acp"
1406
- ? " (optional here; AskAntigravity delegations use the CLI)"
1407
- : "";
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;
1408
1153
  const lines = [
1409
1154
  "Antigravity doctor (no tokens spent)",
1410
1155
  ` engine: ${engine}`,
@@ -1413,7 +1158,7 @@ function registerAgyCommand(pi: ExtensionAPI, ctx: AgyCommandCtx): void {
1413
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})` : ""}`,
1414
1159
  ` sessions: ${ctx.store.size} bound`,
1415
1160
  ` models: ${ctx.entries.length} ${ctx.usingFallback ? "FALLBACK (agy models failed)" : "discovered"}`,
1416
- ` agy cli: ${describeAgyVersionCheck(cliCheck)} (min ${MIN_AGY_VERSION})${cliSuffix}`,
1161
+ ` agy cli: ${describeAgyVersionCheck(cliCheck)} (min ${cliFloor})`,
1417
1162
  ` config: ${CONFIG_PATH}`,
1418
1163
  ` logs: ${logsDir()} (attach recent days' files when reporting issues; set AGY_DEBUG=1 to capture details)`,
1419
1164
  ];
@@ -1421,12 +1166,12 @@ function registerAgyCommand(pi: ExtensionAPI, ctx: AgyCommandCtx): void {
1421
1166
  lines.push(
1422
1167
  ` acp session: ${snap.acp.sessionId ?? "(none)"}`,
1423
1168
  ` acp server: ${snap.acp.serverVersion ?? "unknown"}${snap.acp.agentTitle ? ` (${snap.acp.agentTitle})` : ""}`,
1424
- ` 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}`,
1425
1170
  );
1426
1171
  // Gate B watch: silent while the server offers no token counts; one
1427
1172
  // line the day it starts (then real usage mapping is worth wiring).
1428
1173
  if (snap.acp.usageSeen) {
1429
- lines.push(" acp tokens: AVAILABLE in server payloads (wire real usage mapping next)");
1174
+ lines.push(" acp tokens: NATIVE exact usage in PromptResponse");
1430
1175
  } else if (config.acp.usageEstimate !== "off") {
1431
1176
  lines.push(` acp tokens: ESTIMATED client-side (mode: ${config.acp.usageEstimate}; auto-off once the server sends real usage)`);
1432
1177
  }
@@ -1435,13 +1180,9 @@ function registerAgyCommand(pi: ExtensionAPI, ctx: AgyCommandCtx): void {
1435
1180
  lines.push(" lifecycle (last 5):");
1436
1181
  for (const entry of snap.lifecycle.slice(-5)) lines.push(` ${entry}`);
1437
1182
  }
1438
- if (engine === "acp") {
1439
- const setup = inspectAcpSetup({ configBin: config.acp.bin });
1440
- lines.push(
1441
- ` acp binary: ${setup.bin ?? "not found (auto-setup offers install)"}${setup.source ? ` (${setup.source})` : ""}`,
1442
- ` acp auth: ${setup.auth ?? "not configured (auto-setup bootstraps)"}`,
1443
- );
1444
- }
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`);
1445
1186
  // The config surface lives here too, so doctor is the ONE status
1446
1187
  // surface (former /agy status is gone).
1447
1188
  lines.push("", "settings:", ...settingsRows(ctx));
@@ -1451,7 +1192,7 @@ function registerAgyCommand(pi: ExtensionAPI, ctx: AgyCommandCtx): void {
1451
1192
  if (sub === "mode") {
1452
1193
  if (val === "plan" || val === "accept-edits") {
1453
1194
  if (val === "plan" && ctx.engine === "acp") {
1454
- 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");
1455
1196
  return;
1456
1197
  }
1457
1198
  const next = saveConfig({ mode: val as AgyMode });
@@ -1678,10 +1419,10 @@ function registerAgyCommand(pi: ExtensionAPI, ctx: AgyCommandCtx): void {
1678
1419
  return;
1679
1420
  }
1680
1421
  if (sub === "agent") {
1681
- // 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
1682
1423
  // silently ignoring a configured agent.
1683
1424
  if (ctx.engine === "acp") {
1684
- 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");
1685
1426
  return;
1686
1427
  }
1687
1428
  const name = (args ?? "").trim().split(/\s+/).slice(1).join(" ").trim();
@@ -2006,11 +1747,11 @@ async function openAgyPicker(ui: ExtensionUIContext, ctx: AgyCommandCtx): Promis
2006
1747
  // mode row can still produce plan while the engine is acp - either the
2007
1748
  // session-latched engine or the on-disk one, which the Switch engine row
2008
1749
  // can change mid-session without a restart. Plan cannot be enforced on
2009
- // provider turns on either engine (#1181 + RC01); refuse the combination.
1750
+ // provider turns on either engine; refuse the combination.
2010
1751
  const nextMode = pending.mode ?? config.mode;
2011
1752
  if (nextMode === "plan" && (ctx.engine === "acp" || config.engine === "acp")) {
2012
1753
  ui.notify(
2013
- "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.",
2014
1755
  "warning",
2015
1756
  );
2016
1757
  return;
@@ -2048,8 +1789,7 @@ async function openAgyPicker(ui: ExtensionUIContext, ctx: AgyCommandCtx): Promis
2048
1789
 
2049
1790
  /** The engine-pick modal shared by /agy engine (no args) and the settings
2050
1791
  * picker's engine row: the same explanations as the first-run wizard and the
2051
- * same semantics — plan blocks acp, an acp pick chains setup + sign-in
2052
- * immediately, restart applies the switch. */
1792
+ * same semantics: plan blocks acp and restart applies the switch. */
2053
1793
  async function runEngineModal(ui: ExtensionUIContext, ctx: AgyCommandCtx): Promise<void> {
2054
1794
  const current = loadConfig().engine;
2055
1795
  const picked = await showEnginePicker(ui);
@@ -2062,10 +1802,10 @@ async function runEngineModal(ui: ExtensionUIContext, ctx: AgyCommandCtx): Promi
2062
1802
  return;
2063
1803
  }
2064
1804
  if (picked === "acp" && loadConfig().mode === "plan") {
2065
- 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");
2066
1806
  return;
2067
1807
  }
2068
1808
  saveConfig({ engine: picked });
2069
1809
  ui.notify(savedEngineMessage(picked), "info");
2070
- if (picked === "acp") void ctx.runAcpPickSetup({ ui });
1810
+ if (picked === "acp") void ctx.checkAgySetup({ ui, mode: "tui" }, "acp", true);
2071
1811
  }