@bridge_gpt/mcp-server 0.2.26 → 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.
Files changed (70) hide show
  1. package/README.md +32 -24
  2. package/build/commands.generated.js +2 -2
  3. package/build/conductor/bridge-api-client.js +98 -1
  4. package/build/conductor/epic-reconcile.js +28 -0
  5. package/build/conductor/epic-runtime.js +28 -1
  6. package/build/conductor-bin.js +68 -9267
  7. package/build/conductor-claude-hook-bin.js +2 -172
  8. package/build/connect-github-api.js +9 -0
  9. package/build/connect-github.js +10 -0
  10. package/build/decision-page-template.js +2 -31
  11. package/build/doctor.js +2 -2
  12. package/build/env-flags.js +23 -0
  13. package/build/index.js +4699 -37043
  14. package/build/init.js +80 -56
  15. package/build/install-bridge.js +451 -153
  16. package/build/mcp-host-targets.js +12 -0
  17. package/build/readme.generated.js +1 -1
  18. package/build/setup-epic.js +198 -4
  19. package/build/start-tickets.js +25 -4
  20. package/build/tool-surface-gating.js +13 -2
  21. package/build/version.generated.js +1 -1
  22. package/package.json +3 -5
  23. package/design-assets/favicon/android-chrome-192x192.png +0 -0
  24. package/design-assets/favicon/android-chrome-512x512.png +0 -0
  25. package/design-assets/favicon/apple-touch-icon.png +0 -0
  26. package/design-assets/favicon/favicon-16x16.png +0 -0
  27. package/design-assets/favicon/favicon.ico +0 -0
  28. package/design-assets/favicon/site.webmanifest +0 -1
  29. package/public/css/main.min.css +0 -4971
  30. package/public/css/main.min.css.map +0 -1
  31. package/public/fonts/OFL.txt +0 -93
  32. package/public/fonts/SourceSansPro-Black.ttf +0 -0
  33. package/public/fonts/SourceSansPro-BlackItalic.ttf +0 -0
  34. package/public/fonts/SourceSansPro-Bold.ttf +0 -0
  35. package/public/fonts/SourceSansPro-BoldItalic.ttf +0 -0
  36. package/public/fonts/SourceSansPro-ExtraLight.ttf +0 -0
  37. package/public/fonts/SourceSansPro-ExtraLightItalic.ttf +0 -0
  38. package/public/fonts/SourceSansPro-Italic.ttf +0 -0
  39. package/public/fonts/SourceSansPro-Light.ttf +0 -0
  40. package/public/fonts/SourceSansPro-LightItalic.ttf +0 -0
  41. package/public/fonts/SourceSansPro-Regular.ttf +0 -0
  42. package/public/fonts/SourceSansPro-SemiBold.ttf +0 -0
  43. package/public/fonts/SourceSansPro-SemiBoldItalic.ttf +0 -0
  44. package/public/img/bridge-logo-160x51.webp +0 -0
  45. package/public/img/bridge-logo-300x92.webp +0 -0
  46. package/public/img/favicon/android-chrome-192x192.png +0 -0
  47. package/public/img/favicon/android-chrome-512x512.png +0 -0
  48. package/public/img/favicon/apple-touch-icon.png +0 -0
  49. package/public/img/favicon/favicon-16x16.png +0 -0
  50. package/public/img/favicon/favicon-32x32.png +0 -0
  51. package/public/img/favicon/favicon.ico +0 -0
  52. package/public/img/favicon/site.webmanifest +0 -1
  53. package/public/img/installation/bitbucket/app-password-1.png +0 -0
  54. package/public/img/installation/bitbucket/app-password-2.png +0 -0
  55. package/public/img/installation/bitbucket/create-token-1.png +0 -0
  56. package/public/img/installation/bitbucket/create-token-2.png +0 -0
  57. package/public/img/installation/bitbucket/webhook-1.png +0 -0
  58. package/public/img/installation/github/github-review-webhook.png +0 -0
  59. package/public/img/installation/jira/credentials/api-key.png +0 -0
  60. package/public/img/installation/jira/webhook/create-rule.png +0 -0
  61. package/public/img/installation/jira/webhook/project-settings.png +0 -0
  62. package/public/img/installation/jira/webhook/rule-create-1.png +0 -0
  63. package/public/img/installation/jira/webhook/rule-create-2.png +0 -0
  64. package/public/img/installation/jira/webhook/rule-create-3.png +0 -0
  65. package/public/img/installation/pinecone/pinecone-api-key.png +0 -0
  66. package/public/img/installation/pinecone/pinecone-index.png +0 -0
  67. package/public/js/main.min.js +0 -10409
  68. package/public/js/main.min.js.map +0 -1
  69. package/smoke-test/SMOKE-TEST.md +0 -560
  70. package/smoke-test/smoke-test-mcp.md +0 -23
@@ -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>";
@@ -113,7 +116,11 @@ export const MCP_TIMEOUT_GUIDANCE = "Note: if your MCP client has a very short c
113
116
  "(the initial npx package resolution/download can exceed a short connect timeout).";
114
117
  /** The exact, version-pinned launcher spec the pre-warm spawns (fast-exiting --version). */
115
118
  export function buildPrewarmArgs() {
116
- return ["-y", `@bridge_gpt/mcp-server@${VERSION}`, "--version"];
119
+ // `--prefer-offline`: the initial `@latest` bootstrap already populated the npm
120
+ // cache with the exact tarball this `@${VERSION}`-pinned warm needs, so resolve
121
+ // from cache and skip the redundant registry-metadata round-trip (falls back to
122
+ // the network on a genuine miss).
123
+ return ["-y", "--prefer-offline", `@bridge_gpt/mcp-server@${VERSION}`, "--version"];
117
124
  }
118
125
  /** Secret-free preview of the pre-warm command (no env, no key). */
119
126
  export function buildPrewarmCommandPreview() {
@@ -121,44 +128,37 @@ export function buildPrewarmCommandPreview() {
121
128
  }
122
129
  /**
123
130
  * The natural-language prompt handed to the spawned agent session. It is
124
- * CONFIGURE-ONLY: it derives and applies configuration, presents the capability
125
- * report, and ends with exactly one indexing-consent question. It never chains
126
- * /learn-repository and never indexes before explicit consent.
127
- *
128
- * The index-consent beat is AGENT-OWNED (asked inside this spawned session), not
129
- * parent-CLI-owned, because `spawnTerminalTab()` opens an asynchronous terminal
130
- * session: the parent CLI returns immediately and cannot reliably ask a question
131
- * that must appear AFTER the spawned session's capability report. Asking it via
132
- * the parent's `deps.promptLine` would display the question before the report
133
- * even exists. `deps.promptLine` therefore stays the owner of the parent-CLI
134
- * prompts (repository / overwrite / invite confirmation) and is intentionally
135
- * 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.
136
138
  */
137
139
  export const INSTALL_BRIDGE_AGENT_PROMPT = "Execute the /install-bridge command in the install-spawn context (tell the command it is running " +
138
- "in the install-spawn context so it SKIPS its Stage 8 and Stage 9 offers — this session's only " +
139
- "closing interaction is the single indexing question below). Do NOT run /learn-repository. Do NOT " +
140
- "call parse_repository (or otherwise start indexing) before the capability report and explicit " +
141
- "consent below. " +
142
- "Complete the command's read-after-write five-section capability report first: 'Connected ', " +
143
- "'Not yet connected ✗', 'Tools you can use now', 'Tools you'll unlock', and 'Recommended next " +
144
- "step + why'. " +
145
- "Only AFTER that report is fully presented, ask exactly one question using this visible prompt: " +
146
- "'[Y/n] Index repository now?'. Only an explicit affirmative answer (e.g. 'y'/'yes') starts " +
147
- "indexing; a blank answer, a negative answer, EOF, an unavailable interaction, and any " +
148
- "non-interactive/headless run all resolve to NO. " +
149
- "On an affirmative answer: call the parse_repository MCP tool exactly once, describe the accepted " +
150
- "job as QUEUED, and direct later progress checks to get_parse_status or /check-parse-status " +
151
- "(do NOT poll it to completion). If parse_repository returns a blocking refusal or error, do NOT " +
152
- "claim the job was queued — report the sanitized result and leave indexing pending. " +
153
- "On NO (or any unavailable/non-interactive resolution): do not index; print the exact copy-paste " +
154
- "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. " +
155
155
  "Never request, echo, or transport any credential — only ever direct the human to that " +
156
156
  "integration's own configure_in pointer, verbatim. The pointer is per-integration and is NOT " +
157
157
  "always the setup UI: GitHub's is a terminal command (connect-github), while Jira, SFCC, and " +
158
158
  "Bitbucket point at the setup UI. Follow whatever the report says rather than assuming. " +
159
159
  "End with an explicit summary line stating how many config fields the apply_install_manifest call " +
160
- "applied (e.g. 'Applied 8 of 9 derived fields') and whether indexing was queued or left pending " +
161
- "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.";
162
162
  /** Default base URL when `BAPI_BASE_URL` is unset (mirrors index.ts). */
163
163
  export const DEFAULT_BAPI_BASE_URL = "https://bridgegpt-api.com";
164
164
  /** Default docs dir when `BAPI_DOCS_DIR` is unset (mirrors index.ts). */
@@ -171,8 +171,11 @@ export function getInstallBridgeUsage() {
171
171
  "",
172
172
  "One-command Bridge API project bootstrap. Scaffolds the project, writes the",
173
173
  "per-host MCP config with your credentials, verifies connectivity, persists the",
174
- "routing credential, then opens a fresh agent session to derive the remaining",
175
- "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.",
176
179
  "",
177
180
  "Run it bare — `install-bridge` with no flags — in a terminal and it asks",
178
181
  `\`${INSTALL_BRIDGE_KEY_SELECTOR_PROMPT.trim()}\` first. Answer yes (or press Enter) for the`,
@@ -241,24 +244,35 @@ export function getInstallBridgeUsage() {
241
244
  ` ${HOST_PLATFORM_ORDER.join(", ")}.`,
242
245
  " Both --tools=claude-code,codex and",
243
246
  " --tools claude-code,codex are accepted. On an",
244
- " interactive terminal WITHOUT this flag you are",
245
- " asked which tools you use (Claude Code plus any",
246
- " detected editors are pre-checked). A non-",
247
- " interactive run without --tools writes the legacy",
248
- " automatic set (Claude Code plus any detected",
249
- " Cursor / Copilot VS Code). --tools= (empty) is an",
250
- " 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.",
251
257
  " --force Overwrite an existing real BAPI_API_KEY in a",
252
258
  " host config (or in the credential store) without",
253
259
  " prompting.",
254
260
  " --dry-run Preview every step (scaffold targets, config",
255
261
  " files + keys with the key REDACTED, ping",
256
- " target, credential target, spawn command)",
257
- " without writing, pinging, or spawning anything.",
258
- " With --invite it also never calls the exchange",
259
- " endpoint and never generates or stores a secret.",
260
- " --agent claude|cursor-agent Agent to launch for the agentic remainder",
261
- " (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.",
262
276
  " -h, --help Show this help.",
263
277
  "",
264
278
  "Environment: BAPI_BASE_URL (default https://bridgegpt-api.com) and BAPI_DOCS_DIR",
@@ -284,7 +298,10 @@ export function parseInstallBridgeArgs(argv) {
284
298
  let repo;
285
299
  let force = false;
286
300
  let dryRun = false;
287
- 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;
288
305
  let invite;
289
306
  let email;
290
307
  let tools;
@@ -964,22 +981,26 @@ export async function resolveRepoName(options, deps, mode = "existing-registrati
964
981
  /** Stable wording of the interactive tool-selection prompt (BAPI-635). */
965
982
  export const INSTALL_BRIDGE_TOOL_SELECTOR_PROMPT = "Which AI coding tools do you use on this project?";
966
983
  /**
967
- * Interactive numbered multi-select prompt on stderr (TTY only). Displays each
968
- * option with a checked/unchecked marker seeded from `defaults`, accepts a
969
- * comma-separated list of numbers to TOGGLE, and accepts the current selection on
970
- * a bare Enter. Reprints on an invalid token rather than corrupting state.
971
- * Resolves safely (to the seeded defaults) on EOF / synchronous close so a
972
- * 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.
973
992
  */
974
993
  export function promptMultiSelectViaReadline(promptText, options, defaults, input = process.stdin, output = process.stderr) {
975
994
  return new Promise((resolve) => {
976
995
  const selected = new Set(defaults);
977
996
  const render = () => {
978
997
  output.write(`\n${promptText}\n`);
998
+ output.write("[x] = selected · [ ] = not selected\n");
979
999
  options.forEach((opt, idx) => {
980
1000
  const mark = selected.has(opt.id) ? "[x]" : "[ ]";
981
1001
  output.write(` ${idx + 1}. ${mark} ${opt.label}\n`);
982
1002
  });
1003
+ output.write("Type numbers to toggle, e.g. 1,3 — then Enter. Enter with ≥1 selected accepts.\n");
983
1004
  output.write("Enter numbers to toggle (comma-separated), or press Enter to accept: ");
984
1005
  };
985
1006
  const rl = readline.createInterface({ input, output });
@@ -1000,6 +1021,14 @@ export function promptMultiSelectViaReadline(promptText, options, defaults, inpu
1000
1021
  rl.question("", (answer) => {
1001
1022
  const trimmed = answer.trim();
1002
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
+ }
1003
1032
  finish();
1004
1033
  return;
1005
1034
  }
@@ -1026,7 +1055,9 @@ export function promptMultiSelectViaReadline(promptText, options, defaults, inpu
1026
1055
  else
1027
1056
  selected.add(opt.id);
1028
1057
  }
1029
- 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();
1030
1061
  });
1031
1062
  };
1032
1063
  ask();
@@ -1036,8 +1067,8 @@ export function promptMultiSelectViaReadline(promptText, options, defaults, inpu
1036
1067
  * Resolve the selected host platforms with strict precedence (BAPI-635):
1037
1068
  * 1. explicit `--tools` (including an explicit EMPTY selection — never falls
1038
1069
  * back to detection),
1039
- * 2. interactive multi-select on a TTY (seeded from registry detection, with
1040
- * 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),
1041
1072
  * 3. the legacy non-TTY automatic set: Claude Code plus only the currently
1042
1073
  * detected Copilot VS Code and Cursor automatic targets (never Codex or
1043
1074
  * Copilot CLI just because a global directory exists).
@@ -1049,11 +1080,12 @@ export async function resolveSelectedHostPlatforms(deps, options) {
1049
1080
  }
1050
1081
  const ctx = await buildDetectionContext(deps);
1051
1082
  const detected = new Set(detectDefaultPlatforms(ctx));
1052
- // 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.
1053
1086
  if (deps.isTTY && deps.promptMultiSelect) {
1054
1087
  const optionList = allHostTargets().map((t) => ({ id: t.id, label: t.label }));
1055
- // Claude Code is always a checked default; add every detected platform.
1056
- const defaults = HOST_PLATFORM_ORDER.filter((id) => id === "claude-code" || detected.has(id));
1088
+ const defaults = [];
1057
1089
  const chosen = await deps.promptMultiSelect(INSTALL_BRIDGE_TOOL_SELECTOR_PROMPT, optionList, defaults);
1058
1090
  return HOST_PLATFORM_ORDER.filter((id) => chosen.includes(id));
1059
1091
  }
@@ -1065,6 +1097,146 @@ export async function resolveSelectedHostPlatforms(deps, options) {
1065
1097
  legacy.push("copilot-vscode");
1066
1098
  return HOST_PLATFORM_ORDER.filter((id) => legacy.includes(id));
1067
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
+ }
1068
1240
  /**
1069
1241
  * Build a registry detection context from install deps. install-bridge has only
1070
1242
  * async `stat`, but the registry's `detect` callbacks are synchronous, so we
@@ -1593,7 +1765,7 @@ export function buildDryRunPreview(plan) {
1593
1765
  `Repo name: ${plan.repoName}${plan.attemptedServerResolution ? " (resolved server-side from your API key)" : ""}`,
1594
1766
  `Base URL (ping): ${plan.baseUrl}`,
1595
1767
  `Docs dir: ${plan.docsDir}`,
1596
- `Agent: ${plan.agentName}`,
1768
+ `Agent: ${describePlannedLaunchAgent(plan.launch)}`,
1597
1769
  "",
1598
1770
  "Step 1 — scaffold (runInit): commands, agents, pipelines, .bridge/config, secret-free MCP placeholders.",
1599
1771
  `Step 2 — connectivity ping (before any durable key write): GET ${plan.pingUrl} (X-API-Key: ${REDACTED_API_KEY})`,
@@ -1608,26 +1780,66 @@ export function buildDryRunPreview(plan) {
1608
1780
  ...buildLaunchStepPreview(plan),
1609
1781
  ];
1610
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
+ }
1611
1801
  /**
1612
1802
  * The Step 5 preview lines, shared by both previews so the launch description
1613
- * 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.
1614
1807
  *
1615
- * A --dry-run never writes the launch script, so the preview describes the
1616
- * materialization rather than performing it — but it still shows the full command,
1617
- * 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.
1618
1810
  */
1619
1811
  function buildLaunchStepPreview(plan) {
1620
- return [
1812
+ const githubLines = [
1621
1813
  // BAPI-631: described, never performed in --dry-run — a preview must not open a
1622
1814
  // browser or reach the network. It is also strictly optional, so it carries no step
1623
1815
  // number of its own and never changes the 5-step count.
1624
1816
  "Step 4b — optional GitHub connect (SKIPPED in --dry-run): read GitHub's configured state",
1625
1817
  " via the install manifest and, only when it is unconfigured and the terminal is",
1626
1818
  " interactive, offer 'Connect GitHub? (Y/n)' before the agent session starts.",
1627
- "Step 5 — spawn agent session: the full command below is stored in a restricted launch script",
1628
- " (mode 0600, under the system temp dir) and only a short sourced runner is spawned",
1629
- " (the script itself is NOT written in --dry-run):",
1630
- ` ${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).",
1631
1843
  ];
1632
1844
  }
1633
1845
  /**
@@ -1664,7 +1876,7 @@ function buildBootstrapDryRunPreview(plan) {
1664
1876
  repoLine,
1665
1877
  `Base URL: ${plan.baseUrl}`,
1666
1878
  `Docs dir: ${plan.docsDir}`,
1667
- `Agent: ${plan.agentName}`,
1879
+ `Agent: ${describePlannedLaunchAgent(plan.launch)}`,
1668
1880
  "",
1669
1881
  "Step 1 — scaffold (runInit): commands, agents, pipelines, .bridge/config, secret-free MCP placeholders.",
1670
1882
  ...selfServeStep,
@@ -1875,33 +2087,58 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
1875
2087
  }
1876
2088
  }
1877
2089
  }
1878
- const agent = resolveAgentSpec(options.agentName) ?? resolveAgentSpec(DEFAULT_AGENT_NAME);
1879
- const spawnCommand = deps.buildShellCommand(agent, INSTALL_BRIDGE_AGENT_PROMPT, deps.cwd, deps.platform);
1880
2090
  const credentialStorePath = getPrimaryCredentialStorePath({
1881
2091
  env: deps.env,
1882
2092
  homedir: deps.homedir,
1883
2093
  });
1884
- // BAPI-635: resolve the AI-coding-tool selection (explicit --tools, then TTY
1885
- // multi-select, then the legacy non-TTY automatic set). The selected project
1886
- // JSON targets drive the existing read-merge-write path; selected global
1887
- // (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
1888
2099
  // registry-driven emitter after the connectivity check.
1889
2100
  const selectedPlatforms = await resolveSelectedHostPlatforms(deps, options);
1890
2101
  const targets = hostConfigTargetsForPlatforms(selectedPlatforms);
1891
2102
  // Read-only detection (safe in dry-run) of global-config editors we can't write.
1892
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
+ }
1893
2131
  const plan = {
1894
2132
  repoName,
1895
2133
  baseUrl,
1896
2134
  docsDir,
1897
- agentName: options.agentName,
2135
+ launch: planLaunch,
1898
2136
  configTargets: targets.map((t) => t.relPath),
1899
2137
  manualEditors: manualEditorNames(manualEditors),
1900
2138
  credentialTarget: `bapi:${repoName}`,
1901
2139
  credentialStorePath,
1902
2140
  pingUrl: buildPingUrl(baseUrl, repoName),
1903
2141
  prewarmCommand: buildPrewarmCommandPreview(),
1904
- spawnCommand,
1905
2142
  ...(bootstrapInviteMode
1906
2143
  ? { bootstrapInvite: true, exchangeUrl: buildBootstrapExchangeUrl(baseUrl) }
1907
2144
  : {}),
@@ -1917,30 +2154,71 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
1917
2154
  log(line);
1918
2155
  return 0;
1919
2156
  }
1920
- // ---- Materialize the Step 5 launch command BEFORE any install side effect ----
1921
- // `spawnCommand` embeds the whole INSTALL_BRIDGE_AGENT_PROMPT and is multiple KB
1922
- // far past what osascript can type into a Terminal/iTerm tab, which is why the
1923
- // spawn silently delivered a truncated line. It must travel via a launch script.
1924
- //
1925
- // This runs HERE, before the scaffold/mint/credential/config writes, because a
1926
- // command that cannot be launched safely is a whole-run failure, not a Step 5
1927
- // warning: the deterministic setup would otherwise complete and leave the user
1928
- // in the "configured but never configured by the agent" state the warning below
1929
- // explicitly calls out. Failing first means nothing is half-done.
1930
- const materialized = await materializeWorkerLaunchCommand(deps.startTicketsDeps, "install", spawnCommand);
1931
- if (!materialized.ok) {
1932
- errorLog(`Error: ${materialized.error}`);
1933
- 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;
1934
2164
  }
1935
- const launchCommand = materialized.command;
1936
- // Independent final guard on the line ACTUALLY handed to the terminal not on
1937
- // the original command. It catches the two ways a "successful" materialization
1938
- // can still be unlaunchable: no writer seam (inline command preserved verbatim),
1939
- // and a runner whose script path is unexpectedly long.
1940
- if (Buffer.byteLength(launchCommand, "utf8") >= MAX_TERMINAL_COMMAND_BYTES) {
1941
- errorLog("Error: the agent session command is too long to send to the terminal safely. " +
1942
- "Check that the system temporary directory is writable so the launch script can be used.");
1943
- 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);
1944
2222
  }
1945
2223
  const credentialWriteDeps = {
1946
2224
  env: deps.env,
@@ -2238,25 +2516,6 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
2238
2516
  // ensureGitignored for the state file can still throw — also advisory.
2239
2517
  errorLog("Warning: could not persist the install-state file (non-fatal).");
2240
2518
  }
2241
- // ---- Step 3b — pre-warm the @${VERSION}-pinned _npx bucket (BAPI-451 W3) ----
2242
- // The launcher just written is pinned to @${VERSION}, a DIFFERENT _npx bucket
2243
- // than the @latest bucket this `npx … install-bridge` invocation warmed. Spawn
2244
- // the exact pinned launcher once with the fast-exiting `--version` flag so the
2245
- // first real MCP launch resolves from a warm bucket instead of paying the cold
2246
- // install inline (which can exceed the client's connect deadline). Strictly
2247
- // fail-open: a failure warns (secret-free) but never fails the install.
2248
- log(" pre-warming the version-pinned launcher bucket…");
2249
- const prewarm = await deps.spawnPrewarm("npx", buildPrewarmArgs(), deps.env);
2250
- if (prewarm.ok) {
2251
- log(" launcher bucket warmed (the first MCP launch will not pay a cold install).");
2252
- log(` ${MCP_TIMEOUT_GUIDANCE}`);
2253
- }
2254
- else {
2255
- // Escalate to a stronger, still secret-free warning when pre-warm didn't
2256
- // complete cleanly — but the install itself still succeeds (exit unaffected).
2257
- errorLog(`Warning: could not pre-warm the version-pinned launcher bucket${prewarm.warning ? ` (${prewarm.warning})` : ""}. ` +
2258
- `The first MCP launch may pay a one-time cold install and could be slow. ${MCP_TIMEOUT_GUIDANCE}`);
2259
- }
2260
2519
  // ---- Step 4 — persist the credential ----
2261
2520
  if (bootstrapInviteMode) {
2262
2521
  // FAIL-CLOSED (unlike normal mode below). The pending secret IS the minted admin
@@ -2306,32 +2565,71 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
2306
2565
  // observe GitHub as configured if the user connects it here, and running it after the
2307
2566
  // spawn would put two prompts on the same terminal at once.
2308
2567
  await offerGithubConnection(repoName, deps, log);
2309
- // ---- Step 5spawn a fresh agent session for the agentic remainder ----
2310
- log(`Step 5/5 opening a ${agent.name} session for /install-bridge configuration + capability report…`);
2311
- const terminal = detectTerminal(undefined, deps.env);
2312
- // Only the validated short runner reaches the terminal never the inline prompt.
2313
- const spawnResult = await deps.spawnTerminalTab(deps.startTicketsDeps, terminal, launchCommand, {
2314
- key: "install",
2315
- worktreePath: deps.cwd,
2316
- });
2317
- if (!spawnResult.ok) {
2318
- // Steps 1–4 (scaffold, config, connectivity-verified, credential persist) all
2319
- // succeeded and are durable; only the best-effort Step 5 tab spawn failed. The
2320
- // deterministic setup is complete, so this is a non-fatal warning (exit 0) — the
2321
- // user can run the agentic remainder by hand. Mirrors start-tickets treating a
2322
- // spawn failure as non-fatal to the run.
2323
- errorLog(`Warning: setup steps completed, but the agent session could not be opened (${spawnResult.error}). ` +
2324
- "The project is NOT configured yet — run /install-bridge manually in this project to derive " +
2325
- "and apply the config fields and see the capability report, then choose whether to run " +
2326
- "/parse-repository to index the repository.");
2568
+ // ---- Step 3b (await) settle the background pre-warm before the session opens ----
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
+ }
2586
+ }
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"));
2327
2627
  return 0;
2328
2628
  }
2329
- log("");
2330
- log(`install-bridge setup steps complete. A fresh ${agent.name} session is now applying ` +
2331
- "configuration, presenting the capability report, and ending with one indexing-consent question.");
2332
- log("NOTE: the install is not finished until that session's apply reports applied fields — " +
2333
- "it will pause to ask you to approve the project description, and it will close by asking " +
2334
- "'[Y/n] Index repository now?'. Verify afterwards on the project's Get Started page " +
2335
- "(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"));
2336
2634
  return 0;
2337
2635
  }