@bridge_gpt/mcp-server 0.2.53 → 0.2.55

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 (103) hide show
  1. package/README.md +116 -56
  2. package/build/agent-guidance.generated.js +3 -0
  3. package/build/agent-launchers/claude-executor-adapter.js +3 -0
  4. package/build/agent-launchers/claude.js +3 -3
  5. package/build/agent-launchers/prompt.js +8 -11
  6. package/build/agent-notes.js +178 -0
  7. package/build/agent-registry.js +5 -2
  8. package/build/agent-utils.js +58 -0
  9. package/build/agents.generated.js +1 -1
  10. package/build/base-ref.js +33 -9
  11. package/build/bounded-wait.js +174 -0
  12. package/build/codex-skill-adapter.js +55 -0
  13. package/build/commands.generated.js +6 -5
  14. package/build/conductor/bridge-api-client.js +235 -14
  15. package/build/conductor/bring-up-facts.js +187 -0
  16. package/build/conductor/claude-hook.js +7 -5
  17. package/build/conductor/cli.js +28 -0
  18. package/build/conductor/doctor.js +80 -9
  19. package/build/conductor/epic-implementer-cli.js +1298 -0
  20. package/build/conductor/epic-runtime.js +134 -98
  21. package/build/conductor/errors.js +2 -2
  22. package/build/conductor/git-ci-types.js +1 -1
  23. package/build/conductor/git-hooks.js +28 -14
  24. package/build/conductor/install-doctor.js +11 -5
  25. package/build/conductor/readiness-cli.js +10 -10
  26. package/build/conductor/readiness-sections.js +58 -9
  27. package/build/conductor/readiness.js +120 -4
  28. package/build/conductor/recovery-cli.js +289 -10
  29. package/build/conductor/recovery-operations.js +125 -2
  30. package/build/conductor/repair-contract.js +58 -0
  31. package/build/conductor/run-branch.js +137 -0
  32. package/build/conductor/store.js +2 -2
  33. package/build/conductor/supervisor-runtime.js +1 -1
  34. package/build/conductor/test-run-branch-vectors.js +165 -0
  35. package/build/conductor-bin.js +2 -139
  36. package/build/conductor-claude-hook-bin.js +2 -2
  37. package/build/conductor-claude-hook-removed-stub-bin.js +31 -0
  38. package/build/conductor-removed-stub-bin.js +30 -0
  39. package/build/docs.generated.js +1 -1
  40. package/build/doctor.js +145 -18
  41. package/build/drive-epic.js +752 -90
  42. package/build/epic-implementer-bin.js +145 -0
  43. package/build/epic-implementer-bundle-cli.js +264 -0
  44. package/build/epic-implementer-claude-hook-bin.js +3 -0
  45. package/build/epic-integration-pr.js +5 -3
  46. package/build/executor/claim-scope.js +104 -0
  47. package/build/executor/cli.js +14 -25
  48. package/build/executor/env-file-guard.js +82 -3
  49. package/build/executor/env.js +6 -0
  50. package/build/executor/interrupted-worktree.js +60 -0
  51. package/build/executor/job-errors.js +45 -0
  52. package/build/executor/job-runner.js +334 -9
  53. package/build/executor/job-types.js +25 -9
  54. package/build/executor/merge-tree-classifier.js +171 -0
  55. package/build/executor/reconcile-overlap-governance.js +129 -0
  56. package/build/executor/reconcile-overlap-job.js +989 -0
  57. package/build/executor/reconcile-overlap-types.js +14 -0
  58. package/build/executor/spawn-job-driver.js +1 -0
  59. package/build/executor/types.js +2 -0
  60. package/build/executor/worker-finalization.js +25 -2
  61. package/build/executor/worker-guard-hook.js +15 -7
  62. package/build/implement-epic/bridge-client.js +773 -0
  63. package/build/implement-epic/checkpoint-store.js +542 -0
  64. package/build/implement-epic/cli.js +3158 -0
  65. package/build/implement-epic/cut-protocol.js +392 -0
  66. package/build/implement-epic/lock.js +302 -0
  67. package/build/implement-epic/pr-state.js +286 -0
  68. package/build/implement-epic/spawn.js +113 -0
  69. package/build/index.js +657 -481
  70. package/build/init.js +72 -8
  71. package/build/install-bridge-conductor.js +5 -5
  72. package/build/install-bridge.js +403 -70
  73. package/build/local-artifact-storage.js +130 -0
  74. package/build/mcp-host-config.js +22 -60
  75. package/build/mcp-host-entry-adapter.js +18 -0
  76. package/build/mcp-host-targets.js +1 -21
  77. package/build/merge-pull-request.js +1 -1
  78. package/build/pipelines.generated.js +23 -16
  79. package/build/plan-epic-conductor-eligibility.js +1 -1
  80. package/build/plane/cli.js +321 -41
  81. package/build/plane/manifest.js +209 -1
  82. package/build/plane/member-roster.js +70 -0
  83. package/build/plane/preflight.js +128 -12
  84. package/build/plane/shutdown.js +18 -5
  85. package/build/plane/status.js +35 -1
  86. package/build/plane/supervisor.js +546 -164
  87. package/build/plane/types.js +25 -2
  88. package/build/polling-policy.js +72 -0
  89. package/build/readiness-check.js +3 -3
  90. package/build/readme.generated.js +1 -1
  91. package/build/review-generation.js +219 -0
  92. package/build/run-unit-tests-launcher.js +6 -1
  93. package/build/setup-epic.js +567 -38
  94. package/build/start-tickets-conductor.js +8 -7
  95. package/build/ticket-key-utils.js +4 -3
  96. package/build/ticket-review-artifact-gate.js +461 -0
  97. package/build/upgrade-cli.js +5 -26
  98. package/build/version.generated.js +3 -3
  99. package/build/worker-guard-hook-bin.js +1 -1
  100. package/docs/CONDUCTOR.md +8 -6
  101. package/docs/install/mcp-tool-integrations.md +23 -1
  102. package/package.json +5 -3
  103. package/pipelines/review-ticket.json +17 -4
@@ -149,9 +149,13 @@ import { VERSION } from "./version.generated.js";
149
149
  import { validateRepoName } from "./bridge-config.js";
150
150
  import { MCP_HOST_TARGETS, HOST_PLATFORM_ORDER, allHostTargets, agentForPlatform, isHostPlatformId, detectDefaultPlatforms, hostAdapterForTarget, } from "./mcp-host-targets.js";
151
151
  import { provisionHostTarget, createDefaultVendorProcessDeps, inspectBridgeApiProfileToken, } from "./mcp-host-config.js";
152
+ import { adaptBridgeEntryForHostTarget, } from "./mcp-host-entry-adapter.js";
152
153
  import { MCP_SERVER_NAME, MCP_PACKAGE_NAME, resolveRegistrationKey, } from "./mcp-identity.js";
153
154
  import { DUPLICATE_REGISTRATION_GUIDANCE } from "./launcher-config-inspection.js";
154
155
  import { recordInstalledConductorCapability, recordInstalledExecutorServiceUnit, recordInstalledProjectArtifact, writeMcpInstallState, } from "./mcp-install-state.js";
156
+ // BAPI-1207: opt-in cross-platform agent notes (AGENTS.md / CLAUDE.md).
157
+ import { AGENT_GUIDANCE } from "./agent-guidance.generated.js";
158
+ import { resolveAgentNotesTargets, writeAgentNotes, } from "./agent-notes.js";
155
159
  // Executor provisioning (BAPI-779). The generator and the lifecycle module are
156
160
  // imported DIRECTLY and driven programmatically — never by spawning
157
161
  // `executor install-service` as a subprocess, which would lose the typed plan,
@@ -178,8 +182,8 @@ import { fetchGithubConfigurationState } from "./connect-github-api.js";
178
182
  import { createDefaultConnectGithubDeps, runGithubConnectionFlow } from "./connect-github.js";
179
183
  import { fetchBitbucketConfigurationState } from "./connect-bitbucket-api.js";
180
184
  import { createDefaultConnectBitbucketDeps, runBitbucketConnectionFlow } from "./connect-bitbucket.js";
181
- import { resolveAgentSpec, isAgentName, formatValidAgentNames, } from "./agent-registry.js";
182
- import { buildGenericAgentShellCommand, getDefaultSpawnTerminalTabForPlatform, detectTerminal, createDefaultStartTicketsDeps, materializeWorkerLaunchCommand, MAX_TERMINAL_COMMAND_BYTES, } from "./start-tickets.js";
185
+ import { resolveAgentSpec, isAgentName, formatValidAgentNames, resolveModelAlias, } from "./agent-registry.js";
186
+ import { buildGenericAgentShellCommand, getDefaultSpawnTerminalTabForPlatform, detectTerminal, createDefaultStartTicketsDeps, materializeWorkerLaunchCommand, MAX_TERMINAL_COMMAND_BYTES, validateResolvedModelAliasForAgent, } from "./start-tickets.js";
183
187
  /** Redaction sentinel — the API-key value is NEVER printed; this stands in. */
184
188
  export const REDACTED_API_KEY = "<REDACTED>";
185
189
  /**
@@ -226,8 +230,16 @@ export function buildPrewarmCommandPreview() {
226
230
  * automatic, gated by the existing readiness funnel
227
231
  * (`maybe_auto_parse_if_ready`), with no prompt on any path.
228
232
  */
229
- export const INSTALL_BRIDGE_AGENT_PROMPT = "Execute the /install-bridge command in the install-spawn context (tell the command it is running " +
230
- "in the install-spawn context so it SKIPS its Stage 8, Stage 9, and Stage 10 offers). " +
233
+ export const INSTALL_BRIDGE_AGENT_PROMPT = "Run /install-bridge now, with no arguments that is the entire invocation. You are in the " +
234
+ "install-spawn context: that is a mode this prompt establishes in plain language, not a " +
235
+ "command-line option, so never convert it into --context install-spawn or any other flag. The " +
236
+ "Bridge MCP tools named throughout this command are available only inside this currently running " +
237
+ "agent session — never invoke them through Bash or by constructing a direct HTTP request. " +
238
+ "/install-bridge owns and automatically executes its own internal stages; do not reproduce, " +
239
+ "narrate as separate manual steps, or hand-execute those stages yourself. Never place the " +
240
+ "API-key value in a Bash command, its arguments, printed output, a log line, an error message, " +
241
+ "or a file you write; verify connectivity with the in-session ping MCP tool only — never through " +
242
+ "an environment export, echo, or any other shell probe. " +
231
243
  "The command owns its own Stage 2 decision and evaluates it in a fixed order from the install " +
232
244
  "manifest — do NOT force an outcome. `configured == true` is JOIN MODE; otherwise a manifest whose " +
233
245
  "`greenfield_setup_path` field is already set is GREENFIELD REPORT-ONLY; otherwise the command's " +
@@ -255,6 +267,31 @@ export const INSTALL_BRIDGE_AGENT_PROMPT = "Execute the /install-bridge command
255
267
  "back to the complete tool_capabilities catalog or the workflows collection. If " +
256
268
  "concise_tool_capabilities is missing or malformed, take the command's single fallback line and " +
257
269
  "render nothing else in its place. " +
270
+ "The same manifest response also carries a SEPARATE top-level human_setup field (contract v9) — an " +
271
+ "ordered list naming each remaining unconfigured integration and what finishing it unlocks, " +
272
+ "additive over the same checklist integrations already carries. It is never a third section of " +
273
+ "concise_tool_capabilities; render it as its own separate block, after the two-section report above. " +
274
+ "Absent (an older compatible manifest), malformed (not a list, or an entry missing display_text), or " +
275
+ "a valid empty list all render NOTHING for this block — no heading, no fabricated completion line, " +
276
+ "no invented setup advice; the two-section report above still renders unaffected either way. A " +
277
+ "valid non-empty list renders under the heading 'What finishing setup unlocks', one compact item per " +
278
+ "entry in the server's given order, using human_setup[].display_text as that item's entire " +
279
+ "substantive content, byte-for-byte — never summarized, reworded, sorted, filtered, merged, or " +
280
+ "recomposed from configure_in, setup_instructions, required_for, integrations, or any tool/workflow " +
281
+ "catalog. The same server-authority rule the concise capability report already follows extends to " +
282
+ "this field. " +
283
+ "Plain-language narration (BAPI-1174): explain each major action you take — reading the manifest, " +
284
+ "applying configuration, persisting the routing credential, rendering the capability report — in a " +
285
+ "short, plain-language sentence covering what is happening and why it matters to the person running " +
286
+ "this install, rather than in internal shorthand. Narration you compose yourself must never name an " +
287
+ "internal stage number, this command's contract version, a raw internal MCP tool identifier (like " +
288
+ "get_install_manifest, apply_install_manifest, or invite_member), or a manifest field key (like " +
289
+ "command_contract_version, next_step, or configured) — describe what is actually happening instead. " +
290
+ "This restriction is scoped to narration you author: it does not apply to a user-invocable command " +
291
+ "name you must give the human verbatim (/install-bridge, /learn-repository, connect-github, " +
292
+ "start-tickets, and the like) or to any value a stage marks as server-authored and rendered " +
293
+ "verbatim (the concise capability report, human_setup display text, a next_step.command string) — " +
294
+ "those are never reworded, curated, or filtered by you either way. " +
258
295
  "In the install-spawn context Stage 6 must STILL call persist_routing_credential, but must NOT " +
259
296
  "print or summarize its SUCCESSFUL result anywhere — no 'Stored routing credential for …' line, " +
260
297
  "and no restatement of that target or path in any later summary or Return section — because the " +
@@ -352,13 +389,13 @@ export function getInstallBridgeUsage(baseUrl = DEFAULT_BAPI_BASE_URL) {
352
389
  ...INSTALL_BRIDGE_ONBOARDING_CHOICES.map((choice) => ` ${choice}`),
353
390
  "There is NO default — pressing Enter selects nothing; you must type 1 or 2 (one",
354
391
  "blank or invalid answer re-prompts once, then exits with guidance). Answering 1",
355
- "opens a single hidden token prompt: paste whatever you were given and its SHAPE",
356
- "picks the route a bootstrap invite (bapi_inv_…) is redeemed to CREATE a new",
357
- "project and mint your first admin key, while a Bridge API key connects to the",
358
- "project that already exists. You are never asked which kind of token you hold.",
359
- "Answering 2 prompts for an email and creates a brand-new Bridge workspace for",
360
- "you (the self-serve flow). That question is asked ONLY for a bare interactive",
361
- "run: passing ANY flag, setting BAPI_API_KEY, or running without an interactive",
392
+ "prompts for an email and creates a brand-new Bridge workspace for you (the",
393
+ "self-serve flow). Answering 2 opens a single hidden token prompt: paste whatever",
394
+ "you were given and its SHAPE picks the route a bootstrap invite (bapi_inv_…)",
395
+ "is redeemed to CREATE a new project and mint your first admin key, while a",
396
+ "Bridge API key connects to the project that already exists. You are",
397
+ "never asked which kind of token you hold. That question is asked ONLY for a",
398
+ "bare interactive run: passing ANY flag, setting BAPI_API_KEY, or running without an interactive",
362
399
  "terminal keeps the existing deterministic behavior and no prompt.",
363
400
  "",
364
401
  "On that bare run the route is INFERRED, so it is only announced — the detected",
@@ -426,7 +463,7 @@ export function getInstallBridgeUsage(baseUrl = DEFAULT_BAPI_BASE_URL) {
426
463
  " the code is entered — no workspace, no project, no API key.",
427
464
  " Delivery is asynchronous, so the message arrives shortly",
428
465
  " after you are prompted rather than instantly. The code",
429
- " expires 60 minutes after it is requested, and you can ask",
466
+ " expires 4 hours after it is requested, and you can ask",
430
467
  " for a new one if it does not arrive; earlier codes keep",
431
468
  " working until one is accepted.",
432
469
  "",
@@ -827,7 +864,7 @@ export const INSTALL_BRIDGE_GITHUB_CONNECT_PROMPT = "Connect GitHub? [y/N]: ";
827
864
  * Reuses the shared connect-github flow rather than duplicating the API, polling,
828
865
  * browser, or picker logic — there is exactly one implementation of that handshake.
829
866
  */
830
- export async function offerGithubConnection(repoName, baseUrl, deps, log) {
867
+ export async function offerGithubConnection(repoName, baseUrl, deps, log, session) {
831
868
  // No prompt surface → no offer. Never assume consent on a non-interactive run.
832
869
  if (!deps.isTTY || !deps.promptLine)
833
870
  return;
@@ -875,7 +912,12 @@ export async function offerGithubConnection(repoName, baseUrl, deps, log) {
875
912
  return; // declining is a normal outcome
876
913
  const connectDeps = createDefaultConnectGithubDeps();
877
914
  const code = await runGithubConnectionFlow(connectDeps, api, repoName);
878
- if (code !== 0) {
915
+ if (code === 0) {
916
+ // BAPI-1171: only a CONFIRMED success suppresses the Bitbucket offer below —
917
+ // a decline, failure, or thrown error must never look like a connection.
918
+ session.vcsConnected = true;
919
+ }
920
+ else {
879
921
  log(" note: GitHub was not connected. Your install is complete — connect GitHub later with " +
880
922
  `'npx -y ${MCP_PACKAGE_NAME}@latest connect-github --repo ${repoName}'.`);
881
923
  }
@@ -915,10 +957,15 @@ export const INSTALL_BRIDGE_BITBUCKET_CONNECT_PROMPT = "Connect Bitbucket? [y/N]
915
957
  * Reuses the shared connect-bitbucket flow rather than duplicating the API, polling,
916
958
  * browser, or picker logic — there is exactly one implementation of that handshake.
917
959
  */
918
- export async function offerBitbucketConnection(repoName, baseUrl, deps, log) {
960
+ export async function offerBitbucketConnection(repoName, baseUrl, deps, log, session) {
919
961
  // No prompt surface → no offer. Never assume consent on a non-interactive run.
920
962
  if (!deps.isTTY || !deps.promptLine)
921
963
  return;
964
+ // BAPI-1171: a GitHub connection made earlier in THIS run already leaves the
965
+ // project with a connected VCS provider — skip provider lookup, prompting, the
966
+ // browser flow, and the connection handshake entirely rather than asking again.
967
+ if (session.vcsConnected)
968
+ return;
922
969
  try {
923
970
  const credDeps = {
924
971
  env: deps.env,
@@ -971,6 +1018,88 @@ export async function offerBitbucketConnection(repoName, baseUrl, deps, log) {
971
1018
  `Bitbucket later with 'npx -y ${MCP_PACKAGE_NAME}@latest connect-bitbucket --repo ${repoName}'.`);
972
1019
  }
973
1020
  }
1021
+ /** Default-No agent-notes offer prompt. Mirrors {@link INSTALL_BRIDGE_GITHUB_CONNECT_PROMPT}. */
1022
+ export const INSTALL_BRIDGE_AGENT_NOTES_PROMPT = "Add cross-platform agent notes? [y/N]: ";
1023
+ /**
1024
+ * The offer-context lines printed immediately before the agent-notes prompt
1025
+ * (BAPI-1207). Names the applicable file(s) and states the marker-scoped,
1026
+ * non-destructive contract up front — mirrors the GitHub/Bitbucket offers'
1027
+ * "explain before asking" convention.
1028
+ */
1029
+ export function buildAgentNotesOfferContext(targets) {
1030
+ return [
1031
+ "",
1032
+ "Step 4c — optional: cross-platform agent notes.",
1033
+ ` This creates or refreshes a Bridge-owned region in ${targets.join(" and ")} with situation-`,
1034
+ " to-tool guidance for a few Bridge MCP tools. Only that marked region is created or",
1035
+ " refreshed — any existing content in the file, inside or outside it, is otherwise preserved.",
1036
+ ];
1037
+ }
1038
+ /**
1039
+ * Offer to create or refresh the cross-platform agent notes, if the selected
1040
+ * platforms resolve to at least one writable target (BAPI-1207).
1041
+ *
1042
+ * Entirely best-effort and non-destructive, mirroring
1043
+ * {@link offerGithubConnection}: by the time this runs the install itself is
1044
+ * already complete and durable, so nothing here may fail the run. This
1045
+ * function only resolves the CONSENT decision — the caller performs the
1046
+ * actual write via {@link writeAgentNotes}, so a `--dry-run` (which never
1047
+ * reaches this call) and the live path share exactly one write path.
1048
+ */
1049
+ export async function offerAgentGuidanceNotes(deps, log, targets) {
1050
+ if (targets.length === 0)
1051
+ return { kind: "not-applicable" };
1052
+ // No prompt surface → no offer. Never assume consent on a non-interactive run.
1053
+ if (!deps.isTTY || !deps.promptLine)
1054
+ return { kind: "headless-skip" };
1055
+ try {
1056
+ for (const line of buildAgentNotesOfferContext(targets))
1057
+ log(line);
1058
+ const answer = (await deps.promptLine(INSTALL_BRIDGE_AGENT_NOTES_PROMPT)).trim().toLowerCase();
1059
+ // Only an explicit yes proceeds — blank input, EOF, "n", and anything malformed
1060
+ // all decline without writing.
1061
+ return answer === "y" || answer === "yes" ? { kind: "consented" } : { kind: "declined" };
1062
+ }
1063
+ catch {
1064
+ // The install is already durable; a failure here is never fatal to it.
1065
+ log(" note: the agent-notes offer could not run. No notes files were written.");
1066
+ return { kind: "declined" };
1067
+ }
1068
+ }
1069
+ /**
1070
+ * One secret-free summary line for the final install output (BAPI-1207). Never
1071
+ * reveals guidance content, file bytes, or a credential — only which files
1072
+ * were touched, skipped, or failed, and which selected platforms have no
1073
+ * supported notes convention (currently only Windsurf).
1074
+ */
1075
+ export function buildAgentNotesSummaryLine(offer, result, writeFailed, unsupportedPlatforms) {
1076
+ const unsupportedNote = unsupportedPlatforms.length > 0
1077
+ ? ` (${unsupportedPlatforms.join(", ")}: no supported agent-notes convention)`
1078
+ : "";
1079
+ if (offer.kind === "not-applicable") {
1080
+ return unsupportedPlatforms.length > 0 ? `Agent notes: not written${unsupportedNote}.` : undefined;
1081
+ }
1082
+ if (offer.kind === "headless-skip") {
1083
+ return `Agent notes: skipped (non-interactive session)${unsupportedNote}.`;
1084
+ }
1085
+ if (offer.kind === "declined") {
1086
+ return `Agent notes: skipped (declined)${unsupportedNote}.`;
1087
+ }
1088
+ if (writeFailed || !result) {
1089
+ return `Agent notes: could not be written (unexpected error)${unsupportedNote}.`;
1090
+ }
1091
+ const written = result.files.filter((f) => f.status === "written").map((f) => f.target);
1092
+ const unchanged = result.files.filter((f) => f.status === "skipped").map((f) => f.target);
1093
+ const failed = result.files.filter((f) => f.status === "failed").map((f) => f.target);
1094
+ const parts = [];
1095
+ if (written.length > 0)
1096
+ parts.push(`written to ${written.join(", ")}`);
1097
+ if (unchanged.length > 0)
1098
+ parts.push(`already up to date in ${unchanged.join(", ")}`);
1099
+ if (failed.length > 0)
1100
+ parts.push(`failed for ${failed.join(", ")}`);
1101
+ return `Agent notes: ${parts.join("; ")}${unsupportedNote}.`;
1102
+ }
974
1103
  /**
975
1104
  * Echoed single-line prompt on stderr (used for repo confirmation / value).
976
1105
  * Exported so `index.ts`'s `--init` can share the exact same prompt mechanics
@@ -1484,11 +1613,18 @@ export async function resolveInviteToken(options, deps) {
1484
1613
  * is never written to a log line; it is only ever echoed as the user's own keystrokes
1485
1614
  * and placed in the mint request body.)
1486
1615
  *
1487
- * No email-format validation is performed here — only surrounding whitespace is
1488
- * trimmed, matching the other CLI input seams and the server's deliberately
1489
- * permissive "stored unvalidated" contract (D-2). The entered value is never echoed
1616
+ * Explicit sources (`--email`, `BAPI_SIGNUP_EMAIL`) still take only trimmed
1617
+ * whitespace matching the other CLI input seams and the server's deliberately
1618
+ * permissive "stored unvalidated" contract (D-2). The interactive prompt, added by
1619
+ * BAPI-1171, is bounded and additionally requires the trimmed value to match a
1620
+ * minimal `local@domain` shape before it is accepted — mirroring the one-hint/
1621
+ * one-re-prompt contract {@link resolveInstallBridgeOnboardingBranchForRun} uses,
1622
+ * so an unanswerable prompt cannot loop forever. The entered value is never echoed
1490
1623
  * back in an error message.
1491
1624
  */
1625
+ const INSTALL_BRIDGE_SIGNUP_EMAIL_SHAPE = /^[^@\s]+@[^@\s]+$/;
1626
+ /** The one hint line printed after a blank/malformed email, before the single re-prompt. */
1627
+ export const INSTALL_BRIDGE_SIGNUP_EMAIL_HINT = "Enter a valid email address (e.g. you@example.com).";
1492
1628
  export async function resolveSignupEmail(options, deps) {
1493
1629
  // BAPI-757 (R-3 / AC-9): self-serve signup now REQUIRES an interactive terminal,
1494
1630
  // and the check runs FIRST — before `--email` and before `BAPI_SIGNUP_EMAIL` —
@@ -1507,9 +1643,19 @@ export async function resolveSignupEmail(options, deps) {
1507
1643
  if (typeof fromEnv === "string" && fromEnv.trim().length > 0) {
1508
1644
  return { ok: true, value: fromEnv.trim() };
1509
1645
  }
1510
- const entered = (await deps.promptLine("Email for Bridge workspace setup: ")).trim();
1511
- if (entered.length > 0) {
1512
- return { ok: true, value: entered };
1646
+ const promptLine = deps.promptLine;
1647
+ // BAPI-1171: bounded to two reads total — the first answer, then ONE re-prompt
1648
+ // after a single hint line. EOF resolves as an empty value (promptLineViaReadline
1649
+ // resolves "" on close), which fails the shape check like any other blank
1650
+ // answer, so it costs one re-prompt and then fails cleanly rather than hanging.
1651
+ for (let attempt = 0; attempt < 2; attempt += 1) {
1652
+ const raw = await promptLine("Email for Bridge workspace setup (required): ");
1653
+ const entered = String(raw ?? "").trim();
1654
+ if (INSTALL_BRIDGE_SIGNUP_EMAIL_SHAPE.test(entered)) {
1655
+ return { ok: true, value: entered };
1656
+ }
1657
+ if (attempt === 0)
1658
+ deps.log(INSTALL_BRIDGE_SIGNUP_EMAIL_HINT);
1513
1659
  }
1514
1660
  return { ok: false, error: "No email entered." };
1515
1661
  }
@@ -1557,15 +1703,16 @@ export function resolveInstallBridgeOnboardingBranch(options, env) {
1557
1703
  export const INSTALL_BRIDGE_KEY_SELECTOR_PROMPT = "Welcome to Bridge - do you have a token yet?";
1558
1704
  /**
1559
1705
  * The question's numbered options, in render order. Index + 1 is the accepted
1560
- * token, exactly as before only the option SET narrowed from three to two.
1706
+ * token. BAPI-1171 swapped the order so the self-serve, need-a-token path is
1707
+ * option 1 — first-time signup is the common case and now leads.
1561
1708
  *
1562
1709
  * "Token" is deliberately credential-agnostic: an invite and an API key are both
1563
- * tokens as far as this question is concerned, and answering "Yes" leads to ONE
1710
+ * tokens as far as this question is concerned, and answering "2" leads to ONE
1564
1711
  * hidden prompt whose value selects the route by shape.
1565
1712
  */
1566
1713
  export const INSTALL_BRIDGE_ONBOARDING_CHOICES = [
1567
- "1. Yes, I have received a token",
1568
- "2. No, I need one",
1714
+ "1. I need a token",
1715
+ "2. I already have a token",
1569
1716
  ];
1570
1717
  /** The single-line answer prompt drawn after the options (no bracketed default). */
1571
1718
  export const INSTALL_BRIDGE_ONBOARDING_CHOICE_PROMPT = "Enter 1 or 2: ";
@@ -1579,9 +1726,9 @@ export const INSTALL_BRIDGE_ONBOARDING_CHOICE_HINT = "Enter 1 or 2.";
1579
1726
  * advised "try the hidden prompt again" — false, because the process has already
1580
1727
  * exited by the time the user reads it. All three now name the SAME three routes.
1581
1728
  */
1582
- export const INSTALL_BRIDGE_ENTRY_ROUTES_ADVICE = "Re-run install: on a bare interactive run answer 1 if you already have a token " +
1583
- "(an invite or an API key — either one is accepted at the hidden prompt) or 2 to sign up " +
1584
- "with just an email. Non-interactively, pass --api-key <key> (or set BAPI_API_KEY) for an " +
1729
+ 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 " +
1730
+ "already have one (an invite or an API key — either one is accepted at the hidden prompt). " +
1731
+ "Non-interactively, pass --api-key <key> (or set BAPI_API_KEY) for an " +
1585
1732
  "existing project, --invite if you were sent an invite token, or --email <addr> to sign up.";
1586
1733
  /**
1587
1734
  * Interactive wrapper around {@link resolveInstallBridgeOnboardingBranch}.
@@ -1651,20 +1798,21 @@ export async function resolveInstallBridgeOnboardingBranchForRun(options, deps,
1651
1798
  // it can never hang or silently select a branch.
1652
1799
  for (let attempt = 0; attempt < 2; attempt += 1) {
1653
1800
  const answer = (await promptLine(INSTALL_BRIDGE_ONBOARDING_CHOICE_PROMPT)).trim();
1654
- // BAPI-708 (A-1): the accepted tokens are now exactly 1/2 (no letters, no
1655
- // aliases), preserving the one-strict-line contract. "Yes" routes to the
1801
+ // BAPI-708 (A-1): the accepted tokens are exactly 1/2 (no letters, no
1802
+ // aliases), preserving the one-strict-line contract. BAPI-1171 swapped the
1803
+ // order so "1" is the self-serve need-key branch and "2" routes to the
1656
1804
  // have-key branch's credential-agnostic prompt, whose value is classified by
1657
1805
  // SHAPE — so a user holding an invite still lands in the redemption protocol
1658
1806
  // without ever having been asked which kind of token they hold.
1659
- if (answer === "1")
1660
- return { ok: true, branch: { kind: "have-key" }, credentialIntent: "inferred" };
1661
- if (answer === "2") {
1807
+ if (answer === "1") {
1662
1808
  return {
1663
1809
  ok: true,
1664
1810
  branch: { kind: "need-key", method: "self-serve" },
1665
1811
  credentialIntent: "inferred",
1666
1812
  };
1667
1813
  }
1814
+ if (answer === "2")
1815
+ return { ok: true, branch: { kind: "have-key" }, credentialIntent: "inferred" };
1668
1816
  if (attempt === 0)
1669
1817
  deps.log(INSTALL_BRIDGE_ONBOARDING_CHOICE_HINT);
1670
1818
  }
@@ -1854,8 +2002,8 @@ export function promptMultiSelectViaReadline(promptText, options, input = proces
1854
2002
  * 2. interactive multi-select on a TTY (AC-2: no row pre-selected — the picker
1855
2003
  * starts empty and requires an explicit ≥1 selection),
1856
2004
  * 3. the legacy non-TTY automatic set: Claude Code plus only the currently
1857
- * detected Copilot VS Code and Cursor automatic targets (never Codex or
1858
- * Copilot CLI just because a global directory exists).
2005
+ * detected Copilot VS Code and Cursor automatic targets (never Codex just
2006
+ * because a global directory exists).
1859
2007
  */
1860
2008
  export async function resolveSelectedHostPlatforms(deps, options) {
1861
2009
  // 1. Explicit --tools (empty array is an explicit empty selection).
@@ -2137,8 +2285,8 @@ async function buildDetectionContext(deps) {
2137
2285
  /**
2138
2286
  * Resolve which project-local JSON host configs to write for the selected
2139
2287
  * platforms. Only the project-scoped JSON targets (Claude Code, Cursor, Copilot
2140
- * VS Code) are returned here; global targets (Codex, Copilot CLI) and manual
2141
- * targets (Windsurf) are handled by the registry-driven emitter in
2288
+ * VS Code) are returned here; the global target (Codex) and manual targets
2289
+ * (Windsurf) are handled by the registry-driven emitter in
2142
2290
  * `runInstallBridgeCli`. The returned shape is unchanged so the existing
2143
2291
  * read-merge-write path and overwrite-consent detection are preserved.
2144
2292
  */
@@ -2147,7 +2295,7 @@ function hostConfigTargetsForPlatforms(platforms) {
2147
2295
  return HOST_PLATFORM_ORDER.filter((id) => set.has(id))
2148
2296
  .map((id) => MCP_HOST_TARGETS[id])
2149
2297
  .filter((t) => t.scope === "project" && hostAdapterForTarget(t).format === "json")
2150
- .map((t) => ({ relPath: t.relPath, topLevelKey: hostAdapterForTarget(t).topLevelKey }));
2298
+ .map(toHostConfigTarget);
2151
2299
  }
2152
2300
  /**
2153
2301
  * Derive the human-facing labels for a set of selected platforms, in registry
@@ -2162,7 +2310,7 @@ export function labelsForPlatforms(platforms) {
2162
2310
  }
2163
2311
  /** Project-local host-config metadata sourced from the shared registry/adapter. */
2164
2312
  function toHostConfigTarget(target) {
2165
- return { relPath: target.relPath, topLevelKey: hostAdapterForTarget(target).topLevelKey };
2313
+ return { target, relPath: target.relPath, topLevelKey: hostAdapterForTarget(target).topLevelKey };
2166
2314
  }
2167
2315
  /** Resolve which project-local host configs to write, mirroring runInit detection. */
2168
2316
  async function resolveHostConfigTargets(deps) {
@@ -2400,9 +2548,10 @@ async function requestTrackedConfigConsent(deps, relPath) {
2400
2548
  * helper governs launcher-token migration only, never credentials.
2401
2549
  */
2402
2550
  export function preserveExistingLauncherArgs(existing, entry) {
2551
+ const existingEntry = asRecord(existing) ?? {};
2403
2552
  const existingArgs = asRecord(existing)?.args;
2404
2553
  const preserved = refreshBridgeApiPackageSpec(existingArgs, currentBridgePackageSpec());
2405
- return preserved ? { ...entry, args: preserved } : entry;
2554
+ return { ...existingEntry, ...entry, ...(preserved ? { args: preserved } : {}) };
2406
2555
  }
2407
2556
  /**
2408
2557
  * Read-only pre-scan for both-key configs across EVERY project target (BAPI-807).
@@ -2457,7 +2606,8 @@ async function writeHostConfigs(deps, targets, entries, trackedState, ctx) {
2457
2606
  // file that will be skipped never triggers a misleading write-consent prompt.
2458
2607
  if (read.state === "invalid") {
2459
2608
  deps.errorLog(formatInvalidConfigNotice(target.relPath));
2460
- for (const line of formatSecretFreeManualMerge(target.relPath, target.topLevelKey, entries.secretFree)) {
2609
+ const secretFreeEntry = adaptBridgeEntryForHostTarget(entries.secretFree, target.target);
2610
+ for (const line of formatSecretFreeManualMerge(target.relPath, target.topLevelKey, secretFreeEntry)) {
2461
2611
  deps.errorLog(line);
2462
2612
  }
2463
2613
  skipped.push({ relPath: target.relPath, reason: "invalid" });
@@ -2511,7 +2661,8 @@ async function writeHostConfigs(deps, targets, entries, trackedState, ctx) {
2511
2661
  // the full template args, `serve` included. The rule is identical for the
2512
2662
  // real-key and secret-free variants: credential safety must not decide whether
2513
2663
  // a launcher is migrated.
2514
- topLevel[registrationKey] = preserveExistingLauncherArgs(topLevel[registrationKey], entry);
2664
+ const physicalEntry = adaptBridgeEntryForHostTarget(entry, target.target);
2665
+ topLevel[registrationKey] = preserveExistingLauncherArgs(topLevel[registrationKey], physicalEntry);
2515
2666
  config[target.topLevelKey] = topLevel;
2516
2667
  await deps.mkdir(path.dirname(fullPath), { recursive: true });
2517
2668
  await deps.writeFile(fullPath, JSON.stringify(config, null, 2) + "\n", {
@@ -2522,7 +2673,7 @@ async function writeHostConfigs(deps, targets, entries, trackedState, ctx) {
2522
2673
  return { written, skipped, duplicateRegistrations: [] };
2523
2674
  }
2524
2675
  /**
2525
- * Provision the selected GLOBAL (Codex, Copilot CLI) and MANUAL (Windsurf)
2676
+ * Provision the selected GLOBAL (Codex) and MANUAL (Windsurf)
2526
2677
  * targets through the registry-driven emitter (BAPI-635). Project JSON targets
2527
2678
  * are handled by {@link writeHostConfigs}; this covers everything else. Global
2528
2679
  * config paths are never added to the repository .gitignore. Returns secret-free
@@ -3084,6 +3235,15 @@ export const INSTALL_BRIDGE_VERIFY_THROTTLED = "Bridge is at capacity and could
3084
3235
  * hide a real outage behind a version-skew story.
3085
3236
  */
3086
3237
  export const INSTALL_BRIDGE_VERIFY_SERVER_ERROR = "Error: Bridge could not complete verification right now. Nothing was created — wait a few minutes and re-run this command; the code already emailed to you stays valid until it expires.";
3238
+ /**
3239
+ * BAPI-1173: printed after a successful verification whose response says, with an
3240
+ * explicit `trial_granted: false`, that this email already used its one free trial.
3241
+ *
3242
+ * Text only — deliberately no URL. The canonical purchase link is authored once,
3243
+ * server-side, and the first metered-work denial already delivers it. Non-metered
3244
+ * work still runs, so this informs rather than blocks.
3245
+ */
3246
+ export const INSTALL_BRIDGE_TRIAL_ALREADY_USED = "Your free trial was already used - this project is on the metered plan; add payment to run metered work.";
3087
3247
  /**
3088
3248
  * Local cap on rejected verification attempts before the CLI stops re-prompting.
3089
3249
  *
@@ -3122,6 +3282,25 @@ export const INSTALL_BRIDGE_SIGNUP_UPGRADE_REQUIRED = [
3122
3282
  "",
3123
3283
  ` npx -y ${MCP_PACKAGE_NAME}@latest install --email <addr>`,
3124
3284
  ].join("\n");
3285
+ /**
3286
+ * HTTP 403 on the self-serve request or verify call (BAPI-1171).
3287
+ *
3288
+ * Deliberately distinct from {@link INSTALL_BRIDGE_SIGNUP_UPGRADE_REQUIRED}: a 403
3289
+ * means the request was refused before signup logic ran at all — an upgrade would
3290
+ * not change the outcome, so the message names the actual cause instead.
3291
+ */
3292
+ export const INSTALL_BRIDGE_SIGNUP_BLOCKED = "Error: Bridge signup is not available from your current network — your network/IP address " +
3293
+ "appears to be blocked. Try again from a different network, or contact Bridge if you believe " +
3294
+ "this is a mistake.";
3295
+ /**
3296
+ * A self-serve HTTP status this client has no specific handling for (BAPI-1171).
3297
+ *
3298
+ * Carries only the numeric status — never response text or a body, which could
3299
+ * echo back the email address or another value this client must not print.
3300
+ */
3301
+ export function formatSelfServeUnexpectedStatus(status) {
3302
+ return `Error: Bridge signup returned an unexpected response (status ${status}). Try again in a few minutes.`;
3303
+ }
3125
3304
  /**
3126
3305
  * Mask an address for display: first character, then the domain.
3127
3306
  *
@@ -3156,7 +3335,12 @@ export function formatChallengeRemaining(expiresAtIso, nowMs) {
3156
3335
  return "This code has expired.";
3157
3336
  if (minutes === 1)
3158
3337
  return "This code is valid for about another minute.";
3159
- return `This code is valid for about another ${minutes} minutes.`;
3338
+ if (minutes < 60)
3339
+ return `This code is valid for about another ${minutes} minutes.`;
3340
+ if (minutes < 120)
3341
+ return "This code is valid for about another hour.";
3342
+ const hours = Math.floor(minutes / 60);
3343
+ return `This code is valid for about another ${hours} hours.`;
3160
3344
  }
3161
3345
  /**
3162
3346
  * Prompt for the verification code, validating its shape LOCALLY before any
@@ -3246,8 +3430,11 @@ export async function requestSelfServeChallenge(deps, baseUrl, email) {
3246
3430
  });
3247
3431
  }
3248
3432
  catch (err) {
3433
+ // BAPI-1171: the fetch itself failed (DNS, TLS, connection refused, timeout) —
3434
+ // distinct from the server responding with an error status. The caught value
3435
+ // is never inspected; only its category is reported.
3249
3436
  void err;
3250
- return { ok: false, category: "failed" };
3437
+ return { ok: false, category: "network-error" };
3251
3438
  }
3252
3439
  if (resp.ok) {
3253
3440
  let handle;
@@ -3278,7 +3465,12 @@ export async function requestSelfServeChallenge(deps, baseUrl, email) {
3278
3465
  return { ok: false, category: "unavailable" };
3279
3466
  if (resp.status === 400 || resp.status === 422)
3280
3467
  return { ok: false, category: "invalid" };
3281
- return { ok: false, category: "failed" };
3468
+ // BAPI-1171: 403 means the caller's network/IP was refused outright — distinct
3469
+ // from every status above, and from a genuinely unhandled status below. Neither
3470
+ // branch reads the response body.
3471
+ if (resp.status === 403)
3472
+ return { ok: false, category: "blocked" };
3473
+ return { ok: false, category: "unexpected", status: resp.status };
3282
3474
  }
3283
3475
  /**
3284
3476
  * POST the VERIFY phase: a handle and a code in, the invite token out.
@@ -3297,14 +3489,18 @@ export async function verifySelfServeChallenge(deps, baseUrl, handle, code) {
3297
3489
  });
3298
3490
  }
3299
3491
  catch (err) {
3492
+ // BAPI-1171: the fetch itself failed — distinct from the server responding
3493
+ // with an error status. The caught value is never inspected.
3300
3494
  void err;
3301
- return { ok: false, category: "failed" };
3495
+ return { ok: false, category: "network-error" };
3302
3496
  }
3303
3497
  if (resp.ok) {
3304
3498
  let token;
3499
+ let trialGranted;
3305
3500
  try {
3306
3501
  const body = (await resp.json());
3307
3502
  token = body?.token;
3503
+ trialGranted = body?.trial_granted;
3308
3504
  }
3309
3505
  catch {
3310
3506
  return { ok: false, category: "failed" };
@@ -3314,6 +3510,12 @@ export async function verifySelfServeChallenge(deps, baseUrl, handle, code) {
3314
3510
  !token.startsWith(BOOTSTRAP_INVITE_TOKEN_PREFIX)) {
3315
3511
  return { ok: false, category: "failed" };
3316
3512
  }
3513
+ // BAPI-1173: forward-compatible. Only a real boolean is carried; an absent
3514
+ // field (an older server) or a malformed one leaves the result exactly the
3515
+ // legacy `{ ok: true, token }` shape rather than being coerced to false.
3516
+ if (typeof trialGranted === "boolean") {
3517
+ return { ok: true, token, trialGranted };
3518
+ }
3317
3519
  return { ok: true, token };
3318
3520
  }
3319
3521
  // 401 is the server's uniform invalid-challenge outcome. It covers a wrong code,
@@ -3328,7 +3530,12 @@ export async function verifySelfServeChallenge(deps, baseUrl, handle, code) {
3328
3530
  // caller can say "try again" instead of "upgrade the CLI".
3329
3531
  if (resp.status >= 500)
3330
3532
  return { ok: false, category: "server-error" };
3331
- return { ok: false, category: "failed" };
3533
+ // BAPI-1171: 403 means the caller's network/IP was refused outright — distinct
3534
+ // from every status above, and from a genuinely unhandled status below. Neither
3535
+ // branch reads the response body.
3536
+ if (resp.status === 403)
3537
+ return { ok: false, category: "blocked" };
3538
+ return { ok: false, category: "unexpected", status: resp.status };
3332
3539
  }
3333
3540
  /**
3334
3541
  * POST the RESEND phase: the handle alone, timing metadata out.
@@ -3630,6 +3837,7 @@ export function buildDryRunPreview(ctx) {
3630
3837
  ...credentialStep,
3631
3838
  ...buildCommitAssetsPreviewLines(ctx.commitSafeAssets, ctx.host.kind === "explicit" ? ctx.host.configTargets : []),
3632
3839
  ...(ctx.host.kind === "explicit" ? buildLaunchStepPreview(ctx.host.launch) : buildDeferredLaunchStepPreview()),
3840
+ ...(ctx.host.kind === "explicit" ? buildAgentNotesPreviewLines(ctx.host.notesTargets ?? []) : []),
3633
3841
  ];
3634
3842
  }
3635
3843
  /**
@@ -3729,6 +3937,25 @@ function buildDeferredLaunchStepPreview() {
3729
3937
  " not yet known — a real run resolves it first, then follows from there).",
3730
3938
  ];
3731
3939
  }
3940
+ /**
3941
+ * The optional agent-notes offer preview line (BAPI-1207). A --dry-run never
3942
+ * prompts or writes; when the selection resolves to at least one writable
3943
+ * notes target this states plainly that a real interactive run would ask
3944
+ * default-No consent before touching it. Empty when the selection resolves to
3945
+ * no writable target (e.g. Windsurf-only), mirroring how the GitHub offer
3946
+ * lines are unconditional but this offer's applicability is not.
3947
+ */
3948
+ function buildAgentNotesPreviewLines(notesTargets) {
3949
+ if (notesTargets.length === 0)
3950
+ return [];
3951
+ return [
3952
+ "Step 4c — optional: cross-platform agent notes (SKIPPED in --dry-run): if the terminal is",
3953
+ ` interactive, offer to create or refresh a Bridge-owned marker region in ${notesTargets.join(" and ")}`,
3954
+ " with situation-to-tool guidance for a few Bridge MCP tools; defaults to No, and",
3955
+ " existing content outside that region is always preserved. No notes files are",
3956
+ " written in --dry-run.",
3957
+ ];
3958
+ }
3732
3959
  /**
3733
3960
  * Bootstrap-invite dry-run preview. A --dry-run must not consume the invite OR
3734
3961
  * LEAVE STATE BEHIND: skipping the HTTP call is not enough — a preview that wrote
@@ -3809,6 +4036,7 @@ function buildBootstrapDryRunPreview(ctx) {
3809
4036
  ...promoteStep,
3810
4037
  ...buildCommitAssetsPreviewLines(ctx.commitSafeAssets, ctx.host.kind === "explicit" ? ctx.host.configTargets : []),
3811
4038
  ...(ctx.host.kind === "explicit" ? buildLaunchStepPreview(ctx.host.launch) : buildDeferredLaunchStepPreview()),
4039
+ ...(ctx.host.kind === "explicit" ? buildAgentNotesPreviewLines(ctx.host.notesTargets ?? []) : []),
3812
4040
  ];
3813
4041
  }
3814
4042
  /**
@@ -4150,6 +4378,30 @@ export const INSTALL_BRIDGE_NO_GIT_NONINTERACTIVE_WARNING = `Warning: ${INSTALL_
4150
4378
  async function hasProjectRootMarker(deps) {
4151
4379
  return sharedHasProjectRootMarker(deps.cwd, deps.stat);
4152
4380
  }
4381
+ /**
4382
+ * Resolve, then live-validate, the premium model alias to pin the spawned
4383
+ * install-spawn session to (BAPI-1206). Fail-open at every step: an agent with
4384
+ * no model override support, a validation failure, or an unavailable live
4385
+ * probe all resolve to `null` (omit `--model`, use the agent default) rather
4386
+ * than aborting installation or spawning. For a statically allowlisted agent
4387
+ * (e.g. claude), {@link validateResolvedModelAliasForAgent} returns the
4388
+ * candidate unchanged without probing anything — only an agent like
4389
+ * `cursor-agent`, which has no `staticModelAliasAllowlist`, triggers the live
4390
+ * advertised-model check. A non-null candidate that fails validation logs a
4391
+ * secret-free diagnostic through `deps.debugLog` (silent unless
4392
+ * `BAPI_INSTALL_DEBUG` is set) and is never surfaced as a user-facing warning.
4393
+ */
4394
+ export async function resolveValidatedPremiumModelAlias(deps, agent) {
4395
+ const candidate = resolveModelAlias(agent, "premium");
4396
+ if (!candidate)
4397
+ return null;
4398
+ const validation = await validateResolvedModelAliasForAgent(deps.startTicketsDeps, agent, candidate);
4399
+ if (!validation.ok) {
4400
+ deps.debugLog(`install-bridge: ${validation.warning}`);
4401
+ return null;
4402
+ }
4403
+ return validation.alias;
4404
+ }
4153
4405
  // ---------------------------------------------------------------------------
4154
4406
  // CLI entry
4155
4407
  // ---------------------------------------------------------------------------
@@ -4203,9 +4455,12 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
4203
4455
  // be an argument error. Deliberately after the deps merge and the base-URL
4204
4456
  // resolution above, so the nested command inherits the same final merged deps
4205
4457
  // (including a test's `fetch` override) and the same normalized base URL.
4206
- // Note this is `install-bridge conductor`; the bare top-level `conductor`
4207
- // subcommand still routes to `runConductorCli` (the local event ledger) and is
4208
- // untouched.
4458
+ // Note this is `install-bridge conductor`; it is UNRELATED to the top-level
4459
+ // `conductor`/`epic-implementer` CLI split (S4/BAPI-1080) the bare
4460
+ // top-level `conductor` token is now a fixed migration stub and the real
4461
+ // event-ledger CLI routes through `runEpicImplementerCli` under the
4462
+ // `epic-implementer` name. This nested `install-bridge conductor` surface is
4463
+ // untouched by that rename.
4209
4464
  if (argv[0] === "conductor") {
4210
4465
  if (!baseUrlResult.ok) {
4211
4466
  errorLog(baseUrlResult.error);
@@ -4339,10 +4594,11 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
4339
4594
  errorLog(`Error: no launch agent is registered for '${launchDecision.agent}'.`);
4340
4595
  return 1;
4341
4596
  }
4597
+ const modelAlias = await resolveValidatedPremiumModelAlias(deps, spec);
4342
4598
  launch = {
4343
4599
  kind: "spawn",
4344
4600
  agent: launchDecision.agent,
4345
- spawnCommand: deps.buildShellCommand(spec, INSTALL_BRIDGE_AGENT_PROMPT, deps.cwd, deps.platform),
4601
+ spawnCommand: deps.buildShellCommand(spec, INSTALL_BRIDGE_AGENT_PROMPT, deps.cwd, deps.platform, modelAlias),
4346
4602
  };
4347
4603
  }
4348
4604
  else if (launchDecision.kind === "choose-one") {
@@ -4356,6 +4612,7 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
4356
4612
  configTargets: dryRunTargets.map((t) => t.relPath),
4357
4613
  manualEditors: manualEditorNames(await detectManualEditors(deps)),
4358
4614
  launch,
4615
+ notesTargets: resolveAgentNotesTargets(dryRunPlatforms).targets,
4359
4616
  };
4360
4617
  }
4361
4618
  else {
@@ -4658,8 +4915,8 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
4658
4915
  // BAPI-635: resolve the AI-coding-tool selection FIRST — BAPI-657 makes this
4659
4916
  // selection (not a hidden --agent default) drive which post-install session opens.
4660
4917
  // Explicit --tools, then the TTY multi-select, then the legacy non-TTY automatic
4661
- // set. The selected project JSON targets drive the read-merge-write path; selected
4662
- // global (Codex / Copilot CLI) and manual (Windsurf) targets are provisioned by the
4918
+ // set. The selected project JSON targets drive the read-merge-write path; a selected
4919
+ // global (Codex) or manual (Windsurf) target is provisioned by the
4663
4920
  // registry-driven emitter after the connectivity check.
4664
4921
  const selectedPlatforms = await resolveSelectedHostPlatforms(deps, options);
4665
4922
  const targets = hostConfigTargetsForPlatforms(selectedPlatforms);
@@ -4680,10 +4937,11 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
4680
4937
  errorLog(`Error: no launch agent is registered for '${launchDecision.agent}'.`);
4681
4938
  return 1;
4682
4939
  }
4940
+ const modelAlias = await resolveValidatedPremiumModelAlias(deps, spec);
4683
4941
  planLaunch = {
4684
4942
  kind: "spawn",
4685
4943
  agent: launchDecision.agent,
4686
- spawnCommand: deps.buildShellCommand(spec, INSTALL_BRIDGE_AGENT_PROMPT, deps.cwd, deps.platform),
4944
+ spawnCommand: deps.buildShellCommand(spec, INSTALL_BRIDGE_AGENT_PROMPT, deps.cwd, deps.platform, modelAlias),
4687
4945
  };
4688
4946
  }
4689
4947
  else if (launchDecision.kind === "choose-one") {
@@ -4738,7 +4996,8 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
4738
4996
  return 1;
4739
4997
  }
4740
4998
  finalAgentName = chosen;
4741
- finalSpawnCommand = deps.buildShellCommand(spec, INSTALL_BRIDGE_AGENT_PROMPT, deps.cwd, deps.platform);
4999
+ const modelAlias = await resolveValidatedPremiumModelAlias(deps, spec);
5000
+ finalSpawnCommand = deps.buildShellCommand(spec, INSTALL_BRIDGE_AGENT_PROMPT, deps.cwd, deps.platform, modelAlias);
4742
5001
  }
4743
5002
  // chosen === null → non-TTY / unavailable / invalid → manual continuation below.
4744
5003
  }
@@ -4936,8 +5195,24 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
4936
5195
  else if (requested.category === "invalid") {
4937
5196
  reportFatal("Error: Self-serve setup could not be requested. Check the email value and try again.");
4938
5197
  }
5198
+ else if (requested.category === "blocked") {
5199
+ // BAPI-1171: the request was refused before signup logic ran — an
5200
+ // upgrade would not change the outcome, so this is NOT protocol
5201
+ // guidance.
5202
+ reportFatal(INSTALL_BRIDGE_SIGNUP_BLOCKED);
5203
+ }
5204
+ else if (requested.category === "unexpected") {
5205
+ // BAPI-1171: an HTTP status this client has no specific handling
5206
+ // for. Reports only the numeric status — never response text.
5207
+ reportFatal(formatSelfServeUnexpectedStatus(requested.status ?? 0));
5208
+ }
5209
+ else if (requested.category === "network-error") {
5210
+ // BAPI-1171: the fetch itself failed — a transport problem, not a
5211
+ // protocol mismatch, so this is NOT upgrade guidance either.
5212
+ reportFatal(INSTALL_BRIDGE_SIGNUP_UNAVAILABLE);
5213
+ }
4939
5214
  else {
4940
- // R-2: a malformed or tokenless-unexpected response is a PROTOCOL
5215
+ // R-2: a malformed or tokenless success body is a PROTOCOL
4941
5216
  // mismatch, not a network problem. Saying "check connectivity" here
4942
5217
  // was the old behavior and is exactly what this ticket replaces.
4943
5218
  reportFatal(INSTALL_BRIDGE_SIGNUP_UPGRADE_REQUIRED);
@@ -4986,6 +5261,11 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
4986
5261
  const verified = await verifySelfServeChallenge(deps, baseUrl, challengeHandle, code);
4987
5262
  if (verified.ok) {
4988
5263
  inviteToken = verified.token;
5264
+ // BAPI-1173: only an EXPLICIT false prints. `true`, and an absent field
5265
+ // from an older server, add nothing.
5266
+ if (verified.trialGranted === false) {
5267
+ log(` ${INSTALL_BRIDGE_TRIAL_ALREADY_USED}`);
5268
+ }
4989
5269
  break;
4990
5270
  }
4991
5271
  if (verified.category === "rate-limited") {
@@ -4999,10 +5279,28 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
4999
5279
  reportFatal(INSTALL_BRIDGE_VERIFY_SERVER_ERROR);
5000
5280
  return false;
5001
5281
  }
5282
+ if (verified.category === "blocked") {
5283
+ // BAPI-1171: the request was refused before verification logic ran —
5284
+ // an upgrade would not change the outcome.
5285
+ reportFatal(INSTALL_BRIDGE_SIGNUP_BLOCKED);
5286
+ return false;
5287
+ }
5288
+ if (verified.category === "unexpected") {
5289
+ // BAPI-1171: an HTTP status this client has no specific handling for.
5290
+ // Reports only the numeric status — never response text.
5291
+ reportFatal(formatSelfServeUnexpectedStatus(verified.status ?? 0));
5292
+ return false;
5293
+ }
5294
+ if (verified.category === "network-error") {
5295
+ // BAPI-1171: the fetch itself failed — a transport problem, not a
5296
+ // protocol mismatch.
5297
+ reportFatal(INSTALL_BRIDGE_VERIFY_SERVER_ERROR);
5298
+ return false;
5299
+ }
5002
5300
  if (verified.category === "failed") {
5003
- // Only a genuine protocol mismatch reaches here now: a malformed body,
5004
- // a missing token, a network error, or a 4xx this client does not
5005
- // expect. R-2's upgrade instruction is the right answer for those.
5301
+ // Only a genuine protocol mismatch reaches here now: a malformed body
5302
+ // or a missing token in an otherwise successful response. R-2's
5303
+ // upgrade instruction is the right answer for that.
5006
5304
  reportFatal(INSTALL_BRIDGE_SIGNUP_UPGRADE_REQUIRED);
5007
5305
  return false;
5008
5306
  }
@@ -5377,7 +5675,7 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
5377
5675
  for (const { relPath } of writeResult.skipped) {
5378
5676
  log(` skipped ${relPath} — existing config could not be parsed safely; left untouched`);
5379
5677
  }
5380
- // BAPI-635: provision selected GLOBAL targets (Codex, Copilot CLI) and MANUAL
5678
+ // BAPI-635: provision a selected GLOBAL target (Codex) and MANUAL
5381
5679
  // targets (Windsurf) via the registry-driven emitter. Global paths are never
5382
5680
  // added to the repository .gitignore.
5383
5681
  // BAPI-669 (R8): only a need-key run has a key the user has never seen, so only it
@@ -5413,12 +5711,6 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
5413
5711
  const manualInstructions = buildManualHostInstructions(entry, legacyManualEditors, needKeyManual);
5414
5712
  if (manualInstructions)
5415
5713
  log(manualInstructions);
5416
- // BAPI-635 (Step 7): when both Claude Code and Copilot CLI are selected, warn
5417
- // that they use different, non-shared config surfaces.
5418
- if (selectedPlatforms.includes("claude-code") && selectedPlatforms.includes("copilot-cli")) {
5419
- log(" Note: Claude Code uses the project .mcp.json while GitHub Copilot CLI uses only its " +
5420
- "global ~/.copilot/mcp-config.json — the two are configured separately.");
5421
- }
5422
5714
  // BAPI-708 (B-c): the Claude trust reminder that used to print here was
5423
5715
  // RELOCATED, not dropped — the reload requirement is genuinely actionable (the
5424
5716
  // server does not work until the host reloads), but at Step 3 it duplicated the
@@ -5523,15 +5815,56 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
5523
5815
  // the agent spawn, for two reasons: the spawned session's capability report should
5524
5816
  // observe GitHub as configured if the user connects it here, and running it after the
5525
5817
  // spawn would put two prompts on the same terminal at once.
5526
- await offerGithubConnection(repoName, baseUrl, deps, log);
5818
+ //
5819
+ // BAPI-1171: one shared session-state object, constructed fresh for this run, is
5820
+ // passed to both offers so a GitHub connection made here suppresses the Bitbucket
5821
+ // offer that follows — see `InstallBridgeVcsSessionState`.
5822
+ const vcsSessionState = { vcsConnected: false };
5823
+ await offerGithubConnection(repoName, baseUrl, deps, log, vcsSessionState);
5527
5824
  // ---- optional Bitbucket connect offer (BAPI-966) ----
5528
5825
  // Same placement reasoning as the GitHub offer above. Both offers are called
5529
5826
  // unconditionally here — each self-gates on its OWN provider-scoped configuration
5530
5827
  // state (`fetchGithubConfigurationState`/`fetchBitbucketConfigurationState` return
5531
5828
  // "unavailable" when their integration id is absent from the manifest, which is the
5532
5829
  // normal outcome for the non-matching provider), so only the project's actual VCS
5533
- // provider ever reaches its prompt.
5534
- await offerBitbucketConnection(repoName, baseUrl, deps, log);
5830
+ // provider ever reaches its prompt — and the GitHub offer's own success, tracked in
5831
+ // `vcsSessionState`, can additionally short-circuit this one before either check runs.
5832
+ await offerBitbucketConnection(repoName, baseUrl, deps, log, vcsSessionState);
5833
+ // ---- optional cross-platform agent notes offer (BAPI-1207) ----
5834
+ // Placed alongside the GitHub/Bitbucket offers, after the durable install work —
5835
+ // never re-detects platforms; the plan is computed from THIS run's already
5836
+ // selected set. `writeAgentNotes` never touches `writeMcpInstallState` or any
5837
+ // other persisted install-state contract — consent is re-evaluated every run.
5838
+ const notesPlan = resolveAgentNotesTargets(selectedPlatforms);
5839
+ const notesOffer = await offerAgentGuidanceNotes(deps, log, notesPlan.targets);
5840
+ let notesResult;
5841
+ let notesWriteFailed = false;
5842
+ if (notesOffer.kind === "consented") {
5843
+ try {
5844
+ notesResult = await writeAgentNotes({
5845
+ cwd: deps.cwd,
5846
+ selectedPlatforms,
5847
+ guidance: AGENT_GUIDANCE,
5848
+ readFile: deps.readFile,
5849
+ writeFile: deps.writeFile,
5850
+ mkdir: deps.mkdir,
5851
+ });
5852
+ for (const file of notesResult.files) {
5853
+ if (file.status === "failed") {
5854
+ errorLog(` warning: could not update ${file.target} with agent notes (${file.reason}).`);
5855
+ }
5856
+ }
5857
+ }
5858
+ catch {
5859
+ // Fail-open, mirroring the routing-credential persistence above: the
5860
+ // install itself is already complete and durable.
5861
+ notesWriteFailed = true;
5862
+ errorLog(" warning: could not write agent notes (unexpected error).");
5863
+ }
5864
+ }
5865
+ const notesSummaryLine = buildAgentNotesSummaryLine(notesOffer, notesResult, notesWriteFailed, notesPlan.unsupportedPlatforms);
5866
+ if (notesSummaryLine)
5867
+ log(notesSummaryLine);
5535
5868
  // ---- Step 3b (await) — settle the background pre-warm before the session opens ----
5536
5869
  // The warm was kicked off (only on a spawn-capable path) right after the
5537
5870
  // launch-command guards; await it HERE so the bucket is settled before the spawned