@bridge_gpt/mcp-server 0.2.27 → 0.2.28

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.
@@ -30,11 +30,14 @@
30
30
  *
31
31
  * then SPAWNS a fresh agent session (Step 5) for the CONFIGURE-ONLY agentic
32
32
  * remainder: `/install-bridge` config-field derivation → the read-after-write
33
- * capability report → a single optional indexing-consent question. It does NOT
34
- * chain `/learn-repository` and does NOT index without explicit consent. The
35
- * fresh session is required because a CLI cannot force the running editor to
36
- * reload the just-written `.mcp.json`, and field derivation needs an agent
37
- * runtime the shell does not have.
33
+ * concise capability report → a `/learn-repository` recommendation (BAPI-658).
34
+ * It does NOT chain into running `/learn-repository` itself that stays the
35
+ * human's next explicit invocation and it never asks about, decides, or
36
+ * claims anything about repository indexing: indexing is entirely automatic,
37
+ * gated server-side by the existing readiness funnel, with no prompt on any
38
+ * path. The fresh session is required because a CLI cannot force the running
39
+ * editor to reload the just-written `.mcp.json`, and field derivation needs an
40
+ * agent runtime the shell does not have.
38
41
  *
39
42
  * That spawn command embeds the entire agent prompt and runs to multiple KB, which
40
43
  * no macOS terminal will accept as one typed line. So it is never typed: the full
@@ -88,7 +91,7 @@ import readline from "readline";
88
91
  import { runInit, buildBridgeApiEntry } from "./init.js";
89
92
  import { VERSION } from "./version.generated.js";
90
93
  import { validateRepoName } from "./bridge-config.js";
91
- import { MCP_HOST_TARGETS, HOST_PLATFORM_ORDER, allHostTargets, isHostPlatformId, detectDefaultPlatforms, } from "./mcp-host-targets.js";
94
+ import { MCP_HOST_TARGETS, HOST_PLATFORM_ORDER, allHostTargets, agentForPlatform, isHostPlatformId, detectDefaultPlatforms, } from "./mcp-host-targets.js";
92
95
  import { provisionHostTarget, createDefaultVendorProcessDeps, } from "./mcp-host-config.js";
93
96
  import { writeMcpInstallState } from "./mcp-install-state.js";
94
97
  import { ensureGitignored as ensureGitignoredShared, } from "./git-ignore-utils.js";
@@ -98,7 +101,7 @@ import { upsertBapiCredential, getPrimaryCredentialStorePath, prepareBootstrapPe
98
101
  // API primitives verbatim — no duplicated polling, browser, or picker logic here.
99
102
  import { fetchGithubConfigurationState } from "./connect-github-api.js";
100
103
  import { createDefaultConnectGithubDeps, runGithubConnectionFlow } from "./connect-github.js";
101
- import { DEFAULT_AGENT_NAME, resolveAgentSpec, isAgentName, formatValidAgentNames, } from "./agent-registry.js";
104
+ import { resolveAgentSpec, isAgentName, formatValidAgentNames, } from "./agent-registry.js";
102
105
  import { buildGenericAgentShellCommand, getDefaultSpawnTerminalTabForPlatform, detectTerminal, createDefaultStartTicketsDeps, materializeWorkerLaunchCommand, MAX_TERMINAL_COMMAND_BYTES, } from "./start-tickets.js";
103
106
  /** Redaction sentinel — the API-key value is NEVER printed; this stands in. */
104
107
  export const REDACTED_API_KEY = "<REDACTED>";
@@ -125,44 +128,37 @@ export function buildPrewarmCommandPreview() {
125
128
  }
126
129
  /**
127
130
  * The natural-language prompt handed to the spawned agent session. It is
128
- * CONFIGURE-ONLY: it derives and applies configuration, presents the capability
129
- * report, and ends with exactly one indexing-consent question. It never chains
130
- * /learn-repository and never indexes before explicit consent.
131
- *
132
- * The index-consent beat is AGENT-OWNED (asked inside this spawned session), not
133
- * parent-CLI-owned, because `spawnTerminalTab()` opens an asynchronous terminal
134
- * session: the parent CLI returns immediately and cannot reliably ask a question
135
- * that must appear AFTER the spawned session's capability report. Asking it via
136
- * the parent's `deps.promptLine` would display the question before the report
137
- * even exists. `deps.promptLine` therefore stays the owner of the parent-CLI
138
- * prompts (repository / overwrite / invite confirmation) and is intentionally
139
- * NOT used for this post-report question.
131
+ * CONFIGURE-ONLY: it derives and applies configuration, presents the concise
132
+ * capability report, and closes with a `/learn-repository` recommendation
133
+ * (BAPI-658). It never chains straight into running /learn-repository itself —
134
+ * that remains the human's next explicit invocation — and it never asks about,
135
+ * decides, or claims anything about repository indexing: indexing is entirely
136
+ * automatic, gated by the existing readiness funnel
137
+ * (`maybe_auto_parse_if_ready`), with no prompt on any path.
140
138
  */
141
139
  export const INSTALL_BRIDGE_AGENT_PROMPT = "Execute the /install-bridge command in the install-spawn context (tell the command it is running " +
142
- "in the install-spawn context so it SKIPS its Stage 8 and Stage 9 offers — this session's only " +
143
- "closing interaction is the single indexing question below). Do NOT run /learn-repository. Do NOT " +
144
- "call parse_repository (or otherwise start indexing) before the capability report and explicit " +
145
- "consent below. " +
146
- "Complete the command's read-after-write five-section capability report first: 'Connected ', " +
147
- "'Not yet connected ✗', 'Tools you can use now', 'Tools you'll unlock', and 'Recommended next " +
148
- "step + why'. " +
149
- "Only AFTER that report is fully presented, ask exactly one question using this visible prompt: " +
150
- "'[Y/n] Index repository now?'. Only an explicit affirmative answer (e.g. 'y'/'yes') starts " +
151
- "indexing; a blank answer, a negative answer, EOF, an unavailable interaction, and any " +
152
- "non-interactive/headless run all resolve to NO. " +
153
- "On an affirmative answer: call the parse_repository MCP tool exactly once, describe the accepted " +
154
- "job as QUEUED, and direct later progress checks to get_parse_status or /check-parse-status " +
155
- "(do NOT poll it to completion). If parse_repository returns a blocking refusal or error, do NOT " +
156
- "claim the job was queued — report the sanitized result and leave indexing pending. " +
157
- "On NO (or any unavailable/non-interactive resolution): do not index; print the exact copy-paste " +
158
- "continuation command '/parse-repository' on its own line and state that indexing remains pending. " +
140
+ "in the install-spawn context so it SKIPS its Stage 8, Stage 9, and Stage 10 offers — this " +
141
+ "session's only closing interaction is the concise capability report and learn recommendation " +
142
+ "below). " +
143
+ "Complete the command's read-after-write concise capability report first: the single 'What Bridge " +
144
+ "can help with' section, rendered exactly as the server's concise_tool_capabilities field gives it " +
145
+ "(Regularly useful, then Occasionally useful, each tool's display_name only, plus each tier's " +
146
+ "server-computed '+N more' where non-zero). Do not render the obsolete five-section report, and do " +
147
+ "not locally filter, count, or fall back to the complete tool_capabilities catalog. " +
148
+ "Do NOT ask any indexing question, call parse_repository, mention /parse-repository, or claim " +
149
+ "indexing has started or is pending indexing is decided entirely by the server-side readiness " +
150
+ "funnel with no visibility from this session, so say nothing about it at all. " +
151
+ "After the capability report, recommend /learn-repository as the next step: explain briefly that it " +
152
+ "learns repository-specific architecture, review, testing, correctness, and validation-manual " +
153
+ "configuration by researching the actual codebase. Do NOT run /learn-repository yourself only " +
154
+ "recommend it; running it is the human's next explicit invocation. " +
159
155
  "Never request, echo, or transport any credential — only ever direct the human to that " +
160
156
  "integration's own configure_in pointer, verbatim. The pointer is per-integration and is NOT " +
161
157
  "always the setup UI: GitHub's is a terminal command (connect-github), while Jira, SFCC, and " +
162
158
  "Bitbucket point at the setup UI. Follow whatever the report says rather than assuming. " +
163
159
  "End with an explicit summary line stating how many config fields the apply_install_manifest call " +
164
- "applied (e.g. 'Applied 8 of 9 derived fields') and whether indexing was queued or left pending " +
165
- "if 0 fields were applied, say so loudly and explain what is still pending.";
160
+ "applied (e.g. 'Applied 8 of 9 derived fields') if 0 fields were applied, say so loudly and " +
161
+ "explain what is still pending.";
166
162
  /** Default base URL when `BAPI_BASE_URL` is unset (mirrors index.ts). */
167
163
  export const DEFAULT_BAPI_BASE_URL = "https://bridgegpt-api.com";
168
164
  /** Default docs dir when `BAPI_DOCS_DIR` is unset (mirrors index.ts). */
@@ -175,8 +171,11 @@ export function getInstallBridgeUsage() {
175
171
  "",
176
172
  "One-command Bridge API project bootstrap. Scaffolds the project, writes the",
177
173
  "per-host MCP config with your credentials, verifies connectivity, persists the",
178
- "routing credential, then opens a fresh agent session to derive the remaining",
179
- "config, present a capability report, and offer optional repository indexing.",
174
+ "routing credential, then on a TTY, only after a Y/N consent prompt — opens a",
175
+ "fresh session in your selected tool to derive the remaining config, present a",
176
+ "concise capability report, and recommend /learn-repository. Indexing is never",
177
+ "asked about — it starts automatically once the repository reaches full parse",
178
+ "readiness.",
180
179
  "",
181
180
  "Run it bare — `install-bridge` with no flags — in a terminal and it asks",
182
181
  `\`${INSTALL_BRIDGE_KEY_SELECTOR_PROMPT.trim()}\` first. Answer yes (or press Enter) for the`,
@@ -245,24 +244,35 @@ export function getInstallBridgeUsage() {
245
244
  ` ${HOST_PLATFORM_ORDER.join(", ")}.`,
246
245
  " Both --tools=claude-code,codex and",
247
246
  " --tools claude-code,codex are accepted. On an",
248
- " interactive terminal WITHOUT this flag you are",
249
- " asked which tools you use (Claude Code plus any",
250
- " detected editors are pre-checked). A non-",
251
- " interactive run without --tools writes the legacy",
252
- " automatic set (Claude Code plus any detected",
253
- " Cursor / Copilot VS Code). --tools= (empty) is an",
254
- " explicit empty selection and writes nothing.",
247
+ " interactive terminal WITHOUT this flag the",
248
+ " checklist starts EMPTY no tool is pre-selected",
249
+ " (not even Claude Code) and you must select at",
250
+ " least one. A non-interactive (non-TTY) run without",
251
+ " --tools writes the legacy automatic set (Claude",
252
+ " Code plus any detected Cursor / Copilot VS Code)",
253
+ " and never opens an agent terminal (it prints",
254
+ " manual continuation instead). --tools= (empty) is",
255
+ " an explicit empty selection: it writes nothing and",
256
+ " launches nothing.",
255
257
  " --force Overwrite an existing real BAPI_API_KEY in a",
256
258
  " host config (or in the credential store) without",
257
259
  " prompting.",
258
260
  " --dry-run Preview every step (scaffold targets, config",
259
261
  " files + keys with the key REDACTED, ping",
260
- " target, credential target, spawn command)",
261
- " without writing, pinging, or spawning anything.",
262
- " With --invite it also never calls the exchange",
263
- " endpoint and never generates or stores a secret.",
264
- " --agent claude|cursor-agent Agent to launch for the agentic remainder",
265
- " (default: claude).",
262
+ " target, credential target, and the consent-gated",
263
+ " launch outcome) without writing, pinging,",
264
+ " prompting, or spawning anything. With --invite it",
265
+ " also never calls the exchange endpoint and never",
266
+ " generates or stores a secret.",
267
+ " --agent claude|cursor-agent Explicit launch override for the post-install",
268
+ " session — it always wins, and there is NO Claude",
269
+ " default. Without it the launch tool is derived",
270
+ " from your selection: Claude Code opens `claude`,",
271
+ " Cursor opens `cursor-agent`; if several launchable",
272
+ " tools are selected the wizard asks which single one",
273
+ " to open; and a selection whose tools have no",
274
+ " agentic CLI (e.g. Copilot) opens nothing and prints",
275
+ " how to finish configuring later.",
266
276
  " -h, --help Show this help.",
267
277
  "",
268
278
  "Environment: BAPI_BASE_URL (default https://bridgegpt-api.com) and BAPI_DOCS_DIR",
@@ -288,7 +298,10 @@ export function parseInstallBridgeArgs(argv) {
288
298
  let repo;
289
299
  let force = false;
290
300
  let dryRun = false;
291
- let agentName = DEFAULT_AGENT_NAME;
301
+ // Undefined until `--agent` is explicitly supplied — install-bridge never falls
302
+ // back to DEFAULT_AGENT_NAME (BAPI-657). An omitted agent is derived from the tool
303
+ // selection downstream; only an explicit `--agent` value lands here.
304
+ let agentName;
292
305
  let invite;
293
306
  let email;
294
307
  let tools;
@@ -968,22 +981,26 @@ export async function resolveRepoName(options, deps, mode = "existing-registrati
968
981
  /** Stable wording of the interactive tool-selection prompt (BAPI-635). */
969
982
  export const INSTALL_BRIDGE_TOOL_SELECTOR_PROMPT = "Which AI coding tools do you use on this project?";
970
983
  /**
971
- * Interactive numbered multi-select prompt on stderr (TTY only). Displays each
972
- * option with a checked/unchecked marker seeded from `defaults`, accepts a
973
- * comma-separated list of numbers to TOGGLE, and accepts the current selection on
974
- * a bare Enter. Reprints on an invalid token rather than corrupting state.
975
- * Resolves safely (to the seeded defaults) on EOF / synchronous close so a
976
- * readline regression can never hang or discard the answer.
984
+ * Interactive numbered multi-select prompt on stderr (TTY only). Displays a
985
+ * legend and usage line plus each option with a checked/unchecked marker seeded
986
+ * from `defaults`, accepts a comma-separated list of numbers that TOGGLE the rows
987
+ * and RE-PROMPT (acceptance is a separate bare Enter), and accepts only a NON-EMPTY
988
+ * selection on a bare Enter an empty one prints "Select at least one tool." and
989
+ * re-prompts (AC-1/AC-3). Reprints on an invalid token rather than corrupting
990
+ * state. Resolves safely (to the current set, including an empty one) on EOF /
991
+ * synchronous close so a readline regression can never hang or discard the answer.
977
992
  */
978
993
  export function promptMultiSelectViaReadline(promptText, options, defaults, input = process.stdin, output = process.stderr) {
979
994
  return new Promise((resolve) => {
980
995
  const selected = new Set(defaults);
981
996
  const render = () => {
982
997
  output.write(`\n${promptText}\n`);
998
+ output.write("[x] = selected · [ ] = not selected\n");
983
999
  options.forEach((opt, idx) => {
984
1000
  const mark = selected.has(opt.id) ? "[x]" : "[ ]";
985
1001
  output.write(` ${idx + 1}. ${mark} ${opt.label}\n`);
986
1002
  });
1003
+ output.write("Type numbers to toggle, e.g. 1,3 — then Enter. Enter with ≥1 selected accepts.\n");
987
1004
  output.write("Enter numbers to toggle (comma-separated), or press Enter to accept: ");
988
1005
  };
989
1006
  const rl = readline.createInterface({ input, output });
@@ -1004,6 +1021,14 @@ export function promptMultiSelectViaReadline(promptText, options, defaults, inpu
1004
1021
  rl.question("", (answer) => {
1005
1022
  const trimmed = answer.trim();
1006
1023
  if (trimmed.length === 0) {
1024
+ // AC-3: a bare Enter accepts ONLY a non-empty selection; an empty one
1025
+ // re-prompts rather than resolving to nothing. EOF/close (handled above)
1026
+ // still resolves the current — possibly empty — set so input can never hang.
1027
+ if (selected.size === 0) {
1028
+ output.write("Select at least one tool.\n");
1029
+ ask();
1030
+ return;
1031
+ }
1007
1032
  finish();
1008
1033
  return;
1009
1034
  }
@@ -1030,7 +1055,9 @@ export function promptMultiSelectViaReadline(promptText, options, defaults, inpu
1030
1055
  else
1031
1056
  selected.add(opt.id);
1032
1057
  }
1033
- finish();
1058
+ // Toggling re-renders the updated checklist and re-prompts; acceptance is a
1059
+ // separate, deliberate bare Enter (AC-1/AC-3) rather than an implicit accept.
1060
+ ask();
1034
1061
  });
1035
1062
  };
1036
1063
  ask();
@@ -1040,8 +1067,8 @@ export function promptMultiSelectViaReadline(promptText, options, defaults, inpu
1040
1067
  * Resolve the selected host platforms with strict precedence (BAPI-635):
1041
1068
  * 1. explicit `--tools` (including an explicit EMPTY selection — never falls
1042
1069
  * back to detection),
1043
- * 2. interactive multi-select on a TTY (seeded from registry detection, with
1044
- * Claude Code always checked as a default),
1070
+ * 2. interactive multi-select on a TTY (AC-2: no row pre-selected — the picker
1071
+ * starts empty and requires an explicit ≥1 selection),
1045
1072
  * 3. the legacy non-TTY automatic set: Claude Code plus only the currently
1046
1073
  * detected Copilot VS Code and Cursor automatic targets (never Codex or
1047
1074
  * Copilot CLI just because a global directory exists).
@@ -1053,11 +1080,12 @@ export async function resolveSelectedHostPlatforms(deps, options) {
1053
1080
  }
1054
1081
  const ctx = await buildDetectionContext(deps);
1055
1082
  const detected = new Set(detectDefaultPlatforms(ctx));
1056
- // 2. Interactive multi-select on a TTY.
1083
+ // 2. Interactive multi-select on a TTY. AC-2: no row is pre-selected — the
1084
+ // picker seeds an EMPTY default set (dropping the former forced Claude Code
1085
+ // inclusion and detected-platform seeding) and requires an explicit selection.
1057
1086
  if (deps.isTTY && deps.promptMultiSelect) {
1058
1087
  const optionList = allHostTargets().map((t) => ({ id: t.id, label: t.label }));
1059
- // Claude Code is always a checked default; add every detected platform.
1060
- const defaults = HOST_PLATFORM_ORDER.filter((id) => id === "claude-code" || detected.has(id));
1088
+ const defaults = [];
1061
1089
  const chosen = await deps.promptMultiSelect(INSTALL_BRIDGE_TOOL_SELECTOR_PROMPT, optionList, defaults);
1062
1090
  return HOST_PLATFORM_ORDER.filter((id) => chosen.includes(id));
1063
1091
  }
@@ -1069,6 +1097,146 @@ export async function resolveSelectedHostPlatforms(deps, options) {
1069
1097
  legacy.push("copilot-vscode");
1070
1098
  return HOST_PLATFORM_ORDER.filter((id) => legacy.includes(id));
1071
1099
  }
1100
+ /**
1101
+ * Resolve the post-install launch decision from the selected host platforms and an
1102
+ * OPTIONAL explicit `--agent` override (BAPI-657). Pure and total:
1103
+ *
1104
+ * - An explicit `--agent` is an ABSOLUTE override (ratified AC-4): a valid explicit
1105
+ * agent always returns `spawn(agent)`, regardless of which tools were selected —
1106
+ * the flag is honored even for a host outside the selection.
1107
+ * - Otherwise the selection drives it: map each selected host through
1108
+ * {@link agentForPlatform}, drop non-launchable hosts, dedup in registry order,
1109
+ * then return `manual("empty-selection")` for an empty selection,
1110
+ * `manual("no-launchable-agent")` for zero launchable agents, `spawn` for one, or
1111
+ * `choose-one` for more than one.
1112
+ *
1113
+ * Never prompts, logs, resolves an {@link AgentSpec}, or touches the filesystem.
1114
+ */
1115
+ export function resolveInstallBridgeLaunchDecision(selectedPlatforms, explicitAgent) {
1116
+ if (explicitAgent) {
1117
+ return { kind: "spawn", agent: explicitAgent };
1118
+ }
1119
+ if (selectedPlatforms.length === 0) {
1120
+ return { kind: "manual", reason: "empty-selection" };
1121
+ }
1122
+ const agents = [];
1123
+ for (const id of HOST_PLATFORM_ORDER) {
1124
+ if (!selectedPlatforms.includes(id))
1125
+ continue;
1126
+ const agent = agentForPlatform(id);
1127
+ if (agent && !agents.includes(agent))
1128
+ agents.push(agent);
1129
+ }
1130
+ if (agents.length === 0)
1131
+ return { kind: "manual", reason: "no-launchable-agent" };
1132
+ if (agents.length === 1)
1133
+ return { kind: "spawn", agent: agents[0] };
1134
+ return { kind: "choose-one", agents };
1135
+ }
1136
+ /**
1137
+ * The human-facing tool label for a launch agent, derived by scanning the host
1138
+ * registry's {@link HostTargetDefinition.launchAgent} metadata rather than keeping a
1139
+ * duplicate agent→label map (BAPI-657). Returns e.g. `"Claude Code"` for `"claude"`
1140
+ * and `"Cursor"` for `"cursor-agent"`, falling back to the raw agent name if no host
1141
+ * advertises it (never expected for a registered launch agent).
1142
+ */
1143
+ export function toolLabelForLaunchAgent(agent) {
1144
+ const target = allHostTargets().find((t) => t.launchAgent === agent);
1145
+ return target?.label ?? agent;
1146
+ }
1147
+ /**
1148
+ * Interactive numbered chooser for when MORE THAN ONE launchable tool was selected
1149
+ * (AC-11). Mirrors the connect-github picker: prints each candidate by its host-tool
1150
+ * label, asks for an exact numeric choice with NO default, validates the range, and
1151
+ * returns the chosen agent. Returns `null` — never a silent first-candidate default —
1152
+ * for every unavailable/invalid path: non-TTY, a missing `promptLine` seam, blank/EOF
1153
+ * input, an out-of-range or malformed answer, or a prompt that throws. Single-purpose:
1154
+ * it must run only AFTER the multi-select promise has resolved and closed its readline
1155
+ * interface so two readers never contend for stdin.
1156
+ */
1157
+ export async function chooseInstallBridgeLaunchAgent(agents, deps) {
1158
+ if (!deps.isTTY || !deps.promptLine)
1159
+ return null;
1160
+ const promptLine = deps.promptLine;
1161
+ try {
1162
+ deps.log("");
1163
+ deps.log("More than one selected tool can host the configuration session:");
1164
+ agents.forEach((agent, i) => {
1165
+ deps.log(` ${String(i + 1).padStart(2, " ")}. ${toolLabelForLaunchAgent(agent)}`);
1166
+ });
1167
+ // No default: a stray Enter must not silently pick a tool.
1168
+ const answer = (await promptLine(`Which tool should open? [1-${agents.length}]: `)).trim();
1169
+ const index = Number(answer);
1170
+ if (!/^\d+$/.test(answer) ||
1171
+ !Number.isInteger(index) ||
1172
+ index < 1 ||
1173
+ index > agents.length) {
1174
+ return null;
1175
+ }
1176
+ return agents[index - 1];
1177
+ }
1178
+ catch {
1179
+ // Never surface caught exception text; an unavailable chooser is a no-spawn.
1180
+ return null;
1181
+ }
1182
+ }
1183
+ /**
1184
+ * Shared, copy-pasteable "we didn't open a session for you" guidance so every
1185
+ * no-spawn path (no-launchable selection, non-TTY suppression, chooser failure,
1186
+ * consent decline, terminal-spawn failure) emits ONE consistent remediation
1187
+ * (AC-5 / AC-7). Two variants:
1188
+ *
1189
+ * - `"empty-selection"` — no MCP host config was written at all, so it does NOT tell
1190
+ * the user to run `/install-bridge` (there is no configured host to run it in); it
1191
+ * tells them to re-run `install-bridge` and select at least one tool.
1192
+ * - `"configured"` — at least one host WAS configured, so it tells the user to open
1193
+ * the project in an applicable AI coding tool and run `/install-bridge`, plus the
1194
+ * required notice that their Bridge MCP tools stay limited until configuration
1195
+ * completes.
1196
+ *
1197
+ * Condition-specific warning prefixes are added by the caller, never inside here.
1198
+ */
1199
+ export function buildManualInstallBridgeContinuation(kind) {
1200
+ if (kind === "empty-selection") {
1201
+ return [
1202
+ "No AI coding tools were configured, so nothing was set up for this project.",
1203
+ "Re-run install-bridge and select at least one tool to configure it.",
1204
+ ].join("\n");
1205
+ }
1206
+ return [
1207
+ "To finish configuring this project, open it in an AI coding tool that has the",
1208
+ "Bridge MCP server configured and run /install-bridge.",
1209
+ "Until the project is configured, your Bridge MCP tools stay limited.",
1210
+ ].join("\n");
1211
+ }
1212
+ /** The exact consent-prompt prefix, shared with tests so the copy cannot drift. */
1213
+ export const INSTALL_BRIDGE_LAUNCH_CONSENT_PROMPT_PREFIX = "Bridge can configure and set up this project for you automatically. Open a ";
1214
+ /**
1215
+ * Ask for explicit TTY consent immediately before opening the selected tool (AC-7).
1216
+ * Returns `"spawn"` only when a live terminal user affirmatively agrees; returns
1217
+ * `"no-spawn"` WITHOUT prompting on a non-TTY run or a missing `promptLine` seam, and
1218
+ * on an `n`/`no` answer. Mirrors {@link offerGithubConnection}'s Y/N semantics: a
1219
+ * blank line, `y`, or `yes` accept; `n` or `no` decline. A prompt that throws safely
1220
+ * declines without surfacing the caught exception text. Uses only the per-call
1221
+ * `promptLine` seam, so it must run AFTER the tool-selection, chooser, and GitHub
1222
+ * flows have closed their readline interfaces — never while another prompt is active.
1223
+ */
1224
+ export async function requestInstallBridgeLaunchConsent(toolLabel, deps) {
1225
+ if (!deps.isTTY || !deps.promptLine)
1226
+ return "no-spawn";
1227
+ try {
1228
+ const answer = (await deps.promptLine(`${INSTALL_BRIDGE_LAUNCH_CONSENT_PROMPT_PREFIX}${toolLabel} session to do that now? (Y/n) `))
1229
+ .trim()
1230
+ .toLowerCase();
1231
+ if (answer === "n" || answer === "no")
1232
+ return "no-spawn";
1233
+ return "spawn";
1234
+ }
1235
+ catch {
1236
+ // Never surface caught exception text; treat a prompt failure as a decline.
1237
+ return "no-spawn";
1238
+ }
1239
+ }
1072
1240
  /**
1073
1241
  * Build a registry detection context from install deps. install-bridge has only
1074
1242
  * async `stat`, but the registry's `detect` callbacks are synchronous, so we
@@ -1597,7 +1765,7 @@ export function buildDryRunPreview(plan) {
1597
1765
  `Repo name: ${plan.repoName}${plan.attemptedServerResolution ? " (resolved server-side from your API key)" : ""}`,
1598
1766
  `Base URL (ping): ${plan.baseUrl}`,
1599
1767
  `Docs dir: ${plan.docsDir}`,
1600
- `Agent: ${plan.agentName}`,
1768
+ `Agent: ${describePlannedLaunchAgent(plan.launch)}`,
1601
1769
  "",
1602
1770
  "Step 1 — scaffold (runInit): commands, agents, pipelines, .bridge/config, secret-free MCP placeholders.",
1603
1771
  `Step 2 — connectivity ping (before any durable key write): GET ${plan.pingUrl} (X-API-Key: ${REDACTED_API_KEY})`,
@@ -1612,26 +1780,66 @@ export function buildDryRunPreview(plan) {
1612
1780
  ...buildLaunchStepPreview(plan),
1613
1781
  ];
1614
1782
  }
1783
+ /**
1784
+ * One-line description of the planned launch agent for the dry-run header (BAPI-657).
1785
+ * Never claims an unconditional Claude spawn: it reflects the discriminated launch
1786
+ * outcome — a single consent-gated tool, a choose-one among launchable tools, or a
1787
+ * no-automatic-launch manual outcome.
1788
+ */
1789
+ function describePlannedLaunchAgent(launch) {
1790
+ if (launch.kind === "spawn") {
1791
+ return `${toolLabelForLaunchAgent(launch.agent)} (${launch.agent}) — opens only after Y/N consent`;
1792
+ }
1793
+ if (launch.kind === "choose-one") {
1794
+ const labels = launch.agents.map((a) => toolLabelForLaunchAgent(a)).join(", ");
1795
+ return `choose one of ${labels} — the chosen tool opens only after Y/N consent`;
1796
+ }
1797
+ return launch.reason === "empty-selection"
1798
+ ? "none — no tools selected"
1799
+ : "none — no launchable tool selected; manual continuation is printed";
1800
+ }
1615
1801
  /**
1616
1802
  * The Step 5 preview lines, shared by both previews so the launch description
1617
- * cannot drift between the have-key and need-key flows.
1803
+ * cannot drift between the have-key and need-key flows (BAPI-657). The Step 5 body
1804
+ * is dispatched on the discriminated launch outcome: a consent-gated single-tool
1805
+ * spawn (with the full secret-free command shown), a consent-gated choose-one, or a
1806
+ * no-automatic-launch manual continuation — never an unconditional Claude spawn.
1618
1807
  *
1619
- * A --dry-run never writes the launch script, so the preview describes the
1620
- * materialization rather than performing it — but it still shows the full command,
1621
- * because the command is what the user is previewing and it is secret-free.
1808
+ * A --dry-run never writes the launch script, prompts for consent, or spawns, so
1809
+ * these lines describe the outcome rather than performing it.
1622
1810
  */
1623
1811
  function buildLaunchStepPreview(plan) {
1624
- return [
1812
+ const githubLines = [
1625
1813
  // BAPI-631: described, never performed in --dry-run — a preview must not open a
1626
1814
  // browser or reach the network. It is also strictly optional, so it carries no step
1627
1815
  // number of its own and never changes the 5-step count.
1628
1816
  "Step 4b — optional GitHub connect (SKIPPED in --dry-run): read GitHub's configured state",
1629
1817
  " via the install manifest and, only when it is unconfigured and the terminal is",
1630
1818
  " interactive, offer 'Connect GitHub? (Y/n)' before the agent session starts.",
1631
- "Step 5 — spawn agent session: the full command below is stored in a restricted launch script",
1632
- " (mode 0600, under the system temp dir) and only a short sourced runner is spawned",
1633
- " (the script itself is NOT written in --dry-run):",
1634
- ` ${plan.spawnCommand}`,
1819
+ ];
1820
+ if (plan.launch.kind === "spawn") {
1821
+ return [
1822
+ ...githubLines,
1823
+ `Step 5 — agent session (${toolLabelForLaunchAgent(plan.launch.agent)}): on a TTY the wizard first asks`,
1824
+ " 'Open a … session to do that now? (Y/n)'; only on consent is the full command below",
1825
+ " stored in a restricted launch script (mode 0600, under the system temp dir) and a short",
1826
+ " sourced runner spawned (the script itself is NOT written in --dry-run):",
1827
+ ` ${plan.launch.spawnCommand}`,
1828
+ ];
1829
+ }
1830
+ if (plan.launch.kind === "choose-one") {
1831
+ const labels = plan.launch.agents.map((a) => toolLabelForLaunchAgent(a)).join(", ");
1832
+ return [
1833
+ ...githubLines,
1834
+ `Step 5 — agent session: more than one selected tool can host it (${labels}); on a TTY the wizard`,
1835
+ " asks which single tool to open, then asks Y/N consent before spawning that one session.",
1836
+ ];
1837
+ }
1838
+ return [
1839
+ ...githubLines,
1840
+ plan.launch.reason === "empty-selection"
1841
+ ? "Step 5 — no agent session: no tools were selected, so nothing is configured or opened; re-run and select at least one tool."
1842
+ : "Step 5 — no automatic launch: no selected tool has an agentic CLI. The deterministic setup completes and copy-pasteable /install-bridge continuation is printed (your Bridge MCP tools stay limited until configured).",
1635
1843
  ];
1636
1844
  }
1637
1845
  /**
@@ -1668,7 +1876,7 @@ function buildBootstrapDryRunPreview(plan) {
1668
1876
  repoLine,
1669
1877
  `Base URL: ${plan.baseUrl}`,
1670
1878
  `Docs dir: ${plan.docsDir}`,
1671
- `Agent: ${plan.agentName}`,
1879
+ `Agent: ${describePlannedLaunchAgent(plan.launch)}`,
1672
1880
  "",
1673
1881
  "Step 1 — scaffold (runInit): commands, agents, pipelines, .bridge/config, secret-free MCP placeholders.",
1674
1882
  ...selfServeStep,
@@ -1879,33 +2087,58 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
1879
2087
  }
1880
2088
  }
1881
2089
  }
1882
- const agent = resolveAgentSpec(options.agentName) ?? resolveAgentSpec(DEFAULT_AGENT_NAME);
1883
- const spawnCommand = deps.buildShellCommand(agent, INSTALL_BRIDGE_AGENT_PROMPT, deps.cwd, deps.platform);
1884
2090
  const credentialStorePath = getPrimaryCredentialStorePath({
1885
2091
  env: deps.env,
1886
2092
  homedir: deps.homedir,
1887
2093
  });
1888
- // BAPI-635: resolve the AI-coding-tool selection (explicit --tools, then TTY
1889
- // multi-select, then the legacy non-TTY automatic set). The selected project
1890
- // JSON targets drive the existing read-merge-write path; selected global
1891
- // (Codex / Copilot CLI) and manual (Windsurf) targets are provisioned by the
2094
+ // BAPI-635: resolve the AI-coding-tool selection FIRST BAPI-657 makes this
2095
+ // selection (not a hidden --agent default) drive which post-install session opens.
2096
+ // Explicit --tools, then the TTY multi-select, then the legacy non-TTY automatic
2097
+ // set. The selected project JSON targets drive the read-merge-write path; selected
2098
+ // global (Codex / Copilot CLI) and manual (Windsurf) targets are provisioned by the
1892
2099
  // registry-driven emitter after the connectivity check.
1893
2100
  const selectedPlatforms = await resolveSelectedHostPlatforms(deps, options);
1894
2101
  const targets = hostConfigTargetsForPlatforms(selectedPlatforms);
1895
2102
  // Read-only detection (safe in dry-run) of global-config editors we can't write.
1896
2103
  const manualEditors = await detectManualEditors(deps);
2104
+ // BAPI-657: the pure, selection-driven launch decision. An explicit --agent is an
2105
+ // absolute override (ratified AC-4); otherwise the selected tools map to an agent
2106
+ // (Claude Code → claude, Cursor → cursor-agent), yielding spawn / choose-one /
2107
+ // manual. Performs no prompting, resolveAgentSpec, or I/O.
2108
+ const launchDecision = resolveInstallBridgeLaunchDecision(selectedPlatforms, options.agentName);
2109
+ // Derive the plan's launch data. For a single-agent spawn we resolve the spec and
2110
+ // build the (secret-free) command now so the dry-run preview can show it; a registry
2111
+ // miss here is a hard error — install-bridge NEVER falls back to DEFAULT_AGENT_NAME.
2112
+ let planLaunch;
2113
+ if (launchDecision.kind === "spawn") {
2114
+ const spec = resolveAgentSpec(launchDecision.agent);
2115
+ if (!spec) {
2116
+ errorLog(`Error: no launch agent is registered for '${launchDecision.agent}'.`);
2117
+ return 1;
2118
+ }
2119
+ planLaunch = {
2120
+ kind: "spawn",
2121
+ agent: launchDecision.agent,
2122
+ spawnCommand: deps.buildShellCommand(spec, INSTALL_BRIDGE_AGENT_PROMPT, deps.cwd, deps.platform),
2123
+ };
2124
+ }
2125
+ else if (launchDecision.kind === "choose-one") {
2126
+ planLaunch = { kind: "choose-one", agents: launchDecision.agents };
2127
+ }
2128
+ else {
2129
+ planLaunch = { kind: "manual", reason: launchDecision.reason };
2130
+ }
1897
2131
  const plan = {
1898
2132
  repoName,
1899
2133
  baseUrl,
1900
2134
  docsDir,
1901
- agentName: options.agentName,
2135
+ launch: planLaunch,
1902
2136
  configTargets: targets.map((t) => t.relPath),
1903
2137
  manualEditors: manualEditorNames(manualEditors),
1904
2138
  credentialTarget: `bapi:${repoName}`,
1905
2139
  credentialStorePath,
1906
2140
  pingUrl: buildPingUrl(baseUrl, repoName),
1907
2141
  prewarmCommand: buildPrewarmCommandPreview(),
1908
- spawnCommand,
1909
2142
  ...(bootstrapInviteMode
1910
2143
  ? { bootstrapInvite: true, exchangeUrl: buildBootstrapExchangeUrl(baseUrl) }
1911
2144
  : {}),
@@ -1921,46 +2154,72 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
1921
2154
  log(line);
1922
2155
  return 0;
1923
2156
  }
1924
- // ---- Materialize the Step 5 launch command BEFORE any install side effect ----
1925
- // `spawnCommand` embeds the whole INSTALL_BRIDGE_AGENT_PROMPT and is multiple KB
1926
- // far past what osascript can type into a Terminal/iTerm tab, which is why the
1927
- // spawn silently delivered a truncated line. It must travel via a launch script.
1928
- //
1929
- // This runs HERE, before the scaffold/mint/credential/config writes, because a
1930
- // command that cannot be launched safely is a whole-run failure, not a Step 5
1931
- // warning: the deterministic setup would otherwise complete and leave the user
1932
- // in the "configured but never configured by the agent" state the warning below
1933
- // explicitly calls out. Failing first means nothing is half-done.
1934
- const materialized = await materializeWorkerLaunchCommand(deps.startTicketsDeps, "install", spawnCommand);
1935
- if (!materialized.ok) {
1936
- errorLog(`Error: ${materialized.error}`);
1937
- return 1;
2157
+ // ---- Resolved-empty selection: a handled no-op (exit 0), never a Claude fallback ----
2158
+ // AC-3: with zero configured tools there is nothing to write and no session to open.
2159
+ // Return BEFORE any config/credential write, materialization, prewarm, or spawn, so a
2160
+ // bare `--tools=` (or an EOF-resolved picker) cleanly does nothing but guide a re-run.
2161
+ if (planLaunch.kind === "manual" && planLaunch.reason === "empty-selection") {
2162
+ log(buildManualInstallBridgeContinuation("empty-selection"));
2163
+ return 0;
1938
2164
  }
1939
- const launchCommand = materialized.command;
1940
- // Independent final guard on the line ACTUALLY handed to the terminal not on
1941
- // the original command. It catches the two ways a "successful" materialization
1942
- // can still be unlaunchable: no writer seam (inline command preserved verbatim),
1943
- // and a runner whose script path is unexpectedly long.
1944
- if (Buffer.byteLength(launchCommand, "utf8") >= MAX_TERMINAL_COMMAND_BYTES) {
1945
- errorLog("Error: the agent session command is too long to send to the terminal safely. " +
1946
- "Check that the system temporary directory is writable so the launch script can be used.");
1947
- return 1;
2165
+ // ---- Finalize the launch agent for spawn-capable decisions ----
2166
+ // choose-one: pick exactly one via the numbered chooser (TTY only); a non-TTY run or
2167
+ // a failed/aborted chooser degrades to the manual continuation (finalAgentName stays
2168
+ // null). This runs AFTER the multi-select readline closed and BEFORE the GitHub /
2169
+ // consent flows, so no two stdin readers ever overlap.
2170
+ let finalAgentName = null;
2171
+ let finalSpawnCommand = null;
2172
+ if (planLaunch.kind === "spawn") {
2173
+ finalAgentName = planLaunch.agent;
2174
+ finalSpawnCommand = planLaunch.spawnCommand;
2175
+ }
2176
+ else if (planLaunch.kind === "choose-one") {
2177
+ const chosen = await chooseInstallBridgeLaunchAgent(planLaunch.agents, deps);
2178
+ if (chosen) {
2179
+ const spec = resolveAgentSpec(chosen);
2180
+ if (!spec) {
2181
+ errorLog(`Error: no launch agent is registered for '${chosen}'.`);
2182
+ return 1;
2183
+ }
2184
+ finalAgentName = chosen;
2185
+ finalSpawnCommand = deps.buildShellCommand(spec, INSTALL_BRIDGE_AGENT_PROMPT, deps.cwd, deps.platform);
2186
+ }
2187
+ // chosen === null → non-TTY / unavailable / invalid → manual continuation below.
2188
+ }
2189
+ // planLaunch.kind === "manual" (no-launchable) → finalAgentName stays null.
2190
+ // ---- Materialize the Step 5 launch command + kick off the pre-warm — ONLY for a
2191
+ // spawn-capable path. A manual/no-launchable/chooser-declined path opens no session,
2192
+ // so it neither materializes a launch script nor warms a bucket it will never use.
2193
+ //
2194
+ // Materialization runs HERE, before the scaffold/mint/credential/config writes,
2195
+ // because a command that cannot be launched safely is a whole-run failure, not a
2196
+ // Step 5 warning: the deterministic setup would otherwise complete and leave the user
2197
+ // in the "configured but never configured by the agent" state. Failing first means
2198
+ // nothing is half-done. The @VERSION-pinned launcher lives in a DIFFERENT _npx bucket
2199
+ // than the @latest bucket this invocation warmed, so the pre-warm (fire-and-forget,
2200
+ // strictly fail-open) overlaps the ping/exchange/scaffold/writes and is awaited just
2201
+ // before the consent-gated spawn.
2202
+ let launchCommand = null;
2203
+ let prewarmPromise = null;
2204
+ if (finalAgentName && finalSpawnCommand) {
2205
+ const materialized = await materializeWorkerLaunchCommand(deps.startTicketsDeps, "install", finalSpawnCommand);
2206
+ if (!materialized.ok) {
2207
+ errorLog(`Error: ${materialized.error}`);
2208
+ return 1;
2209
+ }
2210
+ launchCommand = materialized.command;
2211
+ // Independent final guard on the line ACTUALLY handed to the terminal — not on the
2212
+ // original command. It catches the two ways a "successful" materialization can still
2213
+ // be unlaunchable: no writer seam (inline command preserved verbatim), and a runner
2214
+ // whose script path is unexpectedly long.
2215
+ if (Buffer.byteLength(launchCommand, "utf8") >= MAX_TERMINAL_COMMAND_BYTES) {
2216
+ errorLog("Error: the agent session command is too long to send to the terminal safely. " +
2217
+ "Check that the system temporary directory is writable so the launch script can be used.");
2218
+ return 1;
2219
+ }
2220
+ log(" pre-warming the version-pinned launcher bucket (in the background)…");
2221
+ prewarmPromise = deps.spawnPrewarm("npx", buildPrewarmArgs(), deps.env);
1948
2222
  }
1949
- // ---- Step 3b (kickoff) — pre-warm the @${VERSION}-pinned _npx bucket ----
1950
- // The @${VERSION}-pinned launcher written below lives in a DIFFERENT _npx bucket
1951
- // than the @latest bucket this `npx … install-bridge` invocation warmed, so the
1952
- // first real MCP launch would otherwise pay a cold install inline (which can
1953
- // exceed the client's connect deadline). Nothing before the Step 5 terminal spawn
1954
- // depends on the warmed bucket — only the spawned session's first MCP launch does
1955
- // — so START the warm HERE (fire-and-forget) and AWAIT it just before that spawn.
1956
- // Its multi-second `npx` resolve then overlaps the network round-trips (ping /
1957
- // exchange), the scaffold, and the config/credential writes instead of adding to
1958
- // them serially. Strictly fail-open: the default impl always RESOLVES (never
1959
- // rejects), so a later abort-return that skips the await leaves no unhandled
1960
- // rejection. Positioned AFTER the launch-command guards above so an aborted run
1961
- // that never reaches an install side effect also never spawns the pre-warm.
1962
- log(" pre-warming the version-pinned launcher bucket (in the background)…");
1963
- const prewarmPromise = deps.spawnPrewarm("npx", buildPrewarmArgs(), deps.env);
1964
2223
  const credentialWriteDeps = {
1965
2224
  env: deps.env,
1966
2225
  homedir: deps.homedir,
@@ -2307,47 +2566,70 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
2307
2566
  // spawn would put two prompts on the same terminal at once.
2308
2567
  await offerGithubConnection(repoName, deps, log);
2309
2568
  // ---- Step 3b (await) — settle the background pre-warm before the session opens ----
2310
- // The warm was kicked off right after the launch-command guards (above); await it
2311
- // HERE so the bucket is settled before the spawned session's first MCP launch, but
2312
- // AFTER all the work it overlapped with. Strictly fail-open the exit code is
2313
- // never affected, mirroring the original inline Step 3b behavior.
2314
- const prewarm = await prewarmPromise;
2315
- if (prewarm.ok) {
2316
- log(" launcher bucket warmed (the first MCP launch will not pay a cold install).");
2317
- log(` ${MCP_TIMEOUT_GUIDANCE}`);
2569
+ // The warm was kicked off (only on a spawn-capable path) right after the
2570
+ // launch-command guards; await it HERE so the bucket is settled before the spawned
2571
+ // session's first MCP launch, but AFTER all the work it overlapped with — and BEFORE
2572
+ // the consent prompt, so no spawn ever races an in-flight warm. Strictly fail-open —
2573
+ // the exit code is never affected. Skipped entirely when no session will be opened.
2574
+ if (prewarmPromise) {
2575
+ const prewarm = await prewarmPromise;
2576
+ if (prewarm.ok) {
2577
+ log(" launcher bucket warmed (the first MCP launch will not pay a cold install).");
2578
+ log(` ${MCP_TIMEOUT_GUIDANCE}`);
2579
+ }
2580
+ else {
2581
+ // Escalate to a stronger, still secret-free warning when pre-warm didn't
2582
+ // complete cleanly — but the install itself still succeeds (exit unaffected).
2583
+ errorLog(`Warning: could not pre-warm the version-pinned launcher bucket${prewarm.warning ? ` (${prewarm.warning})` : ""}. ` +
2584
+ `The first MCP launch may pay a one-time cold install and could be slow. ${MCP_TIMEOUT_GUIDANCE}`);
2585
+ }
2318
2586
  }
2319
- else {
2320
- // Escalate to a stronger, still secret-free warning when pre-warm didn't
2321
- // complete cleanly but the install itself still succeeds (exit unaffected).
2322
- errorLog(`Warning: could not pre-warm the version-pinned launcher bucket${prewarm.warning ? ` (${prewarm.warning})` : ""}. ` +
2323
- `The first MCP launch may pay a one-time cold install and could be slow. ${MCP_TIMEOUT_GUIDANCE}`);
2324
- }
2325
- // ---- Step 5 — spawn a fresh agent session for the agentic remainder ----
2326
- log(`Step 5/5 opening a ${agent.name} session for /install-bridge configuration + capability report…`);
2327
- const terminal = detectTerminal(undefined, deps.env);
2328
- // Only the validated short runner reaches the terminal never the inline prompt.
2329
- const spawnResult = await deps.spawnTerminalTab(deps.startTicketsDeps, terminal, launchCommand, {
2330
- key: "install",
2331
- worktreePath: deps.cwd,
2332
- });
2333
- if (!spawnResult.ok) {
2334
- // Steps 1–4 (scaffold, config, connectivity-verified, credential persist) all
2335
- // succeeded and are durable; only the best-effort Step 5 tab spawn failed. The
2336
- // deterministic setup is complete, so this is a non-fatal warning (exit 0) — the
2337
- // user can run the agentic remainder by hand. Mirrors start-tickets treating a
2338
- // spawn failure as non-fatal to the run.
2339
- errorLog(`Warning: setup steps completed, but the agent session could not be opened (${spawnResult.error}). ` +
2340
- "The project is NOT configured yet run /install-bridge manually in this project to derive " +
2341
- "and apply the config fields and see the capability report, then choose whether to run " +
2342
- "/parse-repository to index the repository.");
2587
+ // ---- Step 5 — consent-gate, then spawn the selected tool (or print continuation) ----
2588
+ // AC-7: for a spawn-capable path, ask Y/N first (`requestInstallBridgeLaunchConsent`
2589
+ // resolves to no-spawn without prompting on a non-TTY run). A decline, a no-launchable
2590
+ // selection, a non-TTY run, or a declined/aborted chooser all skip the spawn and fall
2591
+ // through to the shared /install-bridge continuation + limited-tools notice below so
2592
+ // every no-session outcome gets identical, accurate remediation.
2593
+ if (finalAgentName && launchCommand) {
2594
+ const consent = await requestInstallBridgeLaunchConsent(toolLabelForLaunchAgent(finalAgentName), deps);
2595
+ if (consent === "spawn") {
2596
+ log(`Step 5/5 opening a ${finalAgentName} session for /install-bridge configuration + concise capability report…`);
2597
+ const terminal = detectTerminal(undefined, deps.env);
2598
+ // Only the validated short runner reaches the terminal — never the inline prompt.
2599
+ // The branded "Bridge Install" title labels the tab/badge instead of "install
2600
+ // Implementation" (AC-6).
2601
+ const spawnResult = await deps.spawnTerminalTab(deps.startTicketsDeps, terminal, launchCommand, {
2602
+ key: "install",
2603
+ worktreePath: deps.cwd,
2604
+ title: "Bridge Install",
2605
+ });
2606
+ if (!spawnResult.ok) {
2607
+ // Steps 1–4 (scaffold, config, connectivity-verified, credential persist) all
2608
+ // succeeded and are durable; only the best-effort Step 5 tab spawn failed. The
2609
+ // deterministic setup is complete, so this is a non-fatal warning (exit 0), with
2610
+ // the same shared continuation so the remediation is consistent.
2611
+ errorLog(`Warning: setup steps completed, but the agent session could not be opened (${spawnResult.error}).`);
2612
+ log(buildManualInstallBridgeContinuation("configured"));
2613
+ return 0;
2614
+ }
2615
+ log("");
2616
+ log(`install-bridge setup steps complete. A fresh ${finalAgentName} session is now applying ` +
2617
+ "configuration, presenting the concise capability report, and recommending /learn-repository.");
2618
+ log("NOTE: the install is not finished until that session's apply reports applied fields — " +
2619
+ "it will pause to ask you to approve the project description. Indexing starts automatically " +
2620
+ "once the repository reaches full parse readiness — there is no indexing question to answer. " +
2621
+ "Verify afterwards on the project's Get Started page (install status panel) or via the " +
2622
+ "session's 'Applied N of M' summary.");
2623
+ return 0;
2624
+ }
2625
+ // Declined (or non-TTY suppression): the deterministic setup is complete and durable.
2626
+ log(buildManualInstallBridgeContinuation("configured"));
2343
2627
  return 0;
2344
2628
  }
2345
- log("");
2346
- log(`install-bridge setup steps complete. A fresh ${agent.name} session is now applying ` +
2347
- "configuration, presenting the capability report, and ending with one indexing-consent question.");
2348
- log("NOTE: the install is not finished until that session's apply reports applied fields — " +
2349
- "it will pause to ask you to approve the project description, and it will close by asking " +
2350
- "'[Y/n] Index repository now?'. Verify afterwards on the project's Get Started page " +
2351
- "(install status panel) or via the session's 'Applied N of M' summary.");
2629
+ // No launchable session (a no-launchable selection, or a non-TTY / declined chooser):
2630
+ // finalAgentName is null, so nothing was materialized. The configured hosts are
2631
+ // durable — print the shared continuation + limited-tools note (never the
2632
+ // empty-selection variant, which already returned above).
2633
+ log(buildManualInstallBridgeContinuation("configured"));
2352
2634
  return 0;
2353
2635
  }