@bridge_gpt/mcp-server 0.2.54 → 0.2.56

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 (89) hide show
  1. package/README.md +45 -59
  2. package/build/agent-capabilities/types.js +4 -0
  3. package/build/agent-guidance.generated.js +3 -0
  4. package/build/agent-launchers/claude-executor-adapter.js +3 -0
  5. package/build/agent-notes.js +178 -0
  6. package/build/agent-registry.js +25 -5
  7. package/build/agent-utils.js +58 -0
  8. package/build/agents.generated.js +1 -1
  9. package/build/codex-launch-scope.js +49 -0
  10. package/build/codex-skill-adapter.js +58 -0
  11. package/build/command-host.js +9 -0
  12. package/build/command-provisioning.js +35 -4
  13. package/build/commands.generated.js +9 -8
  14. package/build/conductor/bridge-api-client.js +199 -6
  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 +1 -1
  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 +37 -6
  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/store.js +2 -2
  32. package/build/conductor/supervisor-runtime.js +1 -1
  33. package/build/conductor-bin.js +2 -139
  34. package/build/conductor-claude-hook-bin.js +2 -2
  35. package/build/conductor-claude-hook-removed-stub-bin.js +31 -0
  36. package/build/conductor-removed-stub-bin.js +30 -0
  37. package/build/docs.generated.js +1 -1
  38. package/build/doctor.js +81 -18
  39. package/build/drive-epic.js +541 -115
  40. package/build/epic-implementer-bin.js +145 -0
  41. package/build/epic-implementer-bundle-cli.js +264 -0
  42. package/build/epic-implementer-claude-hook-bin.js +3 -0
  43. package/build/epic-integration-pr.js +5 -3
  44. package/build/executor/env.js +6 -0
  45. package/build/executor/interrupted-worktree.js +60 -0
  46. package/build/executor/job-errors.js +45 -0
  47. package/build/executor/job-runner.js +274 -9
  48. package/build/executor/job-types.js +25 -9
  49. package/build/executor/merge-tree-classifier.js +171 -0
  50. package/build/executor/reconcile-overlap-governance.js +129 -0
  51. package/build/executor/reconcile-overlap-job.js +989 -0
  52. package/build/executor/reconcile-overlap-types.js +14 -0
  53. package/build/executor/spawn-job-driver.js +1 -0
  54. package/build/executor/types.js +2 -0
  55. package/build/executor/worker-finalization.js +25 -2
  56. package/build/executor/worker-guard-hook.js +15 -7
  57. package/build/implement-epic/bridge-client.js +773 -0
  58. package/build/implement-epic/checkpoint-store.js +542 -0
  59. package/build/implement-epic/cli.js +3158 -0
  60. package/build/implement-epic/cut-protocol.js +392 -0
  61. package/build/implement-epic/lock.js +302 -0
  62. package/build/implement-epic/pr-state.js +286 -0
  63. package/build/implement-epic/spawn.js +113 -0
  64. package/build/index.js +617 -168
  65. package/build/init.js +74 -9
  66. package/build/install-bridge-conductor.js +5 -5
  67. package/build/install-bridge.js +425 -79
  68. package/build/mcp-host-config.js +22 -60
  69. package/build/mcp-host-entry-adapter.js +18 -0
  70. package/build/mcp-host-targets.js +19 -32
  71. package/build/mcp-provisioning.js +1 -1
  72. package/build/mcp-registration-doctor.js +1 -1
  73. package/build/merge-pull-request.js +1 -1
  74. package/build/pipelines.generated.js +7 -7
  75. package/build/plan-epic-conductor-eligibility.js +1 -1
  76. package/build/plane/cli.js +36 -5
  77. package/build/plane/preflight.js +128 -12
  78. package/build/plane/shutdown.js +4 -4
  79. package/build/readiness-check.js +3 -3
  80. package/build/readme.generated.js +1 -1
  81. package/build/review-tickets.js +50 -35
  82. package/build/run-unit-tests-launcher.js +1 -1
  83. package/build/setup-epic.js +69 -31
  84. package/build/start-tickets-conductor.js +8 -7
  85. package/build/start-tickets.js +266 -48
  86. package/build/version.generated.js +3 -3
  87. package/build/worker-guard-hook-bin.js +1 -1
  88. package/docs/CONDUCTOR.md +8 -6
  89. package/package.json +5 -3
@@ -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
  "",
@@ -493,7 +530,7 @@ export function getInstallBridgeUsage(baseUrl = DEFAULT_BAPI_BASE_URL) {
493
530
  " prompting, or spawning anything. With --invite it",
494
531
  " also never calls the exchange endpoint and never",
495
532
  " generates or stores a secret.",
496
- " --agent claude|cursor-agent Explicit launch override for the post-install",
533
+ " --agent claude|cursor-agent|codex Explicit agent choice for post-install",
497
534
  " session — it always wins, and there is NO Claude",
498
535
  " default. Without it the launch tool is derived",
499
536
  " from your selection: Claude Code opens `claude`.",
@@ -502,9 +539,11 @@ export function getInstallBridgeUsage(baseUrl = DEFAULT_BAPI_BASE_URL) {
502
539
  " session); selecting Cursor writes .cursor/mcp.json",
503
540
  " and prints how to finish by running /install-bridge",
504
541
  " in a new Cursor session. A selection whose tools have",
505
- " no agentic CLI (e.g. Copilot) likewise opens nothing",
542
+ " no supported auto-launch path (e.g. Copilot) opens nothing",
506
543
  " and prints how to finish configuring later. Passing",
507
- " --agent cursor-agent still force-spawns it.",
544
+ " --agent cursor-agent still force-spawns it. Codex",
545
+ " is accepted only for manual continuation: neither",
546
+ " this bootstrap nor start-tickets launches it yet.",
508
547
  " -h, --help Show this help.",
509
548
  "",
510
549
  "Subcommands:",
@@ -827,7 +866,7 @@ export const INSTALL_BRIDGE_GITHUB_CONNECT_PROMPT = "Connect GitHub? [y/N]: ";
827
866
  * Reuses the shared connect-github flow rather than duplicating the API, polling,
828
867
  * browser, or picker logic — there is exactly one implementation of that handshake.
829
868
  */
830
- export async function offerGithubConnection(repoName, baseUrl, deps, log) {
869
+ export async function offerGithubConnection(repoName, baseUrl, deps, log, session) {
831
870
  // No prompt surface → no offer. Never assume consent on a non-interactive run.
832
871
  if (!deps.isTTY || !deps.promptLine)
833
872
  return;
@@ -875,7 +914,12 @@ export async function offerGithubConnection(repoName, baseUrl, deps, log) {
875
914
  return; // declining is a normal outcome
876
915
  const connectDeps = createDefaultConnectGithubDeps();
877
916
  const code = await runGithubConnectionFlow(connectDeps, api, repoName);
878
- if (code !== 0) {
917
+ if (code === 0) {
918
+ // BAPI-1171: only a CONFIRMED success suppresses the Bitbucket offer below —
919
+ // a decline, failure, or thrown error must never look like a connection.
920
+ session.vcsConnected = true;
921
+ }
922
+ else {
879
923
  log(" note: GitHub was not connected. Your install is complete — connect GitHub later with " +
880
924
  `'npx -y ${MCP_PACKAGE_NAME}@latest connect-github --repo ${repoName}'.`);
881
925
  }
@@ -915,10 +959,15 @@ export const INSTALL_BRIDGE_BITBUCKET_CONNECT_PROMPT = "Connect Bitbucket? [y/N]
915
959
  * Reuses the shared connect-bitbucket flow rather than duplicating the API, polling,
916
960
  * browser, or picker logic — there is exactly one implementation of that handshake.
917
961
  */
918
- export async function offerBitbucketConnection(repoName, baseUrl, deps, log) {
962
+ export async function offerBitbucketConnection(repoName, baseUrl, deps, log, session) {
919
963
  // No prompt surface → no offer. Never assume consent on a non-interactive run.
920
964
  if (!deps.isTTY || !deps.promptLine)
921
965
  return;
966
+ // BAPI-1171: a GitHub connection made earlier in THIS run already leaves the
967
+ // project with a connected VCS provider — skip provider lookup, prompting, the
968
+ // browser flow, and the connection handshake entirely rather than asking again.
969
+ if (session.vcsConnected)
970
+ return;
922
971
  try {
923
972
  const credDeps = {
924
973
  env: deps.env,
@@ -971,6 +1020,88 @@ export async function offerBitbucketConnection(repoName, baseUrl, deps, log) {
971
1020
  `Bitbucket later with 'npx -y ${MCP_PACKAGE_NAME}@latest connect-bitbucket --repo ${repoName}'.`);
972
1021
  }
973
1022
  }
1023
+ /** Default-No agent-notes offer prompt. Mirrors {@link INSTALL_BRIDGE_GITHUB_CONNECT_PROMPT}. */
1024
+ export const INSTALL_BRIDGE_AGENT_NOTES_PROMPT = "Add cross-platform agent notes? [y/N]: ";
1025
+ /**
1026
+ * The offer-context lines printed immediately before the agent-notes prompt
1027
+ * (BAPI-1207). Names the applicable file(s) and states the marker-scoped,
1028
+ * non-destructive contract up front — mirrors the GitHub/Bitbucket offers'
1029
+ * "explain before asking" convention.
1030
+ */
1031
+ export function buildAgentNotesOfferContext(targets) {
1032
+ return [
1033
+ "",
1034
+ "Step 4c — optional: cross-platform agent notes.",
1035
+ ` This creates or refreshes a Bridge-owned region in ${targets.join(" and ")} with situation-`,
1036
+ " to-tool guidance for a few Bridge MCP tools. Only that marked region is created or",
1037
+ " refreshed — any existing content in the file, inside or outside it, is otherwise preserved.",
1038
+ ];
1039
+ }
1040
+ /**
1041
+ * Offer to create or refresh the cross-platform agent notes, if the selected
1042
+ * platforms resolve to at least one writable target (BAPI-1207).
1043
+ *
1044
+ * Entirely best-effort and non-destructive, mirroring
1045
+ * {@link offerGithubConnection}: by the time this runs the install itself is
1046
+ * already complete and durable, so nothing here may fail the run. This
1047
+ * function only resolves the CONSENT decision — the caller performs the
1048
+ * actual write via {@link writeAgentNotes}, so a `--dry-run` (which never
1049
+ * reaches this call) and the live path share exactly one write path.
1050
+ */
1051
+ export async function offerAgentGuidanceNotes(deps, log, targets) {
1052
+ if (targets.length === 0)
1053
+ return { kind: "not-applicable" };
1054
+ // No prompt surface → no offer. Never assume consent on a non-interactive run.
1055
+ if (!deps.isTTY || !deps.promptLine)
1056
+ return { kind: "headless-skip" };
1057
+ try {
1058
+ for (const line of buildAgentNotesOfferContext(targets))
1059
+ log(line);
1060
+ const answer = (await deps.promptLine(INSTALL_BRIDGE_AGENT_NOTES_PROMPT)).trim().toLowerCase();
1061
+ // Only an explicit yes proceeds — blank input, EOF, "n", and anything malformed
1062
+ // all decline without writing.
1063
+ return answer === "y" || answer === "yes" ? { kind: "consented" } : { kind: "declined" };
1064
+ }
1065
+ catch {
1066
+ // The install is already durable; a failure here is never fatal to it.
1067
+ log(" note: the agent-notes offer could not run. No notes files were written.");
1068
+ return { kind: "declined" };
1069
+ }
1070
+ }
1071
+ /**
1072
+ * One secret-free summary line for the final install output (BAPI-1207). Never
1073
+ * reveals guidance content, file bytes, or a credential — only which files
1074
+ * were touched, skipped, or failed, and which selected platforms have no
1075
+ * supported notes convention (currently only Windsurf).
1076
+ */
1077
+ export function buildAgentNotesSummaryLine(offer, result, writeFailed, unsupportedPlatforms) {
1078
+ const unsupportedNote = unsupportedPlatforms.length > 0
1079
+ ? ` (${unsupportedPlatforms.join(", ")}: no supported agent-notes convention)`
1080
+ : "";
1081
+ if (offer.kind === "not-applicable") {
1082
+ return unsupportedPlatforms.length > 0 ? `Agent notes: not written${unsupportedNote}.` : undefined;
1083
+ }
1084
+ if (offer.kind === "headless-skip") {
1085
+ return `Agent notes: skipped (non-interactive session)${unsupportedNote}.`;
1086
+ }
1087
+ if (offer.kind === "declined") {
1088
+ return `Agent notes: skipped (declined)${unsupportedNote}.`;
1089
+ }
1090
+ if (writeFailed || !result) {
1091
+ return `Agent notes: could not be written (unexpected error)${unsupportedNote}.`;
1092
+ }
1093
+ const written = result.files.filter((f) => f.status === "written").map((f) => f.target);
1094
+ const unchanged = result.files.filter((f) => f.status === "skipped").map((f) => f.target);
1095
+ const failed = result.files.filter((f) => f.status === "failed").map((f) => f.target);
1096
+ const parts = [];
1097
+ if (written.length > 0)
1098
+ parts.push(`written to ${written.join(", ")}`);
1099
+ if (unchanged.length > 0)
1100
+ parts.push(`already up to date in ${unchanged.join(", ")}`);
1101
+ if (failed.length > 0)
1102
+ parts.push(`failed for ${failed.join(", ")}`);
1103
+ return `Agent notes: ${parts.join("; ")}${unsupportedNote}.`;
1104
+ }
974
1105
  /**
975
1106
  * Echoed single-line prompt on stderr (used for repo confirmation / value).
976
1107
  * Exported so `index.ts`'s `--init` can share the exact same prompt mechanics
@@ -1484,11 +1615,18 @@ export async function resolveInviteToken(options, deps) {
1484
1615
  * is never written to a log line; it is only ever echoed as the user's own keystrokes
1485
1616
  * and placed in the mint request body.)
1486
1617
  *
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
1618
+ * Explicit sources (`--email`, `BAPI_SIGNUP_EMAIL`) still take only trimmed
1619
+ * whitespace — matching the other CLI input seams and the server's deliberately
1620
+ * permissive "stored unvalidated" contract (D-2). The interactive prompt, added by
1621
+ * BAPI-1171, is bounded and additionally requires the trimmed value to match a
1622
+ * minimal `local@domain` shape before it is accepted — mirroring the one-hint/
1623
+ * one-re-prompt contract {@link resolveInstallBridgeOnboardingBranchForRun} uses,
1624
+ * so an unanswerable prompt cannot loop forever. The entered value is never echoed
1490
1625
  * back in an error message.
1491
1626
  */
1627
+ const INSTALL_BRIDGE_SIGNUP_EMAIL_SHAPE = /^[^@\s]+@[^@\s]+$/;
1628
+ /** The one hint line printed after a blank/malformed email, before the single re-prompt. */
1629
+ export const INSTALL_BRIDGE_SIGNUP_EMAIL_HINT = "Enter a valid email address (e.g. you@example.com).";
1492
1630
  export async function resolveSignupEmail(options, deps) {
1493
1631
  // BAPI-757 (R-3 / AC-9): self-serve signup now REQUIRES an interactive terminal,
1494
1632
  // and the check runs FIRST — before `--email` and before `BAPI_SIGNUP_EMAIL` —
@@ -1507,9 +1645,19 @@ export async function resolveSignupEmail(options, deps) {
1507
1645
  if (typeof fromEnv === "string" && fromEnv.trim().length > 0) {
1508
1646
  return { ok: true, value: fromEnv.trim() };
1509
1647
  }
1510
- const entered = (await deps.promptLine("Email for Bridge workspace setup: ")).trim();
1511
- if (entered.length > 0) {
1512
- return { ok: true, value: entered };
1648
+ const promptLine = deps.promptLine;
1649
+ // BAPI-1171: bounded to two reads total — the first answer, then ONE re-prompt
1650
+ // after a single hint line. EOF resolves as an empty value (promptLineViaReadline
1651
+ // resolves "" on close), which fails the shape check like any other blank
1652
+ // answer, so it costs one re-prompt and then fails cleanly rather than hanging.
1653
+ for (let attempt = 0; attempt < 2; attempt += 1) {
1654
+ const raw = await promptLine("Email for Bridge workspace setup (required): ");
1655
+ const entered = String(raw ?? "").trim();
1656
+ if (INSTALL_BRIDGE_SIGNUP_EMAIL_SHAPE.test(entered)) {
1657
+ return { ok: true, value: entered };
1658
+ }
1659
+ if (attempt === 0)
1660
+ deps.log(INSTALL_BRIDGE_SIGNUP_EMAIL_HINT);
1513
1661
  }
1514
1662
  return { ok: false, error: "No email entered." };
1515
1663
  }
@@ -1557,15 +1705,16 @@ export function resolveInstallBridgeOnboardingBranch(options, env) {
1557
1705
  export const INSTALL_BRIDGE_KEY_SELECTOR_PROMPT = "Welcome to Bridge - do you have a token yet?";
1558
1706
  /**
1559
1707
  * 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.
1708
+ * token. BAPI-1171 swapped the order so the self-serve, need-a-token path is
1709
+ * option 1 — first-time signup is the common case and now leads.
1561
1710
  *
1562
1711
  * "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
1712
+ * tokens as far as this question is concerned, and answering "2" leads to ONE
1564
1713
  * hidden prompt whose value selects the route by shape.
1565
1714
  */
1566
1715
  export const INSTALL_BRIDGE_ONBOARDING_CHOICES = [
1567
- "1. Yes, I have received a token",
1568
- "2. No, I need one",
1716
+ "1. I need a token",
1717
+ "2. I already have a token",
1569
1718
  ];
1570
1719
  /** The single-line answer prompt drawn after the options (no bracketed default). */
1571
1720
  export const INSTALL_BRIDGE_ONBOARDING_CHOICE_PROMPT = "Enter 1 or 2: ";
@@ -1579,9 +1728,9 @@ export const INSTALL_BRIDGE_ONBOARDING_CHOICE_HINT = "Enter 1 or 2.";
1579
1728
  * advised "try the hidden prompt again" — false, because the process has already
1580
1729
  * exited by the time the user reads it. All three now name the SAME three routes.
1581
1730
  */
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 " +
1731
+ 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 " +
1732
+ "already have one (an invite or an API key — either one is accepted at the hidden prompt). " +
1733
+ "Non-interactively, pass --api-key <key> (or set BAPI_API_KEY) for an " +
1585
1734
  "existing project, --invite if you were sent an invite token, or --email <addr> to sign up.";
1586
1735
  /**
1587
1736
  * Interactive wrapper around {@link resolveInstallBridgeOnboardingBranch}.
@@ -1651,20 +1800,21 @@ export async function resolveInstallBridgeOnboardingBranchForRun(options, deps,
1651
1800
  // it can never hang or silently select a branch.
1652
1801
  for (let attempt = 0; attempt < 2; attempt += 1) {
1653
1802
  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
1803
+ // BAPI-708 (A-1): the accepted tokens are exactly 1/2 (no letters, no
1804
+ // aliases), preserving the one-strict-line contract. BAPI-1171 swapped the
1805
+ // order so "1" is the self-serve need-key branch and "2" routes to the
1656
1806
  // have-key branch's credential-agnostic prompt, whose value is classified by
1657
1807
  // SHAPE — so a user holding an invite still lands in the redemption protocol
1658
1808
  // 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") {
1809
+ if (answer === "1") {
1662
1810
  return {
1663
1811
  ok: true,
1664
1812
  branch: { kind: "need-key", method: "self-serve" },
1665
1813
  credentialIntent: "inferred",
1666
1814
  };
1667
1815
  }
1816
+ if (answer === "2")
1817
+ return { ok: true, branch: { kind: "have-key" }, credentialIntent: "inferred" };
1668
1818
  if (attempt === 0)
1669
1819
  deps.log(INSTALL_BRIDGE_ONBOARDING_CHOICE_HINT);
1670
1820
  }
@@ -1854,8 +2004,8 @@ export function promptMultiSelectViaReadline(promptText, options, input = proces
1854
2004
  * 2. interactive multi-select on a TTY (AC-2: no row pre-selected — the picker
1855
2005
  * starts empty and requires an explicit ≥1 selection),
1856
2006
  * 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).
2007
+ * detected Copilot VS Code and Cursor automatic targets (never Codex just
2008
+ * because a global directory exists).
1859
2009
  */
1860
2010
  export async function resolveSelectedHostPlatforms(deps, options) {
1861
2011
  // 1. Explicit --tools (empty array is an explicit empty selection).
@@ -1909,7 +2059,7 @@ async function resolveDryRunHostPlatforms(deps, options) {
1909
2059
  * Launch agents install-bridge will NEVER auto-spawn from a host SELECTION, even
1910
2060
  * though their CLI exists and the host's MCP config is still written normally.
1911
2061
  *
1912
- * `cursor-agent` is excluded: its FIRST-RUN workspace-trust handshake collapses
2062
+ * `cursor-agent` and `codex` are excluded. Cursor's FIRST-RUN workspace-trust handshake collapses
1913
2063
  * under our osascript-spawned launch — the "Workspace Trust Required" dialog
1914
2064
  * renders, then the process exits before a selection registers, so configuration
1915
2065
  * never runs (reproduced on a pristine, never-trusted workspace; the identical
@@ -1917,16 +2067,20 @@ async function resolveDryRunHostPlatforms(deps, options) {
1917
2067
  * cursor-agent's own behavior on an untrusted workspace and is not something our
1918
2068
  * launch can drive, so we treat Cursor as a no-auto-launch host: selecting it
1919
2069
  * still writes `.cursor/mcp.json`, then prints the manual `/install-bridge`
1920
- * continuation (same path as Copilot/Codex/Windsurf, which have no agentic CLI).
2070
+ * continuation. Codex does have an agentic CLI, but this workflow is a Claude
2071
+ * `/install-bridge` command and its native skill is not available until after
2072
+ * the bootstrap it would be asked to run completes.
1921
2073
  * An explicit `--agent cursor-agent` override still spawns it — a deliberate,
1922
2074
  * documented power-user opt-in that bypasses this selection guard.
1923
2075
  */
1924
- const SELECTION_NON_LAUNCHABLE_AGENTS = new Set(["cursor-agent"]);
2076
+ const SELECTION_NON_LAUNCHABLE_AGENTS = new Set(["cursor-agent", "codex"]);
2077
+ /** Agents whose interactive command surface cannot run the post-install recipe yet. */
2078
+ const INSTALL_BRIDGE_UNSUPPORTED_AGENTS = new Set(["codex"]);
1925
2079
  /**
1926
2080
  * Resolve the post-install launch decision from the selected host platforms and an
1927
2081
  * OPTIONAL explicit `--agent` override (BAPI-657). Pure and total:
1928
2082
  *
1929
- * - An explicit `--agent` is an ABSOLUTE override (ratified AC-4): a valid explicit
2083
+ * - An explicit supported `--agent` is an ABSOLUTE override: a valid explicit
1930
2084
  * agent always returns `spawn(agent)`, regardless of which tools were selected —
1931
2085
  * the flag is honored even for a host outside the selection, and it bypasses the
1932
2086
  * {@link SELECTION_NON_LAUNCHABLE_AGENTS} guard below.
@@ -1941,6 +2095,9 @@ const SELECTION_NON_LAUNCHABLE_AGENTS = new Set(["cursor-agent"]);
1941
2095
  */
1942
2096
  export function resolveInstallBridgeLaunchDecision(selectedPlatforms, explicitAgent) {
1943
2097
  if (explicitAgent) {
2098
+ if (INSTALL_BRIDGE_UNSUPPORTED_AGENTS.has(explicitAgent)) {
2099
+ return { kind: "manual", reason: "unsupported-agent-workflow" };
2100
+ }
1944
2101
  return { kind: "spawn", agent: explicitAgent };
1945
2102
  }
1946
2103
  if (selectedPlatforms.length === 0) {
@@ -2137,8 +2294,8 @@ async function buildDetectionContext(deps) {
2137
2294
  /**
2138
2295
  * Resolve which project-local JSON host configs to write for the selected
2139
2296
  * 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
2297
+ * VS Code) are returned here; the global target (Codex) and manual targets
2298
+ * (Windsurf) are handled by the registry-driven emitter in
2142
2299
  * `runInstallBridgeCli`. The returned shape is unchanged so the existing
2143
2300
  * read-merge-write path and overwrite-consent detection are preserved.
2144
2301
  */
@@ -2147,7 +2304,7 @@ function hostConfigTargetsForPlatforms(platforms) {
2147
2304
  return HOST_PLATFORM_ORDER.filter((id) => set.has(id))
2148
2305
  .map((id) => MCP_HOST_TARGETS[id])
2149
2306
  .filter((t) => t.scope === "project" && hostAdapterForTarget(t).format === "json")
2150
- .map((t) => ({ relPath: t.relPath, topLevelKey: hostAdapterForTarget(t).topLevelKey }));
2307
+ .map(toHostConfigTarget);
2151
2308
  }
2152
2309
  /**
2153
2310
  * Derive the human-facing labels for a set of selected platforms, in registry
@@ -2162,7 +2319,7 @@ export function labelsForPlatforms(platforms) {
2162
2319
  }
2163
2320
  /** Project-local host-config metadata sourced from the shared registry/adapter. */
2164
2321
  function toHostConfigTarget(target) {
2165
- return { relPath: target.relPath, topLevelKey: hostAdapterForTarget(target).topLevelKey };
2322
+ return { target, relPath: target.relPath, topLevelKey: hostAdapterForTarget(target).topLevelKey };
2166
2323
  }
2167
2324
  /** Resolve which project-local host configs to write, mirroring runInit detection. */
2168
2325
  async function resolveHostConfigTargets(deps) {
@@ -2400,9 +2557,10 @@ async function requestTrackedConfigConsent(deps, relPath) {
2400
2557
  * helper governs launcher-token migration only, never credentials.
2401
2558
  */
2402
2559
  export function preserveExistingLauncherArgs(existing, entry) {
2560
+ const existingEntry = asRecord(existing) ?? {};
2403
2561
  const existingArgs = asRecord(existing)?.args;
2404
2562
  const preserved = refreshBridgeApiPackageSpec(existingArgs, currentBridgePackageSpec());
2405
- return preserved ? { ...entry, args: preserved } : entry;
2563
+ return { ...existingEntry, ...entry, ...(preserved ? { args: preserved } : {}) };
2406
2564
  }
2407
2565
  /**
2408
2566
  * Read-only pre-scan for both-key configs across EVERY project target (BAPI-807).
@@ -2457,7 +2615,8 @@ async function writeHostConfigs(deps, targets, entries, trackedState, ctx) {
2457
2615
  // file that will be skipped never triggers a misleading write-consent prompt.
2458
2616
  if (read.state === "invalid") {
2459
2617
  deps.errorLog(formatInvalidConfigNotice(target.relPath));
2460
- for (const line of formatSecretFreeManualMerge(target.relPath, target.topLevelKey, entries.secretFree)) {
2618
+ const secretFreeEntry = adaptBridgeEntryForHostTarget(entries.secretFree, target.target);
2619
+ for (const line of formatSecretFreeManualMerge(target.relPath, target.topLevelKey, secretFreeEntry)) {
2461
2620
  deps.errorLog(line);
2462
2621
  }
2463
2622
  skipped.push({ relPath: target.relPath, reason: "invalid" });
@@ -2511,7 +2670,8 @@ async function writeHostConfigs(deps, targets, entries, trackedState, ctx) {
2511
2670
  // the full template args, `serve` included. The rule is identical for the
2512
2671
  // real-key and secret-free variants: credential safety must not decide whether
2513
2672
  // a launcher is migrated.
2514
- topLevel[registrationKey] = preserveExistingLauncherArgs(topLevel[registrationKey], entry);
2673
+ const physicalEntry = adaptBridgeEntryForHostTarget(entry, target.target);
2674
+ topLevel[registrationKey] = preserveExistingLauncherArgs(topLevel[registrationKey], physicalEntry);
2515
2675
  config[target.topLevelKey] = topLevel;
2516
2676
  await deps.mkdir(path.dirname(fullPath), { recursive: true });
2517
2677
  await deps.writeFile(fullPath, JSON.stringify(config, null, 2) + "\n", {
@@ -2522,7 +2682,7 @@ async function writeHostConfigs(deps, targets, entries, trackedState, ctx) {
2522
2682
  return { written, skipped, duplicateRegistrations: [] };
2523
2683
  }
2524
2684
  /**
2525
- * Provision the selected GLOBAL (Codex, Copilot CLI) and MANUAL (Windsurf)
2685
+ * Provision the selected GLOBAL (Codex) and MANUAL (Windsurf)
2526
2686
  * targets through the registry-driven emitter (BAPI-635). Project JSON targets
2527
2687
  * are handled by {@link writeHostConfigs}; this covers everything else. Global
2528
2688
  * config paths are never added to the repository .gitignore. Returns secret-free
@@ -3084,6 +3244,15 @@ export const INSTALL_BRIDGE_VERIFY_THROTTLED = "Bridge is at capacity and could
3084
3244
  * hide a real outage behind a version-skew story.
3085
3245
  */
3086
3246
  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.";
3247
+ /**
3248
+ * BAPI-1173: printed after a successful verification whose response says, with an
3249
+ * explicit `trial_granted: false`, that this email already used its one free trial.
3250
+ *
3251
+ * Text only — deliberately no URL. The canonical purchase link is authored once,
3252
+ * server-side, and the first metered-work denial already delivers it. Non-metered
3253
+ * work still runs, so this informs rather than blocks.
3254
+ */
3255
+ 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
3256
  /**
3088
3257
  * Local cap on rejected verification attempts before the CLI stops re-prompting.
3089
3258
  *
@@ -3122,6 +3291,25 @@ export const INSTALL_BRIDGE_SIGNUP_UPGRADE_REQUIRED = [
3122
3291
  "",
3123
3292
  ` npx -y ${MCP_PACKAGE_NAME}@latest install --email <addr>`,
3124
3293
  ].join("\n");
3294
+ /**
3295
+ * HTTP 403 on the self-serve request or verify call (BAPI-1171).
3296
+ *
3297
+ * Deliberately distinct from {@link INSTALL_BRIDGE_SIGNUP_UPGRADE_REQUIRED}: a 403
3298
+ * means the request was refused before signup logic ran at all — an upgrade would
3299
+ * not change the outcome, so the message names the actual cause instead.
3300
+ */
3301
+ export const INSTALL_BRIDGE_SIGNUP_BLOCKED = "Error: Bridge signup is not available from your current network — your network/IP address " +
3302
+ "appears to be blocked. Try again from a different network, or contact Bridge if you believe " +
3303
+ "this is a mistake.";
3304
+ /**
3305
+ * A self-serve HTTP status this client has no specific handling for (BAPI-1171).
3306
+ *
3307
+ * Carries only the numeric status — never response text or a body, which could
3308
+ * echo back the email address or another value this client must not print.
3309
+ */
3310
+ export function formatSelfServeUnexpectedStatus(status) {
3311
+ return `Error: Bridge signup returned an unexpected response (status ${status}). Try again in a few minutes.`;
3312
+ }
3125
3313
  /**
3126
3314
  * Mask an address for display: first character, then the domain.
3127
3315
  *
@@ -3156,7 +3344,12 @@ export function formatChallengeRemaining(expiresAtIso, nowMs) {
3156
3344
  return "This code has expired.";
3157
3345
  if (minutes === 1)
3158
3346
  return "This code is valid for about another minute.";
3159
- return `This code is valid for about another ${minutes} minutes.`;
3347
+ if (minutes < 60)
3348
+ return `This code is valid for about another ${minutes} minutes.`;
3349
+ if (minutes < 120)
3350
+ return "This code is valid for about another hour.";
3351
+ const hours = Math.floor(minutes / 60);
3352
+ return `This code is valid for about another ${hours} hours.`;
3160
3353
  }
3161
3354
  /**
3162
3355
  * Prompt for the verification code, validating its shape LOCALLY before any
@@ -3246,8 +3439,11 @@ export async function requestSelfServeChallenge(deps, baseUrl, email) {
3246
3439
  });
3247
3440
  }
3248
3441
  catch (err) {
3442
+ // BAPI-1171: the fetch itself failed (DNS, TLS, connection refused, timeout) —
3443
+ // distinct from the server responding with an error status. The caught value
3444
+ // is never inspected; only its category is reported.
3249
3445
  void err;
3250
- return { ok: false, category: "failed" };
3446
+ return { ok: false, category: "network-error" };
3251
3447
  }
3252
3448
  if (resp.ok) {
3253
3449
  let handle;
@@ -3278,7 +3474,12 @@ export async function requestSelfServeChallenge(deps, baseUrl, email) {
3278
3474
  return { ok: false, category: "unavailable" };
3279
3475
  if (resp.status === 400 || resp.status === 422)
3280
3476
  return { ok: false, category: "invalid" };
3281
- return { ok: false, category: "failed" };
3477
+ // BAPI-1171: 403 means the caller's network/IP was refused outright — distinct
3478
+ // from every status above, and from a genuinely unhandled status below. Neither
3479
+ // branch reads the response body.
3480
+ if (resp.status === 403)
3481
+ return { ok: false, category: "blocked" };
3482
+ return { ok: false, category: "unexpected", status: resp.status };
3282
3483
  }
3283
3484
  /**
3284
3485
  * POST the VERIFY phase: a handle and a code in, the invite token out.
@@ -3297,14 +3498,18 @@ export async function verifySelfServeChallenge(deps, baseUrl, handle, code) {
3297
3498
  });
3298
3499
  }
3299
3500
  catch (err) {
3501
+ // BAPI-1171: the fetch itself failed — distinct from the server responding
3502
+ // with an error status. The caught value is never inspected.
3300
3503
  void err;
3301
- return { ok: false, category: "failed" };
3504
+ return { ok: false, category: "network-error" };
3302
3505
  }
3303
3506
  if (resp.ok) {
3304
3507
  let token;
3508
+ let trialGranted;
3305
3509
  try {
3306
3510
  const body = (await resp.json());
3307
3511
  token = body?.token;
3512
+ trialGranted = body?.trial_granted;
3308
3513
  }
3309
3514
  catch {
3310
3515
  return { ok: false, category: "failed" };
@@ -3314,6 +3519,12 @@ export async function verifySelfServeChallenge(deps, baseUrl, handle, code) {
3314
3519
  !token.startsWith(BOOTSTRAP_INVITE_TOKEN_PREFIX)) {
3315
3520
  return { ok: false, category: "failed" };
3316
3521
  }
3522
+ // BAPI-1173: forward-compatible. Only a real boolean is carried; an absent
3523
+ // field (an older server) or a malformed one leaves the result exactly the
3524
+ // legacy `{ ok: true, token }` shape rather than being coerced to false.
3525
+ if (typeof trialGranted === "boolean") {
3526
+ return { ok: true, token, trialGranted };
3527
+ }
3317
3528
  return { ok: true, token };
3318
3529
  }
3319
3530
  // 401 is the server's uniform invalid-challenge outcome. It covers a wrong code,
@@ -3328,7 +3539,12 @@ export async function verifySelfServeChallenge(deps, baseUrl, handle, code) {
3328
3539
  // caller can say "try again" instead of "upgrade the CLI".
3329
3540
  if (resp.status >= 500)
3330
3541
  return { ok: false, category: "server-error" };
3331
- return { ok: false, category: "failed" };
3542
+ // BAPI-1171: 403 means the caller's network/IP was refused outright — distinct
3543
+ // from every status above, and from a genuinely unhandled status below. Neither
3544
+ // branch reads the response body.
3545
+ if (resp.status === 403)
3546
+ return { ok: false, category: "blocked" };
3547
+ return { ok: false, category: "unexpected", status: resp.status };
3332
3548
  }
3333
3549
  /**
3334
3550
  * POST the RESEND phase: the handle alone, timing metadata out.
@@ -3630,6 +3846,7 @@ export function buildDryRunPreview(ctx) {
3630
3846
  ...credentialStep,
3631
3847
  ...buildCommitAssetsPreviewLines(ctx.commitSafeAssets, ctx.host.kind === "explicit" ? ctx.host.configTargets : []),
3632
3848
  ...(ctx.host.kind === "explicit" ? buildLaunchStepPreview(ctx.host.launch) : buildDeferredLaunchStepPreview()),
3849
+ ...(ctx.host.kind === "explicit" ? buildAgentNotesPreviewLines(ctx.host.notesTargets ?? []) : []),
3633
3850
  ];
3634
3851
  }
3635
3852
  /**
@@ -3663,7 +3880,9 @@ function describePlannedLaunchAgent(launch) {
3663
3880
  }
3664
3881
  return launch.reason === "empty-selection"
3665
3882
  ? "none — no tools selected"
3666
- : "none — no launchable tool selected; manual continuation is printed";
3883
+ : launch.reason === "unsupported-agent-workflow"
3884
+ ? "none — selected agent cannot run the install-bridge workflow; manual continuation is printed"
3885
+ : "none — no launchable tool selected; manual continuation is printed";
3667
3886
  }
3668
3887
  /**
3669
3888
  * The Step 5 preview lines, shared by both previews so the launch description
@@ -3715,7 +3934,9 @@ function buildLaunchStepPreview(launch) {
3715
3934
  ...githubLines,
3716
3935
  launch.reason === "empty-selection"
3717
3936
  ? "Step 5 — no agent session: no tools were selected, so nothing is configured or opened; re-run and select at least one tool."
3718
- : "Step 5 — no automatic launch: no selected tool has an agentic CLI. The deterministic setup completes and copy-pasteable /install-bridge continuation is printed (your Bridge MCP tools stay limited until configured).",
3937
+ : launch.reason === "unsupported-agent-workflow"
3938
+ ? "Step 5 — no automatic launch: the selected agent cannot run the install-bridge workflow. The deterministic setup completes and manual continuation is printed."
3939
+ : "Step 5 — no automatic launch: no selected tool can auto-launch this workflow. The deterministic setup completes and copy-pasteable /install-bridge continuation is printed (your Bridge MCP tools stay limited until configured).",
3719
3940
  ];
3720
3941
  }
3721
3942
  /**
@@ -3729,6 +3950,25 @@ function buildDeferredLaunchStepPreview() {
3729
3950
  " not yet known — a real run resolves it first, then follows from there).",
3730
3951
  ];
3731
3952
  }
3953
+ /**
3954
+ * The optional agent-notes offer preview line (BAPI-1207). A --dry-run never
3955
+ * prompts or writes; when the selection resolves to at least one writable
3956
+ * notes target this states plainly that a real interactive run would ask
3957
+ * default-No consent before touching it. Empty when the selection resolves to
3958
+ * no writable target (e.g. Windsurf-only), mirroring how the GitHub offer
3959
+ * lines are unconditional but this offer's applicability is not.
3960
+ */
3961
+ function buildAgentNotesPreviewLines(notesTargets) {
3962
+ if (notesTargets.length === 0)
3963
+ return [];
3964
+ return [
3965
+ "Step 4c — optional: cross-platform agent notes (SKIPPED in --dry-run): if the terminal is",
3966
+ ` interactive, offer to create or refresh a Bridge-owned marker region in ${notesTargets.join(" and ")}`,
3967
+ " with situation-to-tool guidance for a few Bridge MCP tools; defaults to No, and",
3968
+ " existing content outside that region is always preserved. No notes files are",
3969
+ " written in --dry-run.",
3970
+ ];
3971
+ }
3732
3972
  /**
3733
3973
  * Bootstrap-invite dry-run preview. A --dry-run must not consume the invite OR
3734
3974
  * LEAVE STATE BEHIND: skipping the HTTP call is not enough — a preview that wrote
@@ -3809,6 +4049,7 @@ function buildBootstrapDryRunPreview(ctx) {
3809
4049
  ...promoteStep,
3810
4050
  ...buildCommitAssetsPreviewLines(ctx.commitSafeAssets, ctx.host.kind === "explicit" ? ctx.host.configTargets : []),
3811
4051
  ...(ctx.host.kind === "explicit" ? buildLaunchStepPreview(ctx.host.launch) : buildDeferredLaunchStepPreview()),
4052
+ ...(ctx.host.kind === "explicit" ? buildAgentNotesPreviewLines(ctx.host.notesTargets ?? []) : []),
3812
4053
  ];
3813
4054
  }
3814
4055
  /**
@@ -4150,6 +4391,30 @@ export const INSTALL_BRIDGE_NO_GIT_NONINTERACTIVE_WARNING = `Warning: ${INSTALL_
4150
4391
  async function hasProjectRootMarker(deps) {
4151
4392
  return sharedHasProjectRootMarker(deps.cwd, deps.stat);
4152
4393
  }
4394
+ /**
4395
+ * Resolve, then live-validate, the premium model alias to pin the spawned
4396
+ * install-spawn session to (BAPI-1206). Fail-open at every step: an agent with
4397
+ * no model override support, a validation failure, or an unavailable live
4398
+ * probe all resolve to `null` (omit `--model`, use the agent default) rather
4399
+ * than aborting installation or spawning. For a statically allowlisted agent
4400
+ * (e.g. claude), {@link validateResolvedModelAliasForAgent} returns the
4401
+ * candidate unchanged without probing anything — only an agent like
4402
+ * `cursor-agent`, which has no `staticModelAliasAllowlist`, triggers the live
4403
+ * advertised-model check. A non-null candidate that fails validation logs a
4404
+ * secret-free diagnostic through `deps.debugLog` (silent unless
4405
+ * `BAPI_INSTALL_DEBUG` is set) and is never surfaced as a user-facing warning.
4406
+ */
4407
+ export async function resolveValidatedPremiumModelAlias(deps, agent) {
4408
+ const candidate = resolveModelAlias(agent, "premium");
4409
+ if (!candidate)
4410
+ return null;
4411
+ const validation = await validateResolvedModelAliasForAgent(deps.startTicketsDeps, agent, candidate);
4412
+ if (!validation.ok) {
4413
+ deps.debugLog(`install-bridge: ${validation.warning}`);
4414
+ return null;
4415
+ }
4416
+ return validation.alias;
4417
+ }
4153
4418
  // ---------------------------------------------------------------------------
4154
4419
  // CLI entry
4155
4420
  // ---------------------------------------------------------------------------
@@ -4203,9 +4468,12 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
4203
4468
  // be an argument error. Deliberately after the deps merge and the base-URL
4204
4469
  // resolution above, so the nested command inherits the same final merged deps
4205
4470
  // (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.
4471
+ // Note this is `install-bridge conductor`; it is UNRELATED to the top-level
4472
+ // `conductor`/`epic-implementer` CLI split (S4/BAPI-1080) — the bare
4473
+ // top-level `conductor` token is now a fixed migration stub and the real
4474
+ // event-ledger CLI routes through `runEpicImplementerCli` under the
4475
+ // `epic-implementer` name. This nested `install-bridge conductor` surface is
4476
+ // untouched by that rename.
4209
4477
  if (argv[0] === "conductor") {
4210
4478
  if (!baseUrlResult.ok) {
4211
4479
  errorLog(baseUrlResult.error);
@@ -4339,10 +4607,11 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
4339
4607
  errorLog(`Error: no launch agent is registered for '${launchDecision.agent}'.`);
4340
4608
  return 1;
4341
4609
  }
4610
+ const modelAlias = await resolveValidatedPremiumModelAlias(deps, spec);
4342
4611
  launch = {
4343
4612
  kind: "spawn",
4344
4613
  agent: launchDecision.agent,
4345
- spawnCommand: deps.buildShellCommand(spec, INSTALL_BRIDGE_AGENT_PROMPT, deps.cwd, deps.platform),
4614
+ spawnCommand: deps.buildShellCommand(spec, INSTALL_BRIDGE_AGENT_PROMPT, deps.cwd, deps.platform, modelAlias),
4346
4615
  };
4347
4616
  }
4348
4617
  else if (launchDecision.kind === "choose-one") {
@@ -4356,6 +4625,7 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
4356
4625
  configTargets: dryRunTargets.map((t) => t.relPath),
4357
4626
  manualEditors: manualEditorNames(await detectManualEditors(deps)),
4358
4627
  launch,
4628
+ notesTargets: resolveAgentNotesTargets(dryRunPlatforms).targets,
4359
4629
  };
4360
4630
  }
4361
4631
  else {
@@ -4658,8 +4928,8 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
4658
4928
  // BAPI-635: resolve the AI-coding-tool selection FIRST — BAPI-657 makes this
4659
4929
  // selection (not a hidden --agent default) drive which post-install session opens.
4660
4930
  // 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
4931
+ // set. The selected project JSON targets drive the read-merge-write path; a selected
4932
+ // global (Codex) or manual (Windsurf) target is provisioned by the
4663
4933
  // registry-driven emitter after the connectivity check.
4664
4934
  const selectedPlatforms = await resolveSelectedHostPlatforms(deps, options);
4665
4935
  const targets = hostConfigTargetsForPlatforms(selectedPlatforms);
@@ -4680,10 +4950,11 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
4680
4950
  errorLog(`Error: no launch agent is registered for '${launchDecision.agent}'.`);
4681
4951
  return 1;
4682
4952
  }
4953
+ const modelAlias = await resolveValidatedPremiumModelAlias(deps, spec);
4683
4954
  planLaunch = {
4684
4955
  kind: "spawn",
4685
4956
  agent: launchDecision.agent,
4686
- spawnCommand: deps.buildShellCommand(spec, INSTALL_BRIDGE_AGENT_PROMPT, deps.cwd, deps.platform),
4957
+ spawnCommand: deps.buildShellCommand(spec, INSTALL_BRIDGE_AGENT_PROMPT, deps.cwd, deps.platform, modelAlias),
4687
4958
  };
4688
4959
  }
4689
4960
  else if (launchDecision.kind === "choose-one") {
@@ -4738,7 +5009,8 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
4738
5009
  return 1;
4739
5010
  }
4740
5011
  finalAgentName = chosen;
4741
- finalSpawnCommand = deps.buildShellCommand(spec, INSTALL_BRIDGE_AGENT_PROMPT, deps.cwd, deps.platform);
5012
+ const modelAlias = await resolveValidatedPremiumModelAlias(deps, spec);
5013
+ finalSpawnCommand = deps.buildShellCommand(spec, INSTALL_BRIDGE_AGENT_PROMPT, deps.cwd, deps.platform, modelAlias);
4742
5014
  }
4743
5015
  // chosen === null → non-TTY / unavailable / invalid → manual continuation below.
4744
5016
  }
@@ -4936,8 +5208,24 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
4936
5208
  else if (requested.category === "invalid") {
4937
5209
  reportFatal("Error: Self-serve setup could not be requested. Check the email value and try again.");
4938
5210
  }
5211
+ else if (requested.category === "blocked") {
5212
+ // BAPI-1171: the request was refused before signup logic ran — an
5213
+ // upgrade would not change the outcome, so this is NOT protocol
5214
+ // guidance.
5215
+ reportFatal(INSTALL_BRIDGE_SIGNUP_BLOCKED);
5216
+ }
5217
+ else if (requested.category === "unexpected") {
5218
+ // BAPI-1171: an HTTP status this client has no specific handling
5219
+ // for. Reports only the numeric status — never response text.
5220
+ reportFatal(formatSelfServeUnexpectedStatus(requested.status ?? 0));
5221
+ }
5222
+ else if (requested.category === "network-error") {
5223
+ // BAPI-1171: the fetch itself failed — a transport problem, not a
5224
+ // protocol mismatch, so this is NOT upgrade guidance either.
5225
+ reportFatal(INSTALL_BRIDGE_SIGNUP_UNAVAILABLE);
5226
+ }
4939
5227
  else {
4940
- // R-2: a malformed or tokenless-unexpected response is a PROTOCOL
5228
+ // R-2: a malformed or tokenless success body is a PROTOCOL
4941
5229
  // mismatch, not a network problem. Saying "check connectivity" here
4942
5230
  // was the old behavior and is exactly what this ticket replaces.
4943
5231
  reportFatal(INSTALL_BRIDGE_SIGNUP_UPGRADE_REQUIRED);
@@ -4986,6 +5274,11 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
4986
5274
  const verified = await verifySelfServeChallenge(deps, baseUrl, challengeHandle, code);
4987
5275
  if (verified.ok) {
4988
5276
  inviteToken = verified.token;
5277
+ // BAPI-1173: only an EXPLICIT false prints. `true`, and an absent field
5278
+ // from an older server, add nothing.
5279
+ if (verified.trialGranted === false) {
5280
+ log(` ${INSTALL_BRIDGE_TRIAL_ALREADY_USED}`);
5281
+ }
4989
5282
  break;
4990
5283
  }
4991
5284
  if (verified.category === "rate-limited") {
@@ -4999,10 +5292,28 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
4999
5292
  reportFatal(INSTALL_BRIDGE_VERIFY_SERVER_ERROR);
5000
5293
  return false;
5001
5294
  }
5295
+ if (verified.category === "blocked") {
5296
+ // BAPI-1171: the request was refused before verification logic ran —
5297
+ // an upgrade would not change the outcome.
5298
+ reportFatal(INSTALL_BRIDGE_SIGNUP_BLOCKED);
5299
+ return false;
5300
+ }
5301
+ if (verified.category === "unexpected") {
5302
+ // BAPI-1171: an HTTP status this client has no specific handling for.
5303
+ // Reports only the numeric status — never response text.
5304
+ reportFatal(formatSelfServeUnexpectedStatus(verified.status ?? 0));
5305
+ return false;
5306
+ }
5307
+ if (verified.category === "network-error") {
5308
+ // BAPI-1171: the fetch itself failed — a transport problem, not a
5309
+ // protocol mismatch.
5310
+ reportFatal(INSTALL_BRIDGE_VERIFY_SERVER_ERROR);
5311
+ return false;
5312
+ }
5002
5313
  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.
5314
+ // Only a genuine protocol mismatch reaches here now: a malformed body
5315
+ // or a missing token in an otherwise successful response. R-2's
5316
+ // upgrade instruction is the right answer for that.
5006
5317
  reportFatal(INSTALL_BRIDGE_SIGNUP_UPGRADE_REQUIRED);
5007
5318
  return false;
5008
5319
  }
@@ -5377,7 +5688,7 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
5377
5688
  for (const { relPath } of writeResult.skipped) {
5378
5689
  log(` skipped ${relPath} — existing config could not be parsed safely; left untouched`);
5379
5690
  }
5380
- // BAPI-635: provision selected GLOBAL targets (Codex, Copilot CLI) and MANUAL
5691
+ // BAPI-635: provision a selected GLOBAL target (Codex) and MANUAL
5381
5692
  // targets (Windsurf) via the registry-driven emitter. Global paths are never
5382
5693
  // added to the repository .gitignore.
5383
5694
  // BAPI-669 (R8): only a need-key run has a key the user has never seen, so only it
@@ -5413,12 +5724,6 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
5413
5724
  const manualInstructions = buildManualHostInstructions(entry, legacyManualEditors, needKeyManual);
5414
5725
  if (manualInstructions)
5415
5726
  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
5727
  // BAPI-708 (B-c): the Claude trust reminder that used to print here was
5423
5728
  // RELOCATED, not dropped — the reload requirement is genuinely actionable (the
5424
5729
  // server does not work until the host reloads), but at Step 3 it duplicated the
@@ -5523,15 +5828,56 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
5523
5828
  // the agent spawn, for two reasons: the spawned session's capability report should
5524
5829
  // observe GitHub as configured if the user connects it here, and running it after the
5525
5830
  // spawn would put two prompts on the same terminal at once.
5526
- await offerGithubConnection(repoName, baseUrl, deps, log);
5831
+ //
5832
+ // BAPI-1171: one shared session-state object, constructed fresh for this run, is
5833
+ // passed to both offers so a GitHub connection made here suppresses the Bitbucket
5834
+ // offer that follows — see `InstallBridgeVcsSessionState`.
5835
+ const vcsSessionState = { vcsConnected: false };
5836
+ await offerGithubConnection(repoName, baseUrl, deps, log, vcsSessionState);
5527
5837
  // ---- optional Bitbucket connect offer (BAPI-966) ----
5528
5838
  // Same placement reasoning as the GitHub offer above. Both offers are called
5529
5839
  // unconditionally here — each self-gates on its OWN provider-scoped configuration
5530
5840
  // state (`fetchGithubConfigurationState`/`fetchBitbucketConfigurationState` return
5531
5841
  // "unavailable" when their integration id is absent from the manifest, which is the
5532
5842
  // 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);
5843
+ // provider ever reaches its prompt — and the GitHub offer's own success, tracked in
5844
+ // `vcsSessionState`, can additionally short-circuit this one before either check runs.
5845
+ await offerBitbucketConnection(repoName, baseUrl, deps, log, vcsSessionState);
5846
+ // ---- optional cross-platform agent notes offer (BAPI-1207) ----
5847
+ // Placed alongside the GitHub/Bitbucket offers, after the durable install work —
5848
+ // never re-detects platforms; the plan is computed from THIS run's already
5849
+ // selected set. `writeAgentNotes` never touches `writeMcpInstallState` or any
5850
+ // other persisted install-state contract — consent is re-evaluated every run.
5851
+ const notesPlan = resolveAgentNotesTargets(selectedPlatforms);
5852
+ const notesOffer = await offerAgentGuidanceNotes(deps, log, notesPlan.targets);
5853
+ let notesResult;
5854
+ let notesWriteFailed = false;
5855
+ if (notesOffer.kind === "consented") {
5856
+ try {
5857
+ notesResult = await writeAgentNotes({
5858
+ cwd: deps.cwd,
5859
+ selectedPlatforms,
5860
+ guidance: AGENT_GUIDANCE,
5861
+ readFile: deps.readFile,
5862
+ writeFile: deps.writeFile,
5863
+ mkdir: deps.mkdir,
5864
+ });
5865
+ for (const file of notesResult.files) {
5866
+ if (file.status === "failed") {
5867
+ errorLog(` warning: could not update ${file.target} with agent notes (${file.reason}).`);
5868
+ }
5869
+ }
5870
+ }
5871
+ catch {
5872
+ // Fail-open, mirroring the routing-credential persistence above: the
5873
+ // install itself is already complete and durable.
5874
+ notesWriteFailed = true;
5875
+ errorLog(" warning: could not write agent notes (unexpected error).");
5876
+ }
5877
+ }
5878
+ const notesSummaryLine = buildAgentNotesSummaryLine(notesOffer, notesResult, notesWriteFailed, notesPlan.unsupportedPlatforms);
5879
+ if (notesSummaryLine)
5880
+ log(notesSummaryLine);
5535
5881
  // ---- Step 3b (await) — settle the background pre-warm before the session opens ----
5536
5882
  // The warm was kicked off (only on a spawn-capable path) right after the
5537
5883
  // launch-command guards; await it HERE so the bucket is settled before the spawned