@bridge_gpt/mcp-server 0.2.68 → 0.2.69

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.
@@ -67,9 +67,9 @@ import { collectFirstBuildAdvisory, formatFirstBuildAdvisory } from "./first-bui
67
67
  * (BAPI-606) redeems a pre-issued invite; `self-serve` (BAPI-618) mints one from an
68
68
  * email. Both then feed the SAME redemption protocol. Selection is pure and
69
69
  * deterministic from flags/env, except that a BARE interactive run is asked ONE
70
- * question (BAPI-626/708): `Welcome to Bridge - do you have a token yet?`, answered
71
- * `1` (yes) or `2` (no). Answering `2` is the self-serve route, which a first-time
72
- * user could not otherwise discover. Answering `1` opens a single hidden,
70
+ * question (BAPI-626/708/1448): `Email (or press Enter if you already have a
71
+ * token): `. A valid email is the self-serve route, which a first-time user could
72
+ * not otherwise discover. A blank answer opens a single hidden,
73
73
  * CREDENTIAL-AGNOSTIC token prompt and the route is then inferred from the pasted
74
74
  * value's SHAPE — the user is never asked to pre-classify their own credential,
75
75
  * because the shape checks below do that with no probe request. Either need-key
@@ -155,7 +155,7 @@ import { MCP_HOST_TARGETS, HOST_PLATFORM_ORDER, allHostTargets, agentForPlatform
155
155
  import { provisionHostTarget, createDefaultVendorProcessDeps, inspectBridgeApiProfileToken, } from "./mcp-host-config.js";
156
156
  import { adaptBridgeEntryForHostTarget, } from "./mcp-host-entry-adapter.js";
157
157
  import { MCP_SERVER_NAME, MCP_PACKAGE_NAME, resolveRegistrationKey, } from "./mcp-identity.js";
158
- import { DUPLICATE_REGISTRATION_GUIDANCE } from "./launcher-config-inspection.js";
158
+ import { DUPLICATE_REGISTRATION_GUIDANCE, discoverClaudeCodeEffectivePin } from "./launcher-config-inspection.js";
159
159
  import { codexHome, configureBridgeCodexProfile, inspectBridgeCodexProfile } from "./codex-bridge-profile.js";
160
160
  import { recordInstalledConductorCapability, recordInstalledExecutorServiceUnit, recordInstalledProjectArtifact, writeMcpInstallState, } from "./mcp-install-state.js";
161
161
  // BAPI-1207: opt-in cross-platform agent notes (AGENTS.md / CLAUDE.md).
@@ -193,7 +193,7 @@ import { resolveStartTicketsRepoName } from "./start-tickets-repo.js";
193
193
  import { upsertBapiCredential, getPrimaryCredentialStorePath, prepareBootstrapPendingCredential, repointBootstrapPendingCredential, promoteBootstrapPendingCredential, lookupSelfServeBootstrapPendingCredential, prepareSelfServeBootstrapChallenge as prepareSelfServeBootstrapChallengeCredential, discardBootstrapPendingCredential, getBootstrapPendingTarget, resolveBapiCredentials, } from "./credential-store.js";
194
194
  // BAPI-631: the optional GitHub connect offer reuses the standalone command's flow and
195
195
  // API primitives verbatim — no duplicated polling, browser, or picker logic here.
196
- import { fetchGithubConfigurationState } from "./connect-github-api.js";
196
+ import { fetchGithubConfigurationDetails, fetchGithubConfigurationState, } from "./connect-github-api.js";
197
197
  import { createDefaultConnectGithubDeps, runGithubConnectionFlow } from "./connect-github.js";
198
198
  import { fetchBitbucketConfigurationState } from "./connect-bitbucket-api.js";
199
199
  import { createDefaultConnectBitbucketDeps, runBitbucketConnectionFlow } from "./connect-bitbucket.js";
@@ -274,7 +274,7 @@ export const INSTALL_BRIDGE_AGENT_PROMPT = "Run /install-bridge now, with no arg
274
274
  "'Needs setup' — and each item in them carries display_name, description, how_to_use, and (in the " +
275
275
  "'Needs setup' section only) availability_text. Render each item's display_name, its description, " +
276
276
  "and its how_to_use verbatim when that value is a non-empty string; render availability_text " +
277
- "verbatim as the setup guidance for a 'Needs setup' item, and show NO availability caveat, " +
277
+ "verbatim as the setup guidance for a 'Needs setup' item — except when that section carries a non-empty string heading: print it verbatim as the section's sub-heading instead of its name and print NO item's availability_text; with no usable heading, render each availability_text as usual — and show NO availability caveat, " +
278
278
  "asterisk, or footnote on an 'Available now' item. An item's `kind` ('tool' or 'workflow') is " +
279
279
  "contract metadata only — render both kinds identically, with no label, icon, or separate list, " +
280
280
  "and never branch on it. Keep an empty section visible with a short neutral line rather than " +
@@ -303,7 +303,7 @@ export const INSTALL_BRIDGE_AGENT_PROMPT = "Run /install-bridge now, with no arg
303
303
  "short, plain-language sentence covering what is happening and why it matters to the person running " +
304
304
  "this install, rather than in internal shorthand. Narration you compose yourself must never name an " +
305
305
  "internal stage number, this command's contract version, a raw internal MCP tool identifier (like " +
306
- "get_install_manifest, apply_install_manifest, or invite_member), or a manifest field key (like " +
306
+ "get_install_manifest or apply_install_manifest), or a manifest field key (like " +
307
307
  "command_contract_version, next_step, or configured) — describe what is actually happening instead. " +
308
308
  "This restriction is scoped to narration you author: it does not apply to a user-invocable command " +
309
309
  "name you must give the human verbatim (/install-bridge, /learn-repository, connect-github, " +
@@ -317,11 +317,7 @@ export const INSTALL_BRIDGE_AGENT_PROMPT = "Run /install-bridge now, with no arg
317
317
  "duplicate. This silence applies ONLY to success and ONLY in the install-spawn context: a Stage 6 " +
318
318
  "FAILURE is still reported loudly and in full, and in JOIN MODE or a direct/manual /install-bridge " +
319
319
  "invocation Stage 6 reports its success exactly as it does today. " +
320
- "The install-spawn skip set is exactly Stage 8 and Stage 9. The command-owned Stage 11 " +
321
- "(invite teammates) is NOT in that skip set: it is independently gated and may run only when the " +
322
- "caller's role is admin AND customer_type is b2b AND an interactive response is available; " +
323
- "otherwise it is skipped silently. If Stage 11 mints a teammate key, show that plaintext key " +
324
- "exactly once and never repeat it in any summary, retry, or diagnostic. " +
320
+ "The install-spawn skip set is exactly Stage 8 and Stage 9. " +
325
321
  "Do NOT ask any indexing question, call parse_repository, mention /parse-repository, or claim " +
326
322
  "indexing has started or is pending — indexing is decided entirely by the server-side readiness " +
327
323
  "funnel with no visibility from this session, so say nothing about it at all. " +
@@ -394,9 +390,8 @@ export function getInstallBridgeUsage(baseUrl = DEFAULT_BAPI_BASE_URL) {
394
390
  "the next step the server returned — when it returns one.",
395
391
  "If the project is already configured it instead",
396
392
  "joins you to it without proposing or applying any changes and just shows the",
397
- "concise capability report plus that same conditional next step (a b2b admin is",
398
- "additionally offered a teammate-invite",
399
- "step). So not every run applies config fields. Indexing is never asked about —",
393
+ "concise capability report plus that same conditional next step.",
394
+ "So not every run applies config fields. Indexing is never asked about —",
400
395
  "it starts automatically once the repository reaches full parse readiness.",
401
396
  "",
402
397
  "Your API key is written to a project MCP config only when that file is safe: a",
@@ -406,17 +401,15 @@ export function getInstallBridgeUsage(baseUrl = DEFAULT_BAPI_BASE_URL) {
406
401
  "resolves your key from the credential store at runtime. A config file that",
407
402
  "cannot be parsed is left untouched, with manual-merge instructions printed.",
408
403
  "",
409
- "Run it bare — `install` with no flags — in a terminal and it asks",
410
- `\`${INSTALL_BRIDGE_KEY_SELECTOR_PROMPT}\` first, with two numbered choices:`,
411
- ...INSTALL_BRIDGE_ONBOARDING_CHOICES.map((choice) => ` ${choice}`),
412
- "There is NO default — pressing Enter selects nothing; you must type 1 or 2 (one",
413
- "blank or invalid answer re-prompts once, then exits with guidance). Answering 1",
414
- "prompts for an email and creates a brand-new Bridge workspace for you (the",
415
- "self-serve flow). Answering 2 opens a single hidden token prompt: paste whatever",
416
- "you were given and its SHAPE picks the route — a bootstrap invite (bapi_inv_…)",
417
- "is redeemed to CREATE a new project and mint your first admin key, while a",
418
- "Bridge API key connects to the project that already exists. You are",
419
- "never asked which kind of token you hold. That question is asked ONLY for a",
404
+ "Run it bare — `install` with no flags — in a terminal and it asks one question:",
405
+ `\`${INSTALL_BRIDGE_EMAIL_OR_TOKEN_PROMPT.trim()}\``,
406
+ "Type your email to create a brand-new Bridge workspace for you (the self-serve",
407
+ "flow). Press Enter if you already have a token: a single hidden token prompt",
408
+ "opens, and whatever you paste has its SHAPE pick the route — a bootstrap invite",
409
+ "(bapi_inv_…) is redeemed to CREATE a new project and mint your first admin key,",
410
+ "while a Bridge API key connects to the project that already exists. One",
411
+ "malformed answer re-prompts once, then exits with guidance. You are",
412
+ "never asked which kind of token you hold. The question is asked ONLY for a",
420
413
  "bare interactive run: passing ANY flag, setting BAPI_API_KEY, or running without an interactive",
421
414
  "terminal keeps the existing deterministic behavior and no prompt.",
422
415
  "",
@@ -532,9 +525,11 @@ export function getInstallBridgeUsage(baseUrl = DEFAULT_BAPI_BASE_URL) {
532
525
  " --tools claude-code,codex are accepted. On an",
533
526
  " interactive terminal WITHOUT this flag you get a",
534
527
  " numbered picker: enter one or more tool numbers,",
535
- " comma-separated (e.g. 1,3), and press Enter once —",
536
- " no tool is pre-selected (not even Claude Code) and",
537
- " you must select at least one. A non-interactive",
528
+ " comma-separated (e.g. 1,3), and press Enter once.",
529
+ " Tools Bridge detects on this machine are pre-ticked:",
530
+ " a bare Enter accepts them, and typed numbers",
531
+ " replace them. With none detected you must select",
532
+ " at least one. A non-interactive",
538
533
  " (non-TTY) run without",
539
534
  " --tools writes the legacy automatic set (Claude",
540
535
  " Code plus any detected Cursor / Copilot VS Code)",
@@ -563,6 +558,8 @@ export function getInstallBridgeUsage(baseUrl = DEFAULT_BAPI_BASE_URL) {
563
558
  " session — it always wins, and there is NO Claude",
564
559
  " default. Without it the launch tool is derived",
565
560
  " from your selection: Claude Code opens `claude`.",
561
+ " With more than one launchable tool selected, the",
562
+ " first one is used and no question is asked.",
566
563
  " Cursor is NOT auto-opened (its cursor-agent CLI",
567
564
  " first-run workspace-trust prompt collapses a spawned",
568
565
  " session); selecting Cursor writes .cursor/mcp.json",
@@ -1147,12 +1144,6 @@ export async function offerBitbucketConnection(repoName, baseUrl, deps, log, ses
1147
1144
  `Bitbucket later with 'npx -y ${MCP_PACKAGE_NAME}@latest connect-bitbucket --repo ${repoName}'.`);
1148
1145
  }
1149
1146
  }
1150
- /**
1151
- * The exact agent-notes consent-prompt prefix, shared with tests so the copy
1152
- * cannot drift. Mirrors {@link INSTALL_BRIDGE_LAUNCH_CONSENT_PROMPT_PREFIX}: the
1153
- * prefix is pinned, the platform tail varies with the selection.
1154
- */
1155
- export const INSTALL_BRIDGE_AGENT_NOTES_PROMPT_PREFIX = "Write guidance so ";
1156
1147
  /**
1157
1148
  * Describe the selected notes platforms for the offer copy: a naturally joined
1158
1149
  * subject and whether it is plural (so a verb can agree). `platformLabels` are
@@ -1175,68 +1166,30 @@ function joinLabelsWithAnd(labels) {
1175
1166
  return `${labels.slice(0, -1).join(", ")}, and ${labels[labels.length - 1]}`;
1176
1167
  }
1177
1168
  /**
1178
- * The default-Yes agent-notes consent prompt (BAPI-1435; payoff-led, platform-named
1179
- * copy per the BAPI-1239 install-issue #4 follow-up). Leads with WHY — teaching
1180
- * the agent how to use Bridge's MCP tools — and NAMES the selected platform(s)
1181
- * rather than the old generic "cross-platform" phrasing.
1182
- */
1183
- export function buildAgentNotesPrompt(platformLabels) {
1184
- const { subject, plural } = describeAgentNotesPlatforms(platformLabels);
1185
- return `${INSTALL_BRIDGE_AGENT_NOTES_PROMPT_PREFIX}${subject} know${plural ? "" : "s"} how to use the Bridge MCP? Enter adds guidance for ${subject}. [Y/n]: `;
1186
- }
1187
- /**
1188
- * The offer-context lines printed immediately before the agent-notes prompt
1189
- * (BAPI-1207). Leads with the payoff — teaching the named platform(s) WHEN to
1190
- * reach for Bridge's MCP tools — then states the marker-scoped, non-destructive
1191
- * contract as a secondary line, mirroring the GitHub/Bitbucket offers' "explain
1192
- * before asking" convention.
1193
- */
1194
- export function buildAgentNotesOfferContext(targets, platformLabels) {
1195
- const { subject, plural } = describeAgentNotesPlatforms(platformLabels);
1196
- return [
1197
- "",
1198
- `Step 4c — optional: Bridge MCP guidance for ${subject}.`,
1199
- ` Write guidance so ${subject} know${plural ? "" : "s"} when to reach for Bridge's MCP tools`,
1200
- " during a task — for project standards, a second opinion, a council, or a ticket review.",
1201
- ` It writes a Bridge-owned region to ${targets.join(" and ")}; only that marked region is`,
1202
- " created or refreshed — any existing content, inside or outside it, is otherwise preserved.",
1203
- ];
1204
- }
1205
- function agentGuidancePromptConsents(result) {
1206
- if (result?.kind !== "answer" || typeof result.value !== "string")
1207
- return false;
1208
- const value = result.value.trim().toLowerCase();
1209
- return value === "" || value === "y" || value === "yes";
1210
- }
1211
- /**
1212
- * Offer to create or refresh the cross-platform agent notes, if the selected
1213
- * platforms resolve to at least one writable target (BAPI-1207).
1169
+ * Decide whether to create or refresh the cross-platform agent notes, if the
1170
+ * selected platforms resolve to at least one writable target (BAPI-1207).
1214
1171
  *
1215
- * Entirely best-effort and non-destructive, mirroring
1216
- * {@link offerGithubConnection}: by the time this runs the install itself is
1217
- * already complete and durable, so nothing here may fail the run. This
1218
- * function only resolves the CONSENT decision — the caller performs the
1219
- * actual write via {@link writeAgentNotes}, so a `--dry-run` (which never
1220
- * reaches this call) and the live path share exactly one write path.
1172
+ * BAPI-1448: an interactive run consents by default — the question is gone, with
1173
+ * the product sign-off recorded in the install notes. The write is still bounded to
1174
+ * Bridge's marked region (see {@link writeAgentNotes}) and reported afterwards with
1175
+ * a removal hint. A non-interactive run keeps `headless-skip`: it never writes
1176
+ * guidance on its own.
1177
+ *
1178
+ * Entirely best-effort and non-destructive: by the time this runs the install
1179
+ * itself is already complete and durable, so nothing here may fail the run. This
1180
+ * function only resolves the CONSENT decision — the caller performs the actual
1181
+ * write via {@link writeAgentNotes}, so a `--dry-run` (which never reaches this
1182
+ * call) and the live path share exactly one write path.
1221
1183
  */
1222
- export async function offerAgentGuidanceNotes(deps, log, targets, platformLabels) {
1184
+ export function offerAgentGuidanceNotes(deps, targets) {
1223
1185
  if (targets.length === 0)
1224
1186
  return { kind: "not-applicable" };
1225
- // No prompt surface → no offer. Never assume consent on a non-interactive run.
1226
- if (!deps.isTTY || !deps.promptGuidanceLine)
1187
+ if (!deps.isTTY)
1227
1188
  return { kind: "headless-skip" };
1228
- try {
1229
- for (const line of buildAgentNotesOfferContext(targets, platformLabels))
1230
- log(line);
1231
- const answer = await deps.promptGuidanceLine(buildAgentNotesPrompt(platformLabels));
1232
- return agentGuidancePromptConsents(answer) ? { kind: "consented" } : { kind: "declined" };
1233
- }
1234
- catch {
1235
- // The install is already durable; a failure here is never fatal to it.
1236
- deps.errorLog("Agent guidance skipped: the prompt could not be read; no guidance files were changed.");
1237
- return { kind: "prompt-error" };
1238
- }
1189
+ return { kind: "consented" };
1239
1190
  }
1191
+ /** The one-line removal hint printed after guidance was actually written (BAPI-1448). */
1192
+ export const INSTALL_BRIDGE_AGENT_NOTES_REMOVAL_HINT = "To remove it, delete the lines from <!-- BEGIN BRIDGE MCP GUIDANCE --> to <!-- END BRIDGE MCP GUIDANCE -->.";
1240
1193
  /**
1241
1194
  * One secret-free summary line for the final install output (BAPI-1207). Never
1242
1195
  * reveals guidance content, file bytes, or a credential — only which files
@@ -1273,6 +1226,47 @@ export function buildAgentNotesSummaryLine(offer, result, writeFailed, unsupport
1273
1226
  parts.push(`failed for ${failed.join(", ")}`);
1274
1227
  return `Agent notes: ${parts.join("; ")}${unsupportedNote}.`;
1275
1228
  }
1229
+ const OPTIMIZATION_POSTURE_SUMMARY_LABELS = {
1230
+ quality: "Quality",
1231
+ speed: "Speed",
1232
+ maximize_subscription: "Token Efficiency",
1233
+ };
1234
+ /**
1235
+ * The aligned "defaults Bridge chose" block printed once, right before the launch
1236
+ * consent question. Pure: it formats what the run already knows and performs no I/O.
1237
+ *
1238
+ * Layout: a header, then two rows of two cells. Both rows are indented two spaces and
1239
+ * the left cells are padded to the longer left cell plus three spaces, so the right
1240
+ * cells line up. The Ticket keys cell is omitted entirely when the server supplied no
1241
+ * valid prefix; the prefix is printed as given and never derived locally.
1242
+ */
1243
+ export function buildInstallBridgeDefaultsSummary(input) {
1244
+ const optimization = input.persistedPosture
1245
+ ? OPTIMIZATION_POSTURE_SUMMARY_LABELS[input.persistedPosture]
1246
+ : input.isNewProject
1247
+ ? "Quality"
1248
+ : "unchanged";
1249
+ const guidanceLabel = input.claudeCodeSelected ? "Claude Code guidance" : "Agent guidance";
1250
+ const guidanceValue = input.guidance.written.length > 0
1251
+ ? `added to ${input.guidance.written.join(", ")}`
1252
+ : input.guidance.unchanged.length > 0
1253
+ ? "already up to date"
1254
+ : "not written";
1255
+ const leftTop = `Project name: ${input.displayName}`;
1256
+ const leftBottom = `Optimization: ${optimization}`;
1257
+ const rightTop = input.ticketPrefix
1258
+ ? `Ticket keys: ${input.ticketPrefix}-1, ${input.ticketPrefix}-2, …`
1259
+ : undefined;
1260
+ const rightBottom = `${guidanceLabel}: ${guidanceValue}`;
1261
+ const width = Math.max(leftTop.length, leftBottom.length) + 3;
1262
+ const rowTop = rightTop === undefined ? ` ${leftTop}` : ` ${leftTop.padEnd(width)}${rightTop}`;
1263
+ const rowBottom = ` ${leftBottom.padEnd(width)}${rightBottom}`;
1264
+ return [
1265
+ "Bridge chose these defaults — change them later on the Setup page, or ask your agent:",
1266
+ rowTop,
1267
+ rowBottom,
1268
+ ].join("\n");
1269
+ }
1276
1270
  /**
1277
1271
  * Echoed single-line prompt on stderr (used for repo confirmation / value).
1278
1272
  * Exported so `index.ts`'s `--init` can share the exact same prompt mechanics
@@ -1296,56 +1290,6 @@ export function promptLineViaReadline(promptText, input = process.stdin, output
1296
1290
  });
1297
1291
  });
1298
1292
  }
1299
- /** Observe CR/LF before readline, counting split CRLF as one submission. */
1300
- export function promptGuidanceLineViaReadline(promptText, input = process.stdin, output = process.stderr) {
1301
- return new Promise((resolve) => {
1302
- const readable = input;
1303
- if (readable.readableEnded || readable.destroyed) {
1304
- resolve({ kind: "eof" });
1305
- return;
1306
- }
1307
- let terminators = 0;
1308
- let previousCR = false;
1309
- let settled = false;
1310
- let rl;
1311
- const observe = (chunk) => {
1312
- for (const char of chunk.toString()) {
1313
- if (char === "\r")
1314
- terminators++;
1315
- else if (char === "\n" && !previousCR)
1316
- terminators++;
1317
- previousCR = char === "\r";
1318
- }
1319
- };
1320
- const finish = (result) => {
1321
- if (settled)
1322
- return;
1323
- settled = true;
1324
- input.removeListener("data", observe);
1325
- input.removeListener("end", eof);
1326
- input.removeListener("close", eof);
1327
- input.removeListener("error", eof);
1328
- rl?.removeListener("close", eof);
1329
- rl?.close();
1330
- resolve(result);
1331
- };
1332
- const eof = () => finish({ kind: "eof" });
1333
- input.prependListener("data", observe);
1334
- input.once("end", eof);
1335
- input.once("close", eof);
1336
- input.once("error", eof);
1337
- rl = readline.createInterface({ input, output });
1338
- rl.once("close", eof);
1339
- if (settled || readable.readableEnded || readable.destroyed) {
1340
- rl.close();
1341
- eof();
1342
- return;
1343
- }
1344
- rl.question(promptText, (value) => {
1345
- finish(terminators > 0 ? { kind: "answer", value } : { kind: "eof" });
1346
- });
1347
- });
1348
- }
1349
1293
  /**
1350
1294
  * Default pre-warm spawner (BAPI-451 W3). Spawns array-based (`shell: false`) so
1351
1295
  * no argument is ever shell-interpreted, ignores all child stdio (node-gyp /
@@ -1461,8 +1405,9 @@ export function createDefaultInstallBridgeDeps() {
1461
1405
  randomBytes: (size) => cryptoRandomBytes(size),
1462
1406
  promptSecret: isTTY ? promptSecretViaReadline : undefined,
1463
1407
  promptLine: isTTY ? promptLineViaReadline : undefined,
1464
- promptGuidanceLine: isTTY ? promptGuidanceLineViaReadline : undefined,
1465
- promptMultiSelect: isTTY ? promptMultiSelectViaReadline : undefined,
1408
+ promptMultiSelect: isTTY
1409
+ ? (promptText, options, preselected) => promptMultiSelectViaReadline(promptText, options, process.stdin, process.stderr, preselected)
1410
+ : undefined,
1466
1411
  vendor: createDefaultVendorProcessDeps(spawn),
1467
1412
  fetch: productionFetch,
1468
1413
  resolveRepoViaServer: (baseUrl, apiKey) => resolveRepoViaServer(productionFetch, baseUrl, apiKey),
@@ -2148,7 +2093,7 @@ export function buildInstallBridgeMaintenanceHint() {
2148
2093
  "Persistent host registrations remain pinned until an explicit upgrade; the editor may need to reconnect to use the new server.",
2149
2094
  ];
2150
2095
  }
2151
- export async function resolveSignupEmail(options, deps, baseUrl = DEFAULT_BAPI_BASE_URL) {
2096
+ export async function resolveSignupEmail(options, deps, baseUrl = DEFAULT_BAPI_BASE_URL, onboarding) {
2152
2097
  // BAPI-757 (R-3 / AC-9): self-serve signup now REQUIRES an interactive terminal,
2153
2098
  // and the check runs FIRST — before `--email` and before `BAPI_SIGNUP_EMAIL` —
2154
2099
  // so an unattended run fails before any request is made, no verification email
@@ -2159,6 +2104,13 @@ export async function resolveSignupEmail(options, deps, baseUrl = DEFAULT_BAPI_B
2159
2104
  if (!deps.isTTY || !deps.promptLine) {
2160
2105
  return { ok: false, error: INSTALL_BRIDGE_SIGNUP_REQUIRES_TTY };
2161
2106
  }
2107
+ // BAPI-1448: an email already answered at the merged entry prompt is consumed the
2108
+ // way `--email` is, with no second question and no second privacy notice.
2109
+ if (onboarding) {
2110
+ if (!onboarding.privacyNoticeShown)
2111
+ deps.log(buildInstallBridgeSignupPrivacyNotice(baseUrl));
2112
+ return { ok: true, value: onboarding.email };
2113
+ }
2162
2114
  deps.log(buildInstallBridgeSignupPrivacyNotice(baseUrl));
2163
2115
  if (typeof options.email === "string" && options.email.trim().length > 0) {
2164
2116
  return { ok: true, value: options.email.trim() };
@@ -2222,49 +2174,24 @@ export function resolveInstallBridgeOnboardingBranch(options, env) {
2222
2174
  return { kind: "have-key" };
2223
2175
  }
2224
2176
  /**
2225
- * The bare-TTY onboarding question (BAPI-708, Part A), replacing the BAPI-667/687
2226
- * three-option chooser.
2227
- *
2228
- * The three-option form asked the user to make a distinction the CODE can already
2229
- * make for itself: whether the string in their clipboard is an invite or an API
2230
- * key. `classifyEnteredCredential` + `isHighEntropyApiKeyShape` decide that from
2231
- * SHAPE alone, with no probe request — so the only thing a first-time user
2232
- * genuinely knows, and the only thing worth asking, is whether they have a token
2233
- * at all. Hence a yes/no question, with the route inferred afterwards.
2234
- *
2235
- * Interaction contract is unchanged from BAPI-667/687 and remains load-bearing:
2236
- * render the heading and the numbered options exactly once, accept ONE strict
2237
- * line, no default (a bare Enter selects nothing), bounded to a single re-prompt
2238
- * after one hint, and EOF resolves as an empty answer so it can never hang.
2239
- */
2240
- export const INSTALL_BRIDGE_KEY_SELECTOR_PROMPT = "Welcome to Bridge - do you have a token yet?";
2241
- /**
2242
- * The question's numbered options, in render order. Index + 1 is the accepted
2243
- * token. BAPI-1171 swapped the order so the self-serve, need-a-token path is
2244
- * option 1 — first-time signup is the common case and now leads.
2245
- *
2246
- * "Token" is deliberately credential-agnostic: an invite and an API key are both
2247
- * tokens as far as this question is concerned, and answering "2" leads to ONE
2248
- * hidden prompt whose value selects the route by shape.
2177
+ * The bare-TTY entry prompt (BAPI-1448). One question replaces the BAPI-708 "do you
2178
+ * have a token yet? 1/2" chooser: a typed email selects self-serve signup, and a
2179
+ * blank answer selects the have-key branch, whose value is read ONLY through the
2180
+ * hidden `Paste your Bridge token (input hidden): ` prompt downstream.
2249
2181
  */
2250
- export const INSTALL_BRIDGE_ONBOARDING_CHOICES = [
2251
- "1. I need a token",
2252
- "2. I already have a token",
2253
- ];
2254
- /** The single-line answer prompt drawn after the options (no bracketed default). */
2255
- export const INSTALL_BRIDGE_ONBOARDING_CHOICE_PROMPT = "Enter 1 or 2: ";
2256
- /** The one hint line printed after a blank/invalid answer, before the single re-prompt. */
2257
- export const INSTALL_BRIDGE_ONBOARDING_CHOICE_HINT = "Enter 1 or 2.";
2182
+ export const INSTALL_BRIDGE_EMAIL_OR_TOKEN_PROMPT = "Email (or press Enter if you already have a token): ";
2183
+ /** The one hint line printed after a non-blank malformed answer, before the single re-prompt. */
2184
+ export const INSTALL_BRIDGE_EMAIL_OR_TOKEN_HINT = "Enter an email address, or press Enter if you already have a token.";
2258
2185
  /**
2259
2186
  * The one place the "no usable credential" copy lives (BAPI-667, U1-3).
2260
2187
  *
2261
- * Centralized because the chooser failure and {@link resolveApiKey}'s two failure
2188
+ * Centralized because the entry-prompt failure and {@link resolveApiKey}'s two failure
2262
2189
  * branches previously drifted: only one of them mentioned self-serve, and one
2263
2190
  * advised "try the hidden prompt again" — false, because the process has already
2264
2191
  * exited by the time the user reads it. All three now name the SAME three routes.
2265
2192
  */
2266
- export const INSTALL_BRIDGE_ENTRY_ROUTES_ADVICE = "Re-run install: on a bare interactive run answer 1 to request a new token or 2 if you " +
2267
- "already have one (an invite or an API key — either one is accepted at the hidden prompt). " +
2193
+ export const INSTALL_BRIDGE_ENTRY_ROUTES_ADVICE = "Re-run install: on a bare interactive run, type your email to sign up, or press Enter if you " +
2194
+ "already have a token (an invite or an API key — either one is accepted at the hidden prompt). " +
2268
2195
  "Non-interactively, pass --api-key <key> (or set BAPI_API_KEY) for an " +
2269
2196
  "existing project, --invite if you were sent an invite token, or --email <addr> to sign up.";
2270
2197
  /**
@@ -2312,7 +2239,7 @@ export const INSTALL_BRIDGE_ENTRY_ROUTES_ADVICE = "Re-run install: on a bare int
2312
2239
  * key-on-invite exits non-zero) is preserved BY CONSTRUCTION. Do not "unify" the
2313
2240
  * two.
2314
2241
  */
2315
- export async function resolveInstallBridgeOnboardingBranchForRun(options, deps, argv) {
2242
+ export async function resolveInstallBridgeOnboardingBranchForRun(options, deps, argv, baseUrl = DEFAULT_BAPI_BASE_URL) {
2316
2243
  const branch = resolveInstallBridgeOnboardingBranch(options, deps.env);
2317
2244
  // An explicit need-key intent is already unambiguous — never re-ask it.
2318
2245
  if (branch.kind === "need-key")
@@ -2324,38 +2251,35 @@ export async function resolveInstallBridgeOnboardingBranchForRun(options, deps,
2324
2251
  }
2325
2252
  const promptLine = deps.promptLine;
2326
2253
  try {
2327
- // Render the heading + options EXACTLY once; a re-prompt after an invalid
2328
- // answer re-asks the question without redrawing the list (BAPI-663 contract).
2329
- deps.log(INSTALL_BRIDGE_KEY_SELECTOR_PROMPT);
2330
- for (const choice of INSTALL_BRIDGE_ONBOARDING_CHOICES)
2331
- deps.log(choice);
2254
+ // The answer may be an email, so the privacy notice comes first and prints once.
2255
+ deps.log(buildInstallBridgeSignupPrivacyNotice(baseUrl));
2332
2256
  // Bounded to two reads total: the first answer, then ONE re-prompt after a
2333
2257
  // single hint line. EOF resolves as an empty answer (promptLineViaReadline
2334
- // resolves "" on close), so it costs one re-prompt and then fails cleanly —
2335
- // it can never hang or silently select a branch.
2258
+ // resolves "" on close), which selects the have-key branch rather than hanging.
2336
2259
  for (let attempt = 0; attempt < 2; attempt += 1) {
2337
- const answer = (await promptLine(INSTALL_BRIDGE_ONBOARDING_CHOICE_PROMPT)).trim();
2338
- // BAPI-708 (A-1): the accepted tokens are exactly 1/2 (no letters, no
2339
- // aliases), preserving the one-strict-line contract. BAPI-1171 swapped the
2340
- // order so "1" is the self-serve need-key branch and "2" routes to the
2341
- // have-key branch's credential-agnostic prompt, whose value is classified by
2342
- // SHAPE — so a user holding an invite still lands in the redemption protocol
2343
- // without ever having been asked which kind of token they hold.
2344
- if (answer === "1") {
2260
+ const answer = String((await promptLine(INSTALL_BRIDGE_EMAIL_OR_TOKEN_PROMPT)) ?? "").trim();
2261
+ // Blank (or EOF): the user has a token; it is read only by the hidden prompt.
2262
+ if (answer.length === 0) {
2263
+ return { ok: true, branch: { kind: "have-key" }, credentialIntent: "inferred" };
2264
+ }
2265
+ // A typed value is accepted ONLY as a signup email. It is never classified as a
2266
+ // credential, logged, or persisted here; a token-shaped value fails the shape
2267
+ // check and gets the hint.
2268
+ if (INSTALL_BRIDGE_SIGNUP_EMAIL_SHAPE.test(answer)) {
2345
2269
  return {
2346
2270
  ok: true,
2347
2271
  branch: { kind: "need-key", method: "self-serve" },
2348
2272
  credentialIntent: "inferred",
2273
+ onboardingEmail: answer,
2274
+ privacyNoticeShown: true,
2349
2275
  };
2350
2276
  }
2351
- if (answer === "2")
2352
- return { ok: true, branch: { kind: "have-key" }, credentialIntent: "inferred" };
2353
2277
  if (attempt === 0)
2354
- deps.log(INSTALL_BRIDGE_ONBOARDING_CHOICE_HINT);
2278
+ deps.log(INSTALL_BRIDGE_EMAIL_OR_TOKEN_HINT);
2355
2279
  }
2356
2280
  return {
2357
2281
  ok: false,
2358
- error: `No option was selected. ${INSTALL_BRIDGE_ENTRY_ROUTES_ADVICE}`,
2282
+ error: `No email or token choice was made. ${INSTALL_BRIDGE_ENTRY_ROUTES_ADVICE}`,
2359
2283
  };
2360
2284
  }
2361
2285
  catch {
@@ -2464,22 +2388,31 @@ const SELECTION_TOKEN_PATTERN = /^[1-9][0-9]*$/;
2464
2388
  * Interactive single-shot numbered picker on stderr (TTY only). Renders the
2465
2389
  * heading and each option exactly once, then accepts ONE strict
2466
2390
  * comma-separated line of one-based numbers and resolves immediately — there
2467
- * is no preselected/toggle state and no separate confirmation step (BAPI-663).
2391
+ * is no toggle state and no separate confirmation step (BAPI-663).
2392
+ *
2393
+ * BAPI-1448: when `preselected` names at least one offered option, those rows
2394
+ * render as `[x]` (the rest as `[ ]`) and a blank line accepts the ticked set.
2395
+ * Typed numbers REPLACE the ticked set; they never toggle it. With nothing
2396
+ * pre-ticked the original behaviour is unchanged: a blank line is rejected with
2397
+ * "Select at least one tool."
2468
2398
  *
2469
- * A blank line or a line containing any invalid token (non-numeric, zero,
2470
- * signed, leading-zero, decimal, out-of-range) is rejected atomically — the
2471
- * whole line is discarded, one error line is printed, and the picker
2472
- * re-prompts without re-rendering the option list. A valid line is
2473
- * deduplicated and returned in `options` order regardless of entry order.
2399
+ * A line containing any invalid token (non-numeric, zero, signed,
2400
+ * leading-zero, decimal, out-of-range) is rejected atomically — the whole line
2401
+ * is discarded, one error line is printed, and the picker re-prompts without
2402
+ * re-rendering the option list, so the ticks stay as first shown. A valid line
2403
+ * is deduplicated and returned in `options` order regardless of entry order.
2474
2404
  *
2475
2405
  * Resolves to `[]` on EOF / synchronous close before any line has been
2476
2406
  * committed, so a readline regression can never hang. Returns `[]`
2477
2407
  * immediately, without creating a readline interface, when `options` is
2478
2408
  * empty.
2479
2409
  */
2480
- export function promptMultiSelectViaReadline(promptText, options, input = process.stdin, output = process.stderr) {
2410
+ export function promptMultiSelectViaReadline(promptText, options, input = process.stdin, output = process.stderr, preselected = []) {
2481
2411
  if (options.length === 0)
2482
2412
  return Promise.resolve([]);
2413
+ // Canonical option order, restricted to options actually offered.
2414
+ const ticked = options.filter((o) => preselected.includes(o.id)).map((o) => o.id);
2415
+ const hasTicks = ticked.length > 0;
2483
2416
  return new Promise((resolve) => {
2484
2417
  const rl = readline.createInterface({ input, output });
2485
2418
  let settled = false;
@@ -2496,13 +2429,20 @@ export function promptMultiSelectViaReadline(promptText, options, input = proces
2496
2429
  rl.on("close", () => finish([]));
2497
2430
  output.write(`\n${promptText}\n`);
2498
2431
  options.forEach((opt, idx) => {
2499
- output.write(` ${idx + 1}. ${opt.label}\n`);
2432
+ const box = hasTicks ? `${ticked.includes(opt.id) ? "[x]" : "[ ]"} ` : "";
2433
+ output.write(` ${idx + 1}. ${box}${opt.label}\n`);
2500
2434
  });
2501
- output.write("Enter the number(s) of the tools you use (e.g. 1,3), then Enter.\n");
2435
+ output.write(hasTicks
2436
+ ? "Press Enter to use the ticked tools, or type the numbers you use (e.g. 1,3).\n"
2437
+ : "Enter the number(s) of the tools you use (e.g. 1,3), then Enter.\n");
2502
2438
  const prompt = () => {
2503
2439
  rl.question("> ", (answer) => {
2504
2440
  const trimmed = answer.trim();
2505
2441
  if (trimmed.length === 0) {
2442
+ if (hasTicks) {
2443
+ finish(ticked);
2444
+ return;
2445
+ }
2506
2446
  output.write("Select at least one tool.\n");
2507
2447
  prompt();
2508
2448
  return;
@@ -2553,10 +2493,6 @@ export function optimizationPostureSubscriptionNounPhrase(primaryPlatform) {
2553
2493
  const mapped = primaryPlatform ? OPTIMIZATION_POSTURE_SUBSCRIPTION_NAMES[primaryPlatform] : undefined;
2554
2494
  return mapped ? `${mapped} subscription` : "coding subscription";
2555
2495
  }
2556
- /** Option three's exact wording, platform-aware (BAPI-1221 Step 2). */
2557
- function buildOptimizationPostureOptionThree(primaryPlatform) {
2558
- return `Get the most out of your ${optimizationPostureSubscriptionNounPhrase(primaryPlatform)}?`;
2559
- }
2560
2496
  /** The neutral single-line correction reprompted for any malformed answer. */
2561
2497
  const OPTIMIZATION_POSTURE_CORRECTION = "Enter a single number from 1 to 3.";
2562
2498
  /**
@@ -2572,20 +2508,21 @@ const OPTIMIZATION_POSTURE_CORRECTION = "Enter a single number from 1 to 3.";
2572
2508
  const OPTIMIZATION_POSTURE_MAX_CORRECTIONS = 20;
2573
2509
  /**
2574
2510
  * Render the one-time posture menu and its input instruction. Exactly three
2575
- * vertically ordered numbered options; option three is platform-aware. Plain,
2576
- * wrap-safe text — no color-only state, icons, or decorative framing.
2511
+ * vertically ordered numbered options; option three names the user's own
2512
+ * subscription. Plain, wrap-safe text (every line within 80 columns) — no
2513
+ * color-only state, icons, or decorative framing.
2577
2514
  */
2578
2515
  function buildOptimizationPostureMenu(primaryPlatform) {
2579
- return ("How should Bridge optimize its work on this repository?\n" +
2580
- " 1. Quality — fuller planning and review policy.\n" +
2581
- " 2. Speed — a combined planning pass and normally one review round.\n" +
2582
- ` 3. ${buildOptimizationPostureOptionThree(primaryPlatform)} — leaner planning/review defaults\n` +
2583
- " and lower harness model tiers where supported.\n" +
2584
- "These are active defaults, subject to explicit policy/model overrides.\n" +
2585
- `Your ${optimizationPostureSubscriptionNounPhrase(primaryPlatform)} is separate from Bridge credits.\n` +
2586
- "Enter keeps an existing saved preference; projects without one use Quality.\n" +
2587
- "You can change this later with /teach-bridge.\n" +
2588
- `${OPTIMIZATION_POSTURE_CORRECTION} `);
2516
+ return ("How should Bridge balance quality and cost on this project?\n" +
2517
+ " 1. Quality — Bridge plans and reviews each ticket in full depth; slowest and\n" +
2518
+ " uses the most credits, but catches the most problems.\n" +
2519
+ " 2. Speed — Bridge plans in one pass and reviews once; faster and cheaper, but\n" +
2520
+ " may miss things on some tickets.\n" +
2521
+ " 3. Token Efficiency — Bridge will use Speed settings and hand simpler tickets\n" +
2522
+ " to smaller models more often, which stretches your\n" +
2523
+ ` ${optimizationPostureSubscriptionNounPhrase(primaryPlatform)} the furthest.\n` +
2524
+ "Press Enter to keep Quality (or your saved choice).\n" +
2525
+ "Enter 1, 2, or 3: ");
2589
2526
  }
2590
2527
  /**
2591
2528
  * The purpose-built, strict single-select optimization-posture picker
@@ -2675,7 +2612,7 @@ function optimizationPostureConfirmationPhrase(value, primaryPlatform) {
2675
2612
  }
2676
2613
  /** Printed immediately after a successful posture write, on its own line. */
2677
2614
  const OPTIMIZATION_POSTURE_BLOG_URL = "https://bridgegpt.ai/blog/how-we-optimize-your-setup";
2678
- const OPTIMIZATION_POSTURE_REMEDIATION = "Update it later with /teach-bridge or the optimization_posture setting in the web UI.";
2615
+ const OPTIMIZATION_POSTURE_REMEDIATION = "Change it later by running /teach-bridge in your agent.";
2679
2616
  /**
2680
2617
  * The generic, secret-free fail-open message shared by every non-success,
2681
2618
  * non-forbidden outcome — an interactive no-answer/prompt-exception (no PUT
@@ -2709,8 +2646,9 @@ function reportOptimizationPostureOutcome(log, result, value, primaryPlatform) {
2709
2646
  * Resolve the selected host platforms with strict precedence (BAPI-635):
2710
2647
  * 1. explicit `--tools` (including an explicit EMPTY selection — never falls
2711
2648
  * back to detection),
2712
- * 2. interactive multi-select on a TTY (AC-2: no row pre-selected — the picker
2713
- * starts empty and requires an explicit ≥1 selection),
2649
+ * 2. interactive multi-select on a TTY, pre-ticked with the detected tools
2650
+ * (BAPI-1448 reverses the BAPI-635 AC-2 empty picker; with nothing detected
2651
+ * the picker still requires an explicit ≥1 selection),
2714
2652
  * 3. the legacy non-TTY automatic set: Claude Code plus only the currently
2715
2653
  * detected Copilot VS Code and Cursor automatic targets (never Codex just
2716
2654
  * because a global directory exists).
@@ -2720,11 +2658,12 @@ export async function resolveSelectedHostPlatforms(deps, options) {
2720
2658
  if (options.tools !== undefined) {
2721
2659
  return options.tools;
2722
2660
  }
2723
- // 2. Interactive single-shot numbered picker on a TTY. No row is pre-selected
2724
- // — the picker has no default state and requires an explicit selection.
2661
+ // 2. Interactive single-shot numbered picker on a TTY, seeded with the locally
2662
+ // detected tools (BAPI-1448): Enter accepts them, typed numbers replace them.
2725
2663
  if (deps.isTTY && deps.promptMultiSelect) {
2726
2664
  const optionList = allHostTargets().map((t) => ({ id: t.id, label: t.label }));
2727
- const chosen = await deps.promptMultiSelect(INSTALL_BRIDGE_TOOL_SELECTOR_PROMPT, optionList);
2665
+ const detected = detectDefaultPlatforms(await buildDetectionContext(deps));
2666
+ const chosen = await deps.promptMultiSelect(INSTALL_BRIDGE_TOOL_SELECTOR_PROMPT, optionList, detected);
2728
2667
  return HOST_PLATFORM_ORDER.filter((id) => chosen.includes(id));
2729
2668
  }
2730
2669
  // 3. Legacy non-TTY automatic set: Claude + detected Cursor / Copilot VS Code.
@@ -2842,42 +2781,6 @@ export function toolLabelForLaunchAgent(agent) {
2842
2781
  const target = allHostTargets().find((t) => t.launchAgent === agent);
2843
2782
  return target?.label ?? UNKNOWN_LAUNCH_TOOL_LABEL;
2844
2783
  }
2845
- /**
2846
- * Interactive numbered chooser for when MORE THAN ONE launchable tool was selected
2847
- * (AC-11). Mirrors the connect-github picker: prints each candidate by its host-tool
2848
- * label, asks for an exact numeric choice with NO default, validates the range, and
2849
- * returns the chosen agent. Returns `null` — never a silent first-candidate default —
2850
- * for every unavailable/invalid path: non-TTY, a missing `promptLine` seam, blank/EOF
2851
- * input, an out-of-range or malformed answer, or a prompt that throws. Single-purpose:
2852
- * it must run only AFTER the multi-select promise has resolved and closed its readline
2853
- * interface so two readers never contend for stdin.
2854
- */
2855
- export async function chooseInstallBridgeLaunchAgent(agents, deps) {
2856
- if (!deps.isTTY || !deps.promptLine)
2857
- return null;
2858
- const promptLine = deps.promptLine;
2859
- try {
2860
- deps.log("");
2861
- deps.log("More than one selected tool can host the configuration session:");
2862
- agents.forEach((agent, i) => {
2863
- deps.log(` ${String(i + 1).padStart(2, " ")}. ${toolLabelForLaunchAgent(agent)}`);
2864
- });
2865
- // No default: a stray Enter must not silently pick a tool.
2866
- const answer = (await promptLine(`Which tool should open? [1-${agents.length}]: `)).trim();
2867
- const index = Number(answer);
2868
- if (!/^\d+$/.test(answer) ||
2869
- !Number.isInteger(index) ||
2870
- index < 1 ||
2871
- index > agents.length) {
2872
- return null;
2873
- }
2874
- return agents[index - 1];
2875
- }
2876
- catch {
2877
- // Never surface caught exception text; an unavailable chooser is a no-spawn.
2878
- return null;
2879
- }
2880
- }
2881
2784
  /**
2882
2785
  * Shared, copy-pasteable "we didn't open a session for you" guidance so every
2883
2786
  * no-spawn path (no-launchable selection, non-TTY suppression, chooser failure,
@@ -4128,6 +4031,27 @@ export function buildSelfServeMintUrl(baseUrl) {
4128
4031
  export const SIGNUP_VERIFICATION_CODE_LENGTH = 6;
4129
4032
  /** The one prompt label. The E2E harness and the static contract both pin it. */
4130
4033
  export const INSTALL_BRIDGE_CODE_PROMPT = "Email verification code: ";
4034
+ /**
4035
+ * The address verification codes are sent from. Mirrors the server's
4036
+ * `DEFAULT_FROM_ADDRESS` in `api/library/email/provider.py`; keep the two in step.
4037
+ */
4038
+ export const INSTALL_BRIDGE_VERIFICATION_SENDER = "noreply@bridgegpt-api.com";
4039
+ /**
4040
+ * The shared lead-in printed immediately before every self-serve
4041
+ * {@link promptForVerificationCode} call (modern and legacy paths, fresh or resumed):
4042
+ * three unindented lines, then a blank line. It prints neither the prompt nor any
4043
+ * expiry or time-left text.
4044
+ *
4045
+ * BAPI-1449 (verification recovery) builds on this lead-in instead of adding its
4046
+ * own, and may later return the sender from the request endpoint rather than using
4047
+ * {@link INSTALL_BRIDGE_VERIFICATION_SENDER}.
4048
+ */
4049
+ export function logVerificationCodeLeadIn(log, email) {
4050
+ log(`We emailed a ${SIGNUP_VERIFICATION_CODE_LENGTH}-digit code to ${maskSignupEmail(email)}.`);
4051
+ log(`It usually arrives within two minutes. It comes from ${INSTALL_BRIDGE_VERIFICATION_SENDER} — check your`);
4052
+ log("spam or junk folder if you don't see it.");
4053
+ log("");
4054
+ }
4131
4055
  /** Shown when a stored, unexpired challenge is picked up on a later run. */
4132
4056
  export const INSTALL_BRIDGE_RESUME_VERIFICATION_HEADING = "Resume email verification";
4133
4057
  /**
@@ -4898,8 +4822,8 @@ function buildDeferredLaunchStepPreview() {
4898
4822
  /**
4899
4823
  * The optional agent-notes offer preview line (BAPI-1207). A --dry-run never
4900
4824
  * prompts or writes; when the selection resolves to at least one writable
4901
- * notes target this states plainly that a real interactive run would ask
4902
- * default-No consent before touching it. Empty when the selection resolves to
4825
+ * notes target this states plainly that a real interactive run writes it
4826
+ * without asking. Empty when the selection resolves to
4903
4827
  * no writable target (e.g. Windsurf-only), mirroring how the GitHub offer
4904
4828
  * lines are unconditional but this offer's applicability is not.
4905
4829
  */
@@ -4914,10 +4838,10 @@ function buildAgentNotesPreviewLines(notesTargets, platformLabels) {
4914
4838
  "Step 4c — reached only after the Step 4b VCS connect offer has completed or been",
4915
4839
  " explicitly deferred (a declined or incomplete connect is announced first,",
4916
4840
  " with the command to finish it later, and never blocks this step).",
4917
- `Step 4c — optional: Bridge MCP guidance for ${subject} (SKIPPED in --dry-run): if the`,
4918
- ` terminal is interactive, offer to write guidance so ${subject} know${plural ? "" : "s"} when to reach`,
4919
- ` for Bridge's MCP tools, in a Bridge-owned marker region in ${notesTargets.join(" and ")}; defaults`,
4920
- " to Yes [Y/n] on submitted Enter; EOF skips. Existing content outside that region is preserved. No notes files are",
4841
+ `Step 4c — Bridge MCP guidance for ${subject} (SKIPPED in --dry-run): if the`,
4842
+ ` terminal is interactive, a real run writes guidance so ${subject} know${plural ? "" : "s"} when to reach`,
4843
+ ` for Bridge's MCP tools, in a Bridge-owned marker region in ${notesTargets.join(" and ")}, without asking,`,
4844
+ " then prints how to remove it. Existing content outside that region is preserved. No notes files are",
4921
4845
  " written in --dry-run.",
4922
4846
  ];
4923
4847
  }
@@ -5395,10 +5319,6 @@ export function isMinttyNonInteractiveInstall(platform, env, isTTY) {
5395
5319
  }
5396
5320
  export async function runInstallBridgeCli(argv, overrides = {}) {
5397
5321
  const deps = { ...createDefaultInstallBridgeDeps(), ...overrides };
5398
- if (Object.prototype.hasOwnProperty.call(overrides, "promptLine") &&
5399
- !Object.prototype.hasOwnProperty.call(overrides, "promptGuidanceLine")) {
5400
- deps.promptGuidanceLine = undefined;
5401
- }
5402
5322
  // When the caller overrode only `fetch` (the common test seam), bind the
5403
5323
  // default resolver to the FINAL merged fetch so server resolution still routes
5404
5324
  // through the override. An explicit `resolveRepoViaServer` override is honored.
@@ -5749,7 +5669,7 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
5749
5669
  // The selection is pure and deterministic for every explicit, env-driven, and
5750
5670
  // non-TTY invocation; only a BARE interactive run is asked which branch it wants
5751
5671
  // (BAPI-626 — otherwise a first-time user can never reach self-serve).
5752
- const branchResult = await resolveInstallBridgeOnboardingBranchForRun(options, deps, argv);
5672
+ const branchResult = await resolveInstallBridgeOnboardingBranchForRun(options, deps, argv, baseUrl);
5753
5673
  if (!branchResult.ok) {
5754
5674
  errorLog(`Error: ${branchResult.error}`);
5755
5675
  return 1;
@@ -5791,7 +5711,12 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
5791
5711
  // key-authentication ping, so there is no source to attribute.
5792
5712
  let apiKeySource;
5793
5713
  if (selfServeSignupMode) {
5794
- const emailResult = await resolveSignupEmail(options, deps, baseUrl);
5714
+ const emailResult = await resolveSignupEmail(options, deps, baseUrl, branchResult.onboardingEmail
5715
+ ? {
5716
+ email: branchResult.onboardingEmail,
5717
+ privacyNoticeShown: branchResult.privacyNoticeShown === true,
5718
+ }
5719
+ : undefined);
5795
5720
  if (!emailResult.ok) {
5796
5721
  errorLog(`Error: ${emailResult.error}`);
5797
5722
  return 1;
@@ -6137,10 +6062,10 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
6137
6062
  return 0;
6138
6063
  }
6139
6064
  // ---- Finalize the launch agent for spawn-capable decisions ----
6140
- // choose-one: pick exactly one via the numbered chooser (TTY only); a non-TTY run or
6141
- // a failed/aborted chooser degrades to the manual continuation (finalAgentName stays
6142
- // null). This runs AFTER the multi-select readline closed and BEFORE the GitHub /
6143
- // consent flows, so no two stdin readers ever overlap.
6065
+ // choose-one: on a TTY, take the first launchable agent the user selected and say so
6066
+ // (BAPI-1448 retired the launch-agent chooser; `--agent` still wins
6067
+ // upstream in the pure launch decision). A non-TTY run degrades to the manual
6068
+ // continuation (finalAgentName stays null).
6144
6069
  let finalAgentName = null;
6145
6070
  let finalSpawnCommand = null;
6146
6071
  if (planLaunch.kind === "spawn") {
@@ -6148,8 +6073,9 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
6148
6073
  finalSpawnCommand = planLaunch.spawnCommand;
6149
6074
  }
6150
6075
  else if (planLaunch.kind === "choose-one") {
6151
- const chosen = await chooseInstallBridgeLaunchAgent(planLaunch.agents, deps);
6076
+ const chosen = deps.isTTY ? (planLaunch.agents[0] ?? null) : null;
6152
6077
  if (chosen) {
6078
+ log(`Using ${toolLabelForLaunchAgent(chosen)} for the setup session (the first tool you selected). Pass --agent to choose another.`);
6153
6079
  const spec = resolveAgentSpec(chosen);
6154
6080
  if (!spec) {
6155
6081
  errorLog(`Error: no launch agent is registered for '${chosen}'.`);
@@ -6159,7 +6085,7 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
6159
6085
  const modelAlias = await resolveValidatedPremiumModelAlias(deps, spec);
6160
6086
  finalSpawnCommand = deps.buildShellCommand(spec, INSTALL_BRIDGE_AGENT_PROMPT, deps.cwd, deps.platform, modelAlias);
6161
6087
  }
6162
- // chosen === null → non-TTY / unavailable / invalid → manual continuation below.
6088
+ // chosen === null → non-TTY → manual continuation below.
6163
6089
  }
6164
6090
  // planLaunch.kind === "manual" (no-launchable) → finalAgentName stays null.
6165
6091
  // ---- Materialize the Step 5 launch command + kick off the pre-warm — ONLY for a
@@ -6348,7 +6274,6 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
6348
6274
  challengeExpiresAt = liveChallenge.expiresAt;
6349
6275
  challengeEmail = liveChallenge.email;
6350
6276
  log(` ${INSTALL_BRIDGE_RESUME_VERIFICATION_HEADING}`);
6351
- log(` We already emailed a code to ${maskSignupEmail(challengeEmail)}.`);
6352
6277
  }
6353
6278
  else {
6354
6279
  const requested = await requestSelfServeChallenge(deps, baseUrl, signupEmail);
@@ -6408,12 +6333,9 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
6408
6333
  "No workspace has been set up — fix the problem and re-run.");
6409
6334
  return false;
6410
6335
  }
6411
- log(` We emailed a code to ${maskSignupEmail(challengeEmail)}.`);
6412
6336
  }
6413
- log(` Enter the ${SIGNUP_VERIFICATION_CODE_LENGTH}-digit code to finish setting up your workspace.`);
6414
- const remaining = formatChallengeRemaining(challengeExpiresAt, deps.now());
6415
- if (remaining)
6416
- log(` ${remaining}`);
6337
+ // BAPI-1448: the shared lead-in, fresh or resumed, directly before code entry.
6338
+ logVerificationCodeLeadIn(log, challengeEmail);
6417
6339
  // Phase 2, second half, and phase 3. One resend is offered on the recovery
6418
6340
  // path only — never proactively — so the budget is spent on the user's
6419
6341
  // explicit request rather than by the CLI's own retry logic.
@@ -6820,9 +6742,12 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
6820
6742
  // existing value unconditionally — the old value is never read first. A
6821
6743
  // no-answer (or a prompt/stream exception the picker already absorbed)
6822
6744
  // reports the same fail-open remediation without ever attempting a write.
6745
+ let persistedPosture;
6823
6746
  if (optimizationPostureSelection) {
6824
6747
  const postureResult = await persistOptimizationPosture(deps, baseUrl, apiKey, repoName, optimizationPostureSelection);
6825
6748
  reportOptimizationPostureOutcome(log, postureResult, optimizationPostureSelection, selectedPlatforms[0]);
6749
+ if (postureResult.kind === "success")
6750
+ persistedPosture = optimizationPostureSelection;
6826
6751
  }
6827
6752
  else if (optimizationPostureEligible) {
6828
6753
  log(OPTIMIZATION_POSTURE_UNCHANGED_MESSAGE);
@@ -7106,8 +7031,7 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
7106
7031
  // selected set. `writeAgentNotes` never touches `writeMcpInstallState` or any
7107
7032
  // other persisted install-state contract — consent is re-evaluated every run.
7108
7033
  const notesPlan = resolveAgentNotesTargets(selectedPlatforms);
7109
- const notesPlatformLabels = labelsForPlatforms(notesPlan.supportedPlatforms);
7110
- const notesOffer = await offerAgentGuidanceNotes(deps, log, notesPlan.targets, notesPlatformLabels);
7034
+ const notesOffer = offerAgentGuidanceNotes(deps, notesPlan.targets);
7111
7035
  let notesResult;
7112
7036
  let notesWriteFailed = false;
7113
7037
  if (notesOffer.kind === "consented") {
@@ -7137,10 +7061,20 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
7137
7061
  const notesSummaryLine = buildAgentNotesSummaryLine(notesOffer, notesResult, notesWriteFailed, notesPlan.unsupportedPlatforms);
7138
7062
  if (notesSummaryLine)
7139
7063
  log(notesSummaryLine);
7064
+ // The removal hint only when something was actually written (BAPI-1448).
7065
+ const notesWrittenTargets = (notesResult?.files ?? [])
7066
+ .filter((f) => f.status === "written")
7067
+ .map((f) => f.target);
7068
+ if (notesWrittenTargets.length > 0)
7069
+ log(INSTALL_BRIDGE_AGENT_NOTES_REMOVAL_HINT);
7070
+ const notesUnchangedTargets = (notesResult?.files ?? [])
7071
+ .filter((f) => f.status === "skipped")
7072
+ .map((f) => f.target);
7140
7073
  // ---- BAPI-1398: Bridge permission rules for /drive-epic-run (Claude Code) ----
7141
7074
  // After the durable install work and the other offers, before the session
7142
- // opens. Project rules are written as part of the normal install; the user
7143
- // settings change asks once and is skipped without a yes or a terminal. Every
7075
+ // opens. Project rules are written as part of the normal install. The user
7076
+ // settings change is NOT part of install (BAPI-1448): it lives in the
7077
+ // `bridge-permissions` subcommand, offered in-session by /drive-epic-run. Every
7144
7078
  // failure here is advisory: a malformed settings file is reported and skipped,
7145
7079
  // and the install still completes.
7146
7080
  if (selectedPlatforms.includes("claude-code")) {
@@ -7149,13 +7083,14 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
7149
7083
  cwd: deps.cwd,
7150
7084
  homeDir: deps.homedir(),
7151
7085
  env: deps.env,
7152
- promptLine: deps.isTTY ? deps.promptLine : undefined,
7153
7086
  readFile: deps.readFile,
7154
7087
  writeFile: (p, data) => deps.writeFile(p, data, { encoding: "utf-8" }),
7155
7088
  mkdir: (p, o) => deps.mkdir(p, o),
7156
7089
  log,
7157
7090
  errorLog,
7158
7091
  ensureGitignored: (relPath) => ensureGitignoredShared(deps.cwd, relPath, gitignoreDeps),
7092
+ // BAPI-1446: pin the SessionStart upgrade-notice hook to the effective Claude Code pin.
7093
+ sessionStartHook: { pinnedVersion: await discoverClaudeCodeEffectivePin({ cwd: deps.cwd, readFile: deps.readFile, targetVersion: VERSION }) },
7159
7094
  });
7160
7095
  }
7161
7096
  catch {
@@ -7187,6 +7122,27 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
7187
7122
  for (const line of buildInstallBridgeMaintenanceHint())
7188
7123
  log(line);
7189
7124
  await showFirstBuildAdvisory(finalAgentName);
7125
+ // ---- BAPI-1448: the defaults summary, once, right before the launch consent ----
7126
+ // Every input is already in memory except the optional ticket prefix, which is one
7127
+ // bounded read of the manifest the GitHub offer already uses. Any failure there just
7128
+ // omits the cell: the summary never blocks or changes the install's outcome.
7129
+ let ticketPrefix;
7130
+ try {
7131
+ const details = await fetchGithubConfigurationDetails({ fetch: deps.fetch, baseUrl, apiKey }, repoName);
7132
+ ticketPrefix = details.ticketPrefix;
7133
+ }
7134
+ catch {
7135
+ ticketPrefix = undefined;
7136
+ }
7137
+ log("");
7138
+ log(buildInstallBridgeDefaultsSummary({
7139
+ displayName: modernAttempt?.displayName ?? repoName,
7140
+ ...(ticketPrefix ? { ticketPrefix } : {}),
7141
+ isNewProject: Boolean(modernAttempt) || bootstrapExchangeSucceeded,
7142
+ ...(persistedPosture ? { persistedPosture } : {}),
7143
+ claudeCodeSelected: selectedPlatforms.includes("claude-code"),
7144
+ guidance: { written: notesWrittenTargets, unchanged: notesUnchangedTargets },
7145
+ }));
7190
7146
  // ---- Step 5 — consent-gate, then spawn the selected tool (or print continuation) ----
7191
7147
  // AC-7: for a spawn-capable path, ask Y/N first (`requestInstallBridgeLaunchConsent`
7192
7148
  // resolves to no-spawn without prompting on a non-TTY run). A decline, a no-launchable
@@ -7326,13 +7282,19 @@ async function redeemModernInstallAttempt(deps, options, baseUrl, token, signupE
7326
7282
  if (label === undefined) {
7327
7283
  if (!deps.isTTY || !deps.promptLine)
7328
7284
  throw new ModernInstallRecoveryError("Modern invite creation requires --display-name in a non-interactive terminal.");
7285
+ // BAPI-1448: a valid folder name is the label, with no question. The prompt
7286
+ // survives only for a folder name the validator rejects.
7329
7287
  let suggestion = "";
7330
7288
  try {
7331
7289
  suggestion = validateBootstrapDisplayName(path.basename(folder));
7332
7290
  }
7333
7291
  catch { /* explicit label required */ }
7334
- const entered = await deps.promptLine(suggestion ? `Project display name [${suggestion}]: ` : "Project display name: ");
7335
- label = validateBootstrapDisplayName(entered || suggestion);
7292
+ if (suggestion) {
7293
+ label = suggestion;
7294
+ }
7295
+ else {
7296
+ label = validateBootstrapDisplayName(await deps.promptLine("Project display name: "));
7297
+ }
7336
7298
  }
7337
7299
  if (selfServe) {
7338
7300
  if (!deps.isTTY || !deps.promptLine)
@@ -7364,6 +7326,7 @@ async function redeemModernInstallAttempt(deps, options, baseUrl, token, signupE
7364
7326
  throw new ModernInstallRecoveryError("The previous signup response is unresolved. Preserve proof and contact installation support before restarting.");
7365
7327
  if (Date.parse(record.challengeExpiresAt) <= deps.now())
7366
7328
  throw new ModernInstallRecoveryError("The saved verification challenge expired. Preserve proof and contact installation support.");
7329
+ logVerificationCodeLeadIn(deps.log, record.signupEmail ?? signupEmail);
7367
7330
  const code = await promptForVerificationCode(deps, deps.log);
7368
7331
  if (!code)
7369
7332
  throw new ModernInstallRecoveryError("Verification incomplete. Resume the same signup from this folder.");