@estebanforge/pi-antigravity-bridge 1.9.3 → 1.10.1

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, loadConfig, logsDir, MAX_TURN_CAP_MIN, parseCapMinutes, removeLegacyAcpBin, 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,16 @@ 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 legacyConfigCleanupRan = false;
209
+ let legacyConfigCleanupFailed = false;
210
+ let legacyEnvAcpWarningShown = false;
211
+ let legacyFileAcpWarningShown = false;
212
+ let agySetupOffer: (() => Promise<void>) | null = null;
219
213
  let agyMissingWarned = false;
220
214
  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
215
+ // Two turn engines behind one contract: `agy acp` (default for new
216
+ // installs) and the stream-json `agy` CLI
234
217
  // (fallback). Neither spawns anything until its first turn.
235
218
  // ACP log routing: only genuine failures reach stderr. Routine lifecycle
236
219
  // (driver-created, spawn, session-new, ...) stays in the driver's
@@ -260,63 +243,17 @@ export default async function (pi: ExtensionAPI): Promise<void> {
260
243
  const level = failed ? "warn" : msg === "turn-start" ? "info" : "debug";
261
244
  fileLog.log(msg, data, level);
262
245
  };
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
246
  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
247
  const expectedExit =
279
248
  msg === "connection-exited" &&
280
249
  (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.
250
+ const level = expectedExit ? "debug" : acpFailures.has(msg) ? "warn" : msg === "turn-start" ? "info" : "debug";
251
+ fileLog.log(msg, data, level);
311
252
  };
312
253
  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 }),
254
+ bin: binary,
255
+ binArgs: ["acp"],
256
+ isolateHome: true,
320
257
  // Resolved per turn: /agy and AGY_USAGE_ESTIMATE changes apply without
321
258
  // a restart. Without this the driver defaults to "estimate" and the
322
259
  // config knob (incl. "off") is dead.
@@ -338,7 +275,6 @@ export default async function (pi: ExtensionAPI): Promise<void> {
338
275
  return idx >= 0 ? (req.options[idx]?.optionId ?? undefined) : undefined;
339
276
  });
340
277
  },
341
- ...(authCapture ? { extraEnv: authCapture.browserEnv, authUrlFile: authCapture.file } : {}),
342
278
  log: acpLog,
343
279
  mcpServers: () => {
344
280
  const handle = mcpHandle;
@@ -445,7 +381,7 @@ export default async function (pi: ExtensionAPI): Promise<void> {
445
381
  return;
446
382
  }
447
383
  closePollRun();
448
- if (event.mcpServer === "pi-bridge") return;
384
+ if (/^pi-bridge(?:-\d+)?$/.test(event.mcpServer ?? "")) return;
449
385
  // MCP calls through our own pi-bridge already have genuine Pi tool cards;
450
386
  // only native Antigravity / other MCP events need display-only entries.
451
387
  if (event.status === "started") {
@@ -567,11 +503,9 @@ export default async function (pi: ExtensionAPI): Promise<void> {
567
503
  getMcpPort: () => mcpHandle?.port ?? null,
568
504
  bridgeTools: getBridgeTools,
569
505
  hiddenBridgeTools,
570
- acpLog,
571
506
  fileLog,
572
507
  roster,
573
- authCapture: authCapture ?? null,
574
- runAcpPickSetup,
508
+ checkAgySetup,
575
509
  });
576
510
 
577
511
  // AskAntigravity tool: one-shot delegation to agy (ported from
@@ -607,74 +541,48 @@ export default async function (pi: ExtensionAPI): Promise<void> {
607
541
  },
608
542
  });
609
543
 
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…");
544
+ function offerSetup(ui: ExtensionUIContext, explicit = false): void {
545
+ if (agySetupOffer && explicit) {
546
+ ui.notify(`CLI setup was already offered in this session. ${agyMissingMessage()}`, "info");
547
+ }
548
+ agySetupOffer ??= createAgySetupOffer({
549
+ ui,
550
+ binaryOverride: process.env.AGY_BIN || undefined,
551
+ missingMessage: agyMissingMessage,
552
+ log: (event, data, level) => fileLog.log(event, data, level),
553
+ });
554
+ void agySetupOffer();
555
+ }
556
+
557
+ // Availability checks never block bridge startup. The setup offer owns
558
+ // prompting, deduplication, and failure containment.
559
+ async function checkAgySetup(ctx: { ui: ExtensionUIContext; mode: string }, selectedEngine: Engine, explicit = false): Promise<void> {
622
560
  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");
561
+ if (!isAgyInstalled(binary)) {
562
+ if (ctx.mode === "tui") {
563
+ offerSetup(ctx.ui, explicit);
564
+ } else if (!agyMissingWarned) {
565
+ agyMissingWarned = true;
566
+ console.error(`[antigravity-bridge] ${agyMissingMessage()}`);
567
+ }
648
568
  return;
649
569
  }
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");
570
+ if (agyVersionWarned) {
571
+ if (explicit && ctx.mode === "tui") offerSetup(ctx.ui, true);
655
572
  return;
656
573
  }
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");
574
+ const rawCheck = await checkAgyCliVersion(binary);
575
+ const floor = selectedEngine === "acp" ? MIN_AGY_ACP_VERSION : MIN_AGY_VERSION;
576
+ const check = agyVersionVerdict(rawCheck.raw, floor);
577
+ if (check.status !== "unsupported") return;
578
+ agyVersionWarned = true;
579
+ const msg = `agy ${check.version} is too old for ${selectedEngine}; install ${floor} or newer.`;
580
+ if (ctx.mode === "tui") {
581
+ ctx.ui.notify(msg, "warning");
582
+ offerSetup(ctx.ui, explicit);
583
+ } else console.error(`[antigravity-bridge] ${msg}`);
674
584
  } 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");
585
+ fileLog.log("agy-version-check", { error: String(err) }, "warn");
678
586
  }
679
587
  }
680
588
 
@@ -693,6 +601,7 @@ export default async function (pi: ExtensionAPI): Promise<void> {
693
601
  // (drivers wire at load). The await intentionally runs before the
694
602
  // bridge startup below: on a genuine first run the modal blocks input
695
603
  // anyway, so the delay is invisible.
604
+ let setupEngine = engine;
696
605
  if (event.reason === "startup" && ctx.mode === "tui" && shouldOfferEnginePicker(CONFIG_PATH)) {
697
606
  // Best-effort, like the legacy-patch notice below: a picker failure
698
607
  // (mid-prompt TUI teardown, resize races) must never take down the
@@ -701,40 +610,50 @@ export default async function (pi: ExtensionAPI): Promise<void> {
701
610
  try {
702
611
  const picked = await showEnginePicker(ctx.ui);
703
612
  if (picked) {
613
+ setupEngine = picked;
704
614
  saveConfig({ engine: picked });
705
615
  ctx.ui.notify(savedEngineMessage(picked), "info");
706
- if (picked === "acp") void runAcpPickSetup(ctx);
707
616
  }
708
617
  } catch (err) {
709
618
  fileLog.log("engine-picker", { error: String(err) }, "warn");
710
619
  console.error(`[antigravity-bridge] engine picker failed: ${String(err)}`);
711
620
  }
712
621
  }
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();
622
+ // The retired acp.bin key heals itself in the config file; the env var
623
+ // lives outside our reach, so it is the only case that still warns.
624
+ // Independent once-flags: a failed cleanup must never suppress the
625
+ // env-var warning (or vice versa) for the rest of the process.
626
+ if (!legacyConfigCleanupRan) {
627
+ legacyConfigCleanupRan = true;
628
+ try {
629
+ removeLegacyAcpBin();
630
+ } catch (err) {
631
+ legacyConfigCleanupFailed = true;
632
+ fileLog.log("legacy-acp-config-cleanup", { error: String(err) }, "error");
633
+ }
634
+ }
635
+ if (!legacyEnvAcpWarningShown && process.env.AGY_ACP_BIN?.trim()) {
636
+ legacyEnvAcpWarningShown = true;
637
+ const msg = "AGY_ACP_BIN is set but no longer used; turns run \"agy acp\" from the CLI. Remove it from your shell config, then start a new terminal.";
721
638
  if (ctx.hasUI) ctx.ui.notify(msg, "warning");
722
639
  else console.error(`[antigravity-bridge] ${msg}`);
723
640
  }
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
- });
641
+ if (!legacyFileAcpWarningShown && legacyConfigCleanupFailed) {
642
+ legacyFileAcpWarningShown = true;
643
+ const msg = `Couldn't remove the old "bin" setting from ${CONFIG_PATH} (details in the bridge log). Delete the "bin" line under "acp" by hand; turns run "agy acp" from the CLI.`;
644
+ if (ctx.hasUI) ctx.ui.notify(msg, "warning");
645
+ else console.error(`[antigravity-bridge] ${msg}`);
646
+ }
647
+ if (!managedAcpCleanupRan) {
648
+ managedAcpCleanupRan = true;
649
+ try {
650
+ const cleanup = cleanupManagedAcpInstall();
651
+ if (cleanup.removedBuilds.length > 0) fileLog.log("legacy-acp-cleanup", cleanup, "info");
652
+ } catch (err) {
653
+ fileLog.log("legacy-acp-cleanup", { error: String(err) }, "warn");
654
+ }
737
655
  }
656
+ void checkAgySetup(ctx, setupEngine);
738
657
  // Legacy cleanup: users who ran the old consent-gated patcher still
739
658
  // carry pi.invokeTool in their installed pi. Inert, but tell them once
740
659
  // and offer /agy patch-cleanup. Never auto-edits the install.
@@ -752,36 +671,6 @@ export default async function (pi: ExtensionAPI): Promise<void> {
752
671
  } catch {
753
672
  /* detection is best-effort */
754
673
  }
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
674
  // Bridge failure logger. Routine lifecycle (listening,
786
675
  // bridge-config-written/removed, closed) is normal startup/teardown
787
676
  // traffic: toasting it every session, or pinning it via stderr in
@@ -825,6 +714,10 @@ export default async function (pi: ExtensionAPI): Promise<void> {
825
714
  if (ctx.hasUI) ctx.ui.notify(msg, "warning");
826
715
  else console.error(msg);
827
716
  };
717
+ // Sweep crashed sessions even when the bridge is disabled.
718
+ sweepStaleBridgeServers();
719
+ sweepStaleBridgeDirs();
720
+ sweepStaleAcpHomes();
828
721
  // Start the bridge unless the user turned it off. No patch gate, no
829
722
  // consent flow: calls route through pi's normal toolUse loop.
830
723
  const bridgeMode: BridgeTools = loadConfig().bridgeTools;
@@ -927,14 +820,13 @@ export default async function (pi: ExtensionAPI): Promise<void> {
927
820
  );
928
821
  if (r.ok && r.handle) {
929
822
  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
823
+ // Entries a crashed delegation left suppressed are healed here,
824
+ // but only when no live delegation is in
932
825
  // flight anywhere (marker-aware heal). A blind re-enable used to
933
826
  // un-hide the bridge during another session's active delegation.
934
827
  // Global-config hygiene runs on BOTH engines: a crashed stream-json
935
828
  // session must not leave dead-pid entries or stuck suppression flags
936
829
  // for the next one.
937
- sweepStaleBridgeServers();
938
830
  healBridgeSuppression();
939
831
  // ACP supplies the bridge per-session (session/new | session/load);
940
832
  // only the stream-json CLI reads the global MCP config.
@@ -1058,8 +950,6 @@ export default async function (pi: ExtensionAPI): Promise<void> {
1058
950
  activeUi = null;
1059
951
  const h = mcpHandle;
1060
952
  mcpHandle = null;
1061
- await h?.close();
1062
- roundTrips.failAll(FAIL_REASON_SHUTDOWN);
1063
953
  // "recycle", NOT "shutdown": pi fires session_shutdown on /new, /resume
1064
954
  // and /fork (docs/extensions.md session lifecycle), not only on process
1065
955
  // exit. The drivers are process-lifetime singletons; closing them with
@@ -1070,6 +960,8 @@ export default async function (pi: ExtensionAPI): Promise<void> {
1070
960
  // ("quit") - the connection kill is identical and nothing runs after.
1071
961
  await streamDriver.close("recycle", "session shutdown");
1072
962
  await acpDriver.close("recycle", "session shutdown");
963
+ roundTrips.failAll(FAIL_REASON_SHUTDOWN);
964
+ await h?.close();
1073
965
  if (engine === "stream-json") unregisterBridgeServer(process.pid);
1074
966
  hiddenBridgeTools.clear();
1075
967
  // Approval gate: unstage hooks and remove the per-pid script. Pending
@@ -1089,14 +981,6 @@ export default async function (pi: ExtensionAPI): Promise<void> {
1089
981
 
1090
982
  // --- /agy command -----------------------------------------------------------
1091
983
 
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
984
  interface AgyCommandCtx {
1101
985
  entries: AgyModelEntry[];
1102
986
  store: SessionStore;
@@ -1108,18 +992,11 @@ interface AgyCommandCtx {
1108
992
  getMcpPort: () => number | null;
1109
993
  bridgeTools: () => Array<{ name: string }>;
1110
994
  hiddenBridgeTools: Set<string>;
1111
- /** Shared ACP log sink (login URL surfacing + failure events). */
1112
- acpLog: (msg: string, data?: unknown) => void;
1113
995
  /** Daily file logger (src/daily-log.ts); command + doctor surfacing. */
1114
996
  fileLog: DailyLogger;
1115
997
  /** Live subagent roster (folded in the provider); /agy subagents. */
1116
998
  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;
999
+ checkAgySetup: (ctx: { ui: ExtensionUIContext; mode: string }, engine: Engine, explicit?: boolean) => Promise<void>;
1123
1000
  }
1124
1001
 
1125
1002
  interface PendingConfig {
@@ -1166,13 +1043,13 @@ function statusText(ctx: AgyCommandCtx): string {
1166
1043
  const source = ctx.usingFallback ? "fallback (agy models failed)" : "discovered";
1167
1044
  return [
1168
1045
  "Antigravity bridge",
1169
- ` engine: ${config.engine}${config.engine === "stream-json" ? " (agy CLI fallback)" : " (official server)"}`,
1046
+ ` engine: ${config.engine}${config.engine === "stream-json" ? " (agy CLI fallback)" : " (agy acp)"}`,
1170
1047
  ` models: ${ctx.entries.length} ${source}`,
1171
1048
  ...settingsRows(ctx),
1172
1049
  ` sessions: ${ctx.store.size} bound`,
1173
1050
  ` config: ${CONFIG_PATH}`,
1174
1051
  "",
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",
1052
+ "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
1053
  ].join("\n");
1177
1054
  }
1178
1055
 
@@ -1180,7 +1057,7 @@ function statusText(ctx: AgyCommandCtx): string {
1180
1057
  function registerAgyCommand(pi: ExtensionAPI, ctx: AgyCommandCtx): void {
1181
1058
  pi.registerCommand("agy", {
1182
1059
  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]",
1060
+ "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
1061
  handler: async (args, cmdCtx: ExtensionCommandContext) => {
1185
1062
  const ui = cmdCtx.ui;
1186
1063
  if (ui) activeUi = ui;
@@ -1245,7 +1122,7 @@ function registerAgyCommand(pi: ExtensionAPI, ctx: AgyCommandCtx): void {
1245
1122
  if (sub === "engine") {
1246
1123
  if (val === "acp" || val === "stream-json") {
1247
1124
  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");
1125
+ ui?.notify("mode is plan; provider plan turns are disabled on ACP. /agy mode accept-edits first.", "warning");
1249
1126
  return;
1250
1127
  }
1251
1128
  const next = saveConfig({ engine: val });
@@ -1253,39 +1130,12 @@ function registerAgyCommand(pi: ExtensionAPI, ctx: AgyCommandCtx): void {
1253
1130
  ui?.notify("engine set to stream-json. Takes effect on the next pi start (or /reload).", "info");
1254
1131
  return;
1255
1132
  }
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
- }
1133
+ 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");
1134
+ void ctx.checkAgySetup(cmdCtx, "acp", true);
1284
1135
  } else if (!val && mode === "tui" && ui) {
1285
1136
  // Same modal as the first-run wizard: switching engines deserves
1286
1137
  // 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.
1138
+ // direct path above: plan blocks acp and restart applies the switch.
1289
1139
  await runEngineModal(ui, ctx);
1290
1140
  } else {
1291
1141
  ui?.notify(`current engine: ${loadConfig().engine}\nusage: /agy engine stream-json|acp`, "info");
@@ -1293,99 +1143,20 @@ function registerAgyCommand(pi: ExtensionAPI, ctx: AgyCommandCtx): void {
1293
1143
  return;
1294
1144
  }
1295
1145
  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
- }
1146
+ ui?.notify(agyAuthenticationMessage(), "info");
1361
1147
  return;
1362
1148
  }
1363
1149
  if (sub === "auth-manual") {
1364
1150
  ui?.notify(
1365
1151
  [
1366
- "ACP engine authentication (one-time; usually automatic -",
1367
- "/agy engine acp and session start set this up for you):",
1152
+ "Both engines use the agy CLI and its cached Google credentials.",
1368
1153
  "",
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.",
1154
+ "Install on Linux or macOS:",
1155
+ " curl -fsSL https://antigravity.google/cli/install.sh | bash",
1156
+ "Install on Windows PowerShell:",
1157
+ " irm https://antigravity.google/cli/install.ps1 | iex",
1375
1158
  "",
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.",
1159
+ agyAuthenticationMessage(),
1389
1160
  ].join("\n"),
1390
1161
  "info",
1391
1162
  );
@@ -1396,15 +1167,11 @@ function registerAgyCommand(pi: ExtensionAPI, ctx: AgyCommandCtx): void {
1396
1167
  const engine = ctx.engine;
1397
1168
  const snap = (engine === "acp" ? ctx.acpDriver : ctx.driver).snapshot();
1398
1169
  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
- : "";
1170
+ const rawCliCheck = await checkAgyCliVersion(resolveAgyBinary());
1171
+ const cliFloor = engine === "acp" ? MIN_AGY_ACP_VERSION : MIN_AGY_VERSION;
1172
+ const cliCheck = rawCliCheck.status === "ok" || rawCliCheck.status === "unsupported"
1173
+ ? agyVersionVerdict(rawCliCheck.raw, cliFloor)
1174
+ : rawCliCheck;
1408
1175
  const lines = [
1409
1176
  "Antigravity doctor (no tokens spent)",
1410
1177
  ` engine: ${engine}`,
@@ -1413,7 +1180,7 @@ function registerAgyCommand(pi: ExtensionAPI, ctx: AgyCommandCtx): void {
1413
1180
  ` 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
1181
  ` sessions: ${ctx.store.size} bound`,
1415
1182
  ` models: ${ctx.entries.length} ${ctx.usingFallback ? "FALLBACK (agy models failed)" : "discovered"}`,
1416
- ` agy cli: ${describeAgyVersionCheck(cliCheck)} (min ${MIN_AGY_VERSION})${cliSuffix}`,
1183
+ ` agy cli: ${describeAgyVersionCheck(cliCheck)} (min ${cliFloor})`,
1417
1184
  ` config: ${CONFIG_PATH}`,
1418
1185
  ` logs: ${logsDir()} (attach recent days' files when reporting issues; set AGY_DEBUG=1 to capture details)`,
1419
1186
  ];
@@ -1421,12 +1188,12 @@ function registerAgyCommand(pi: ExtensionAPI, ctx: AgyCommandCtx): void {
1421
1188
  lines.push(
1422
1189
  ` acp session: ${snap.acp.sessionId ?? "(none)"}`,
1423
1190
  ` 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)"}`,
1191
+ ` acp stats: prompts=${snap.acp.prompts} created=${snap.acp.sessionsCreated} loaded=${snap.acp.sessionsLoaded} kills=${snap.acp.kills} reconnects=${snap.acp.reconnects}`,
1425
1192
  );
1426
1193
  // Gate B watch: silent while the server offers no token counts; one
1427
1194
  // line the day it starts (then real usage mapping is worth wiring).
1428
1195
  if (snap.acp.usageSeen) {
1429
- lines.push(" acp tokens: AVAILABLE in server payloads (wire real usage mapping next)");
1196
+ lines.push(" acp tokens: NATIVE exact usage in PromptResponse");
1430
1197
  } else if (config.acp.usageEstimate !== "off") {
1431
1198
  lines.push(` acp tokens: ESTIMATED client-side (mode: ${config.acp.usageEstimate}; auto-off once the server sends real usage)`);
1432
1199
  }
@@ -1435,13 +1202,9 @@ function registerAgyCommand(pi: ExtensionAPI, ctx: AgyCommandCtx): void {
1435
1202
  lines.push(" lifecycle (last 5):");
1436
1203
  for (const entry of snap.lifecycle.slice(-5)) lines.push(` ${entry}`);
1437
1204
  }
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
- }
1205
+ if (engine === "acp") lines.push(" acp transport: agy acp (shared CLI authentication, isolated MCP home)");
1206
+ const retiredAuth = path.join(os.homedir(), ".gemini", "antigravity-acp");
1207
+ if (fs.existsSync(retiredAuth)) lines.push(` retired auth: ${retiredAuth} is unused; remove it manually when no longer needed`);
1445
1208
  // The config surface lives here too, so doctor is the ONE status
1446
1209
  // surface (former /agy status is gone).
1447
1210
  lines.push("", "settings:", ...settingsRows(ctx));
@@ -1451,7 +1214,7 @@ function registerAgyCommand(pi: ExtensionAPI, ctx: AgyCommandCtx): void {
1451
1214
  if (sub === "mode") {
1452
1215
  if (val === "plan" || val === "accept-edits") {
1453
1216
  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");
1217
+ 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
1218
  return;
1456
1219
  }
1457
1220
  const next = saveConfig({ mode: val as AgyMode });
@@ -1678,10 +1441,10 @@ function registerAgyCommand(pi: ExtensionAPI, ctx: AgyCommandCtx): void {
1678
1441
  return;
1679
1442
  }
1680
1443
  if (sub === "agent") {
1681
- // ACP has no agent slot in the protocol (RC01): refuse instead of
1444
+ // ACP has no per-turn agent slot in the protocol: refuse instead of
1682
1445
  // silently ignoring a configured agent.
1683
1446
  if (ctx.engine === "acp") {
1684
- ui?.notify("the ACP engine has no agent selection (RC01). /agy engine stream-json first.", "warning");
1447
+ ui?.notify("the ACP engine has no per-turn agent selection. /agy engine stream-json first.", "warning");
1685
1448
  return;
1686
1449
  }
1687
1450
  const name = (args ?? "").trim().split(/\s+/).slice(1).join(" ").trim();
@@ -2006,11 +1769,11 @@ async function openAgyPicker(ui: ExtensionUIContext, ctx: AgyCommandCtx): Promis
2006
1769
  // mode row can still produce plan while the engine is acp - either the
2007
1770
  // session-latched engine or the on-disk one, which the Switch engine row
2008
1771
  // can change mid-session without a restart. Plan cannot be enforced on
2009
- // provider turns on either engine (#1181 + RC01); refuse the combination.
1772
+ // provider turns on either engine; refuse the combination.
2010
1773
  const nextMode = pending.mode ?? config.mode;
2011
1774
  if (nextMode === "plan" && (ctx.engine === "acp" || config.engine === "acp")) {
2012
1775
  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.",
1776
+ "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
1777
  "warning",
2015
1778
  );
2016
1779
  return;
@@ -2048,8 +1811,7 @@ async function openAgyPicker(ui: ExtensionUIContext, ctx: AgyCommandCtx): Promis
2048
1811
 
2049
1812
  /** The engine-pick modal shared by /agy engine (no args) and the settings
2050
1813
  * 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. */
1814
+ * same semantics: plan blocks acp and restart applies the switch. */
2053
1815
  async function runEngineModal(ui: ExtensionUIContext, ctx: AgyCommandCtx): Promise<void> {
2054
1816
  const current = loadConfig().engine;
2055
1817
  const picked = await showEnginePicker(ui);
@@ -2062,10 +1824,10 @@ async function runEngineModal(ui: ExtensionUIContext, ctx: AgyCommandCtx): Promi
2062
1824
  return;
2063
1825
  }
2064
1826
  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");
1827
+ ui.notify("mode is plan; provider plan turns are disabled on ACP. /agy mode accept-edits first.", "warning");
2066
1828
  return;
2067
1829
  }
2068
1830
  saveConfig({ engine: picked });
2069
1831
  ui.notify(savedEngineMessage(picked), "info");
2070
- if (picked === "acp") void ctx.runAcpPickSetup({ ui });
1832
+ if (picked === "acp") void ctx.checkAgySetup({ ui, mode: "tui" }, "acp", true);
2071
1833
  }