@bridge_gpt/mcp-server 0.2.67 → 0.2.68

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 (114) hide show
  1. package/README.md +178 -15
  2. package/build/agent-launchers/claude-executor-adapter.js +27 -0
  3. package/build/agents.generated.js +22 -2
  4. package/build/bitbucket-workspace-remote.js +136 -0
  5. package/build/bootstrap-project-identity.js +24 -0
  6. package/build/bridge-permissions.js +829 -0
  7. package/build/claude-settings.js +18 -0
  8. package/build/cli-help.js +21 -0
  9. package/build/command-assets-doctor.js +24 -70
  10. package/build/command-provisioning.js +29 -5
  11. package/build/commands.generated.js +8 -8
  12. package/build/conductor/bridge-api-client.js +714 -0
  13. package/build/conductor/command-drift-production.js +92 -0
  14. package/build/conductor/command-drift.js +89 -0
  15. package/build/conductor/doctor.js +69 -26
  16. package/build/conductor/epic-branch-ci-inspection.js +1375 -0
  17. package/build/conductor/epic-implementer-cli.js +76 -22
  18. package/build/conductor/gate-discovery.js +890 -0
  19. package/build/conductor/install-doctor.js +15 -3
  20. package/build/conductor/json-output.js +63 -0
  21. package/build/conductor/readiness-cli.js +95 -10
  22. package/build/conductor/readiness-sections.js +369 -3
  23. package/build/conductor/readiness.js +211 -8
  24. package/build/conductor/recovery-cli.js +110 -4
  25. package/build/conductor/recovery-operations.js +99 -1
  26. package/build/connect-bitbucket-api.js +41 -6
  27. package/build/connect-bitbucket.js +106 -33
  28. package/build/credential-store.js +207 -47
  29. package/build/docs.generated.js +57 -3
  30. package/build/doctor.js +92 -1
  31. package/build/drive-epic.js +45 -8
  32. package/build/epic-coherence-input.test-fixtures.js +4 -3
  33. package/build/epic-implementer-bin.js +1374 -25
  34. package/build/epic-workarounds-path.js +39 -0
  35. package/build/executor/cli.js +30 -0
  36. package/build/executor/completion-contract.js +18 -5
  37. package/build/executor/deps.js +4 -1
  38. package/build/executor/gate-qualification-job.js +499 -0
  39. package/build/executor/guarded-fast-forward-push.js +129 -0
  40. package/build/executor/integration-job.js +122 -8
  41. package/build/executor/job-errors.js +8 -1
  42. package/build/executor/job-runner.js +589 -47
  43. package/build/executor/likely-tests.js +290 -0
  44. package/build/executor/live-worker-registry.js +1 -1
  45. package/build/executor/observation.js +31 -2
  46. package/build/executor/packaged-recipe.js +106 -0
  47. package/build/executor/permissions.js +115 -28
  48. package/build/executor/platform.js +1 -1
  49. package/build/executor/preflight.js +61 -12
  50. package/build/executor/process-containment.js +36 -12
  51. package/build/executor/process.js +15 -7
  52. package/build/executor/prompt-spec.js +33 -1
  53. package/build/executor/recently-failed-tests-file.js +71 -0
  54. package/build/executor/reconcile-overlap-job.js +41 -23
  55. package/build/executor/required-verification.js +1 -1
  56. package/build/executor/required-work-ledger.js +105 -17
  57. package/build/executor/retirement.js +69 -0
  58. package/build/executor/runner.js +114 -11
  59. package/build/executor/test-clock.js +15 -0
  60. package/build/executor/types.js +32 -0
  61. package/build/executor/viewer-tabs.js +58 -8
  62. package/build/executor/worker-command.js +23 -7
  63. package/build/executor/worker-diagnostics.js +37 -0
  64. package/build/executor/worker-finalization.js +31 -5
  65. package/build/executor/worker-guard-hook.js +179 -25
  66. package/build/executor/worker-log.js +4 -3
  67. package/build/executor/worker-verification-guidance.js +37 -0
  68. package/build/executor/worktree.js +88 -11
  69. package/build/first-build-advisory.js +97 -0
  70. package/build/generated-assets-advisory.js +14 -0
  71. package/build/generated-assets-catalog.js +66 -0
  72. package/build/generated-assets-context.js +46 -0
  73. package/build/generated-assets-dependencies.js +26 -0
  74. package/build/generated-assets-doctor.js +50 -0
  75. package/build/generated-assets-health.js +65 -0
  76. package/build/generated-assets-host.js +17 -0
  77. package/build/generated-assets-inventory.js +17 -0
  78. package/build/generated-assets-paths.js +60 -0
  79. package/build/generated-assets-publisher.js +133 -0
  80. package/build/generated-assets-remedy.js +25 -0
  81. package/build/generated-assets-repair.js +72 -0
  82. package/build/generated-assets-session.js +71 -0
  83. package/build/generated-assets-test-support.js +35 -0
  84. package/build/index.js +964 -768
  85. package/build/init.js +54 -177
  86. package/build/install-bridge-conductor-gate.js +1372 -0
  87. package/build/install-bridge-conductor.js +289 -36
  88. package/build/install-bridge.js +544 -37
  89. package/build/mcp-install-state.js +50 -4
  90. package/build/merge-workflow-install.js +3 -4
  91. package/build/pipeline-utils.js +6 -2
  92. package/build/pipelines.generated.js +22 -142
  93. package/build/plan-execution-ledger.js +2 -32
  94. package/build/plane/manifest.js +16 -1
  95. package/build/plane/preflight.js +54 -1
  96. package/build/printed-remedy-commands.js +16 -1
  97. package/build/readme.generated.js +1 -1
  98. package/build/repair-assets.js +93 -0
  99. package/build/run-unit-tests-launcher.js +130 -49
  100. package/build/secret-redaction.js +33 -0
  101. package/build/setup-epic.js +108 -3
  102. package/build/start-tickets-prereqs.js +70 -16
  103. package/build/start-tickets.js +29 -5
  104. package/build/tool-surface-gating.js +36 -0
  105. package/build/upgrade-cli.js +55 -1
  106. package/build/version.generated.js +3 -3
  107. package/build/worker-guard-hook-bin.js +3 -2
  108. package/build/worker-guard-hook-main.js +1 -1
  109. package/build/worktree-core.js +15 -2
  110. package/docs/EPIC-IMPLEMENTER-INSTALL.md +30 -12
  111. package/docs/EPIC-IMPLEMENTER.md +68 -2
  112. package/package.json +3 -3
  113. package/pipelines/greenfield-setup.json +4 -128
  114. package/pipelines/implement-ticket.json +3 -2
@@ -1,3 +1,6 @@
1
+ import { validateBootstrapDisplayName, validateBootstrapServerIdentity } from "./bootstrap-project-identity.js";
2
+ import { lookupModernBootstrapRecord, mutateModernBootstrapRecord, prepareModernBootstrapInvite, prepareModernBootstrapSignup, transitionModernBootstrapSignup, lookupLegacyBootstrapInvite, checkpointModernBootstrapIdentity, promoteModernBootstrapCredential } from "./credential-store.js";
3
+ import { collectFirstBuildAdvisory, formatFirstBuildAdvisory } from "./first-build-advisory.js";
1
4
  /**
2
5
  * install-bridge — the one-command Bridge API project bootstrap (BAPI-429).
3
6
  *
@@ -137,7 +140,7 @@
137
140
  * 0600, fsync'd pending record as the `key_secret`, purely so the signup can be
138
141
  * replayed, and it is discarded when that record is promoted.
139
142
  */
140
- import { readFile, writeFile, mkdir, stat, rename, chmod, unlink, open } from "fs/promises";
143
+ import { readFile, writeFile, mkdir, stat, rename, chmod, unlink, open, realpath } from "fs/promises";
141
144
  import { spawn } from "child_process";
142
145
  import { randomBytes as cryptoRandomBytes, createHash } from "crypto";
143
146
  import os from "os";
@@ -168,9 +171,13 @@ import { executorLaunchdLabelForId, executorSystemdUnitNameForId, executorLaunch
168
171
  import { collectConductorNativeLedgerSafe, } from "./conductor/doctor.js";
169
172
  import { installConductorGitHooks, } from "./conductor/git-hooks.js";
170
173
  import { runInstallBridgeConductorCli, } from "./install-bridge-conductor.js";
174
+ import { createQualificationExecutorRunner, } from "./install-bridge-conductor-gate.js";
175
+ import { createNodeGateDiscoveryFs } from "./conductor/gate-discovery.js";
176
+ import { runExecutorCli } from "./executor/cli.js";
177
+ import { runExecutor } from "./executor/runner.js";
171
178
  // BAPI-1136: the merge-workflow generate-and-PR installer. Domain logic lives in
172
179
  // its own module; this file supplies the parser and the process seams.
173
- import { runMergeWorkflowInstall, } from "./merge-workflow-install.js";
180
+ import { parseGithubRemote, runMergeWorkflowInstall, } from "./merge-workflow-install.js";
174
181
  import { MERGE_WORKFLOW_CODEOWNER_INSTRUCTION, MERGE_WORKFLOW_REMEDY_COMMAND, } from "./conductor/merge-execution-contract.js";
175
182
  import { MERGE_WORKFLOW_API_KEY_SECRET_NAME, } from "./merge-workflow-config.js";
176
183
  import { runConductorInstallDoctor, EPIC_IMPLEMENTER_PROFILE_TOKEN, } from "./conductor/install-doctor.js";
@@ -180,6 +187,7 @@ import { claudeReviewWorkflowPath, writeClaudeReviewWorkflow, } from "./claude-r
180
187
  // doctor's optional drift seam below.
181
188
  import { probeClaudeReviewWorkflowDrift, resolveCurrentBranch, resolveRepositoryDefaultBranch, } from "./claude-review-workflow-drift-probe.js";
182
189
  import { runSetupEpicCli } from "./setup-epic.js";
190
+ import { runBridgePermissionsInstallStep, runBridgePermissionsRemoval, } from "./bridge-permissions.js";
183
191
  import { ensureGitignored as ensureGitignoredShared, } from "./git-ignore-utils.js";
184
192
  import { resolveStartTicketsRepoName } from "./start-tickets-repo.js";
185
193
  import { upsertBapiCredential, getPrimaryCredentialStorePath, prepareBootstrapPendingCredential, repointBootstrapPendingCredential, promoteBootstrapPendingCredential, lookupSelfServeBootstrapPendingCredential, prepareSelfServeBootstrapChallenge as prepareSelfServeBootstrapChallengeCredential, discardBootstrapPendingCredential, getBootstrapPendingTarget, resolveBapiCredentials, } from "./credential-store.js";
@@ -189,6 +197,7 @@ import { fetchGithubConfigurationState } from "./connect-github-api.js";
189
197
  import { createDefaultConnectGithubDeps, runGithubConnectionFlow } from "./connect-github.js";
190
198
  import { fetchBitbucketConfigurationState } from "./connect-bitbucket-api.js";
191
199
  import { createDefaultConnectBitbucketDeps, runBitbucketConnectionFlow } from "./connect-bitbucket.js";
200
+ import { defaultGitRemoteRunner, resolveBitbucketWorkspace, } from "./bitbucket-workspace-remote.js";
192
201
  import { resolveAgentSpec, isAgentName, formatValidAgentNames, resolveModelAlias, } from "./agent-registry.js";
193
202
  import { buildGenericAgentShellCommand, getDefaultSpawnTerminalTabForPlatform, detectTerminal, createDefaultStartTicketsDeps, materializeWorkerLaunchCommand, MAX_TERMINAL_COMMAND_BYTES, validateResolvedModelAliasForAgent, } from "./start-tickets.js";
194
203
  import { defaultNpmEntryFileExists, resolveNpmEntry } from "./windows-npm-entry.js";
@@ -443,7 +452,10 @@ export function getInstallBridgeUsage(baseUrl = DEFAULT_BAPI_BASE_URL) {
443
452
  " DECLARES the key route, so that switch is confirmed on a",
444
453
  " terminal; the bare run's hidden token prompt infers instead",
445
454
  " and only announces. NEVER printed or logged.",
446
- " --repo <name> Repository name. --repo and BAPI_REPO_NAME still take",
455
+ " --display-name <label> Friendly creation label; required for non-TTY modern invites.",
456
+ " Do not combine with --repo or BAPI_REPO_NAME. Existing keys use canonical selectors.",
457
+ " Self-serve verification still requires an interactive terminal.",
458
+ " --repo <name> Canonical selector for reconnect or explicit legacy creation. --repo and BAPI_REPO_NAME still take",
447
459
  " priority and short-circuit before any network call. When",
448
460
  " neither is set, a compatible server resolves the unique",
449
461
  " repository from your existing API key automatically. If the",
@@ -561,6 +573,13 @@ export function getInstallBridgeUsage(baseUrl = DEFAULT_BAPI_BASE_URL) {
561
573
  " --agent cursor-agent still force-spawns it. Codex",
562
574
  " is accepted only for manual continuation: neither",
563
575
  " this bootstrap nor start-tickets launches it yet.",
576
+ " --remove-bridge-permissions Remove ONLY the permission rules and autoMode",
577
+ " entries Bridge wrote for /drive-epic-run",
578
+ " (recorded in .bridge/bridge-permissions.json)",
579
+ " from .claude/settings.local.json and",
580
+ " ~/.claude/settings.json. Must be used alone.",
581
+ " Entries you added yourself stay. Runs nothing",
582
+ " else: no onboarding, no credential, no network.",
564
583
  " -h, --help Show this help.",
565
584
  "",
566
585
  "Subcommands:",
@@ -642,6 +661,7 @@ export function parseInstallBridgeArgs(argv) {
642
661
  }
643
662
  let apiKey;
644
663
  let repo;
664
+ let displayName;
645
665
  let force = false;
646
666
  let dryRun = false;
647
667
  // Undefined until `--agent` is explicitly supplied — install-bridge never falls
@@ -667,8 +687,32 @@ export function parseInstallBridgeArgs(argv) {
667
687
  }
668
688
  return { value: argv[i + 1], nextIndex: i + 1 };
669
689
  };
690
+ // BAPI-1398: the removal flag selects a different, narrow action, so it is
691
+ // accepted only on its own — combining it with an install flag is ambiguous.
692
+ if (argv.includes("--remove-bridge-permissions")) {
693
+ if (argv.length !== 1) {
694
+ return {
695
+ status: "error",
696
+ message: "--remove-bridge-permissions must be used alone; it removes Bridge's permission rules and does nothing else.",
697
+ };
698
+ }
699
+ return { status: "ok", options: { force: false, dryRun: false, removeBridgePermissions: true } };
700
+ }
670
701
  for (let i = 0; i < argv.length; i++) {
671
702
  const arg = argv[i];
703
+ if (arg === "--display-name" || arg.startsWith("--display-name=")) {
704
+ const result = readValue(arg, "--display-name", i);
705
+ if ("error" in result)
706
+ return { status: "error", message: result.error };
707
+ try {
708
+ displayName = validateBootstrapDisplayName(result.value);
709
+ }
710
+ catch {
711
+ return { status: "error", message: "--display-name requires a safe label of 1–255 Unicode characters." };
712
+ }
713
+ i = result.nextIndex;
714
+ continue;
715
+ }
672
716
  if (arg === "--force") {
673
717
  force = true;
674
718
  continue;
@@ -797,7 +841,7 @@ export function parseInstallBridgeArgs(argv) {
797
841
  }
798
842
  return {
799
843
  status: "ok",
800
- options: { apiKey, repo, force, dryRun, agentName, invite, inviteMode: inviteSupplied, email, tools, codexProfile },
844
+ options: { apiKey, repo, displayName, force, dryRun, agentName, invite, inviteMode: inviteSupplied, email, tools, codexProfile },
801
845
  };
802
846
  }
803
847
  /**
@@ -918,6 +962,8 @@ export async function offerGithubConnection(repoName, baseUrl, deps, log, sessio
918
962
  // No prompt surface → no offer. Never assume consent on a non-interactive run.
919
963
  if (!deps.isTTY || !deps.promptLine)
920
964
  return;
965
+ let attempted = false;
966
+ const fallbackContext = " This attempt did not establish a successful Bridge GitHub link. Bitbucket may be offered next as an optional alternative; you can decline it and retry GitHub later.";
921
967
  try {
922
968
  const credDeps = {
923
969
  env: deps.env,
@@ -961,6 +1007,7 @@ export async function offerGithubConnection(repoName, baseUrl, deps, log, sessio
961
1007
  if (answer !== "y" && answer !== "yes")
962
1008
  return; // declining is a normal outcome
963
1009
  const connectDeps = createDefaultConnectGithubDeps();
1010
+ attempted = true;
964
1011
  const code = await runGithubConnectionFlow(connectDeps, api, repoName);
965
1012
  if (code === 0) {
966
1013
  // BAPI-1171: only a CONFIRMED success suppresses the Bitbucket offer below —
@@ -968,12 +1015,15 @@ export async function offerGithubConnection(repoName, baseUrl, deps, log, sessio
968
1015
  session.vcsConnected = true;
969
1016
  }
970
1017
  else {
1018
+ log(fallbackContext);
971
1019
  log(" note: GitHub was not connected. Your install is complete — connect GitHub later with " +
972
1020
  `'npx -y ${MCP_PACKAGE_NAME}@latest connect-github --repo ${repoName}'.`);
973
1021
  }
974
1022
  }
975
1023
  catch {
976
1024
  // The install is already durable; a failure here is never fatal to it.
1025
+ if (attempted)
1026
+ log(fallbackContext);
977
1027
  log(" note: the GitHub connection offer could not run. Your install is complete — connect " +
978
1028
  `GitHub later with 'npx -y ${MCP_PACKAGE_NAME}@latest connect-github --repo ${repoName}'.`);
979
1029
  }
@@ -995,7 +1045,10 @@ export const INSTALL_BRIDGE_BITBUCKET_OFFER_CONTEXT = [
995
1045
  // BAPI-1247: name the destination precisely. "Admin panel" sent people looking for a
996
1046
  // page that does not exist under that name — the app lives under the workspace's own
997
1047
  // settings, and saying so is the difference between a 20-second step and a hunt.
998
- ' You finish by pasting a printed code into Workspace settings → Apps → "Connect Bitbucket to Bridge API".',
1048
+ // BAPI-1401: say up front that there are two steps. Bitbucket's install screen gives no
1049
+ // sign that a second one exists.
1050
+ " There are two steps: (1) install the app, then (2) approve the request on the app's page",
1051
+ ' under Workspace settings → Forge Apps → "Connect Bitbucket to Bridge API".',
999
1052
  ` You can do this later instead: ${INSTALL_BRIDGE_CONNECT_BITBUCKET_COMMAND}`,
1000
1053
  ];
1001
1054
  /** Default-No Bitbucket offer prompt. Mirrors {@link INSTALL_BRIDGE_GITHUB_CONNECT_PROMPT}. */
@@ -1060,11 +1113,13 @@ export async function offerBitbucketConnection(repoName, baseUrl, deps, log, ses
1060
1113
  if (answer !== "y" && answer !== "yes")
1061
1114
  return; // declining is a normal outcome
1062
1115
  const connectDeps = createDefaultConnectBitbucketDeps();
1116
+ // BAPI-1401: the workspace comes from the project's origin remote, the same way the
1117
+ // standalone command infers it. With one, the flow mints an approval-mode request
1118
+ // (and prints the direct link when the server has a template); without one it falls
1119
+ // back to the code flow and its textual navigation path.
1120
+ const workspace = await resolveBitbucketWorkspace(undefined, deps.cwd, deps.runGit ?? defaultGitRemoteRunner, log);
1063
1121
  const result = await runBitbucketConnectionFlow(connectDeps, api, repoName, {
1064
- // The install path has no workspace slug to give: a workspace is only discoverable
1065
- // AFTER redemption stages candidates, which is later than this. The connection flow
1066
- // falls back to its textual navigation path rather than guessing a link.
1067
- workspace: undefined,
1122
+ workspace,
1068
1123
  // The Step 4b prompt just above IS the approval for the first mint. Asking again
1069
1124
  // inside the flow would be a second confirmation for one decision.
1070
1125
  initialMintApproved: true,
@@ -1120,14 +1175,14 @@ function joinLabelsWithAnd(labels) {
1120
1175
  return `${labels.slice(0, -1).join(", ")}, and ${labels[labels.length - 1]}`;
1121
1176
  }
1122
1177
  /**
1123
- * The default-No agent-notes consent prompt (BAPI-1207; payoff-led, platform-named
1178
+ * The default-Yes agent-notes consent prompt (BAPI-1435; payoff-led, platform-named
1124
1179
  * copy per the BAPI-1239 install-issue #4 follow-up). Leads with WHY — teaching
1125
1180
  * the agent how to use Bridge's MCP tools — and NAMES the selected platform(s)
1126
1181
  * rather than the old generic "cross-platform" phrasing.
1127
1182
  */
1128
1183
  export function buildAgentNotesPrompt(platformLabels) {
1129
1184
  const { subject, plural } = describeAgentNotesPlatforms(platformLabels);
1130
- return `${INSTALL_BRIDGE_AGENT_NOTES_PROMPT_PREFIX}${subject} know${plural ? "" : "s"} how to use the Bridge MCP? [y/N]: `;
1185
+ return `${INSTALL_BRIDGE_AGENT_NOTES_PROMPT_PREFIX}${subject} know${plural ? "" : "s"} how to use the Bridge MCP? Enter adds guidance for ${subject}. [Y/n]: `;
1131
1186
  }
1132
1187
  /**
1133
1188
  * The offer-context lines printed immediately before the agent-notes prompt
@@ -1147,6 +1202,12 @@ export function buildAgentNotesOfferContext(targets, platformLabels) {
1147
1202
  " created or refreshed — any existing content, inside or outside it, is otherwise preserved.",
1148
1203
  ];
1149
1204
  }
1205
+ function agentGuidancePromptConsents(result) {
1206
+ if (result?.kind !== "answer" || typeof result.value !== "string")
1207
+ return false;
1208
+ const value = result.value.trim().toLowerCase();
1209
+ return value === "" || value === "y" || value === "yes";
1210
+ }
1150
1211
  /**
1151
1212
  * Offer to create or refresh the cross-platform agent notes, if the selected
1152
1213
  * platforms resolve to at least one writable target (BAPI-1207).
@@ -1162,20 +1223,18 @@ export async function offerAgentGuidanceNotes(deps, log, targets, platformLabels
1162
1223
  if (targets.length === 0)
1163
1224
  return { kind: "not-applicable" };
1164
1225
  // No prompt surface → no offer. Never assume consent on a non-interactive run.
1165
- if (!deps.isTTY || !deps.promptLine)
1226
+ if (!deps.isTTY || !deps.promptGuidanceLine)
1166
1227
  return { kind: "headless-skip" };
1167
1228
  try {
1168
1229
  for (const line of buildAgentNotesOfferContext(targets, platformLabels))
1169
1230
  log(line);
1170
- const answer = (await deps.promptLine(buildAgentNotesPrompt(platformLabels))).trim().toLowerCase();
1171
- // Only an explicit yes proceeds — blank input, EOF, "n", and anything malformed
1172
- // all decline without writing.
1173
- return answer === "y" || answer === "yes" ? { kind: "consented" } : { kind: "declined" };
1231
+ const answer = await deps.promptGuidanceLine(buildAgentNotesPrompt(platformLabels));
1232
+ return agentGuidancePromptConsents(answer) ? { kind: "consented" } : { kind: "declined" };
1174
1233
  }
1175
1234
  catch {
1176
1235
  // The install is already durable; a failure here is never fatal to it.
1177
- log(" note: the agent-notes offer could not run. No notes files were written.");
1178
- return { kind: "declined" };
1236
+ deps.errorLog("Agent guidance skipped: the prompt could not be read; no guidance files were changed.");
1237
+ return { kind: "prompt-error" };
1179
1238
  }
1180
1239
  }
1181
1240
  /**
@@ -1192,11 +1251,13 @@ export function buildAgentNotesSummaryLine(offer, result, writeFailed, unsupport
1192
1251
  return unsupportedPlatforms.length > 0 ? `Agent notes: not written${unsupportedNote}.` : undefined;
1193
1252
  }
1194
1253
  if (offer.kind === "headless-skip") {
1195
- return `Agent notes: skipped (non-interactive session)${unsupportedNote}.`;
1254
+ return `Agent notes: skipped (non-interactive session or submission provenance unavailable)${unsupportedNote}.`;
1196
1255
  }
1197
1256
  if (offer.kind === "declined") {
1198
- return `Agent notes: skipped (declined)${unsupportedNote}.`;
1257
+ return `Agent notes: skipped (declined or no submitted consent)${unsupportedNote}.`;
1199
1258
  }
1259
+ if (offer.kind === "prompt-error")
1260
+ return undefined;
1200
1261
  if (writeFailed || !result) {
1201
1262
  return `Agent notes: could not be written (unexpected error)${unsupportedNote}.`;
1202
1263
  }
@@ -1235,6 +1296,56 @@ export function promptLineViaReadline(promptText, input = process.stdin, output
1235
1296
  });
1236
1297
  });
1237
1298
  }
1299
+ /** Observe CR/LF before readline, counting split CRLF as one submission. */
1300
+ export function promptGuidanceLineViaReadline(promptText, input = process.stdin, output = process.stderr) {
1301
+ return new Promise((resolve) => {
1302
+ const readable = input;
1303
+ if (readable.readableEnded || readable.destroyed) {
1304
+ resolve({ kind: "eof" });
1305
+ return;
1306
+ }
1307
+ let terminators = 0;
1308
+ let previousCR = false;
1309
+ let settled = false;
1310
+ let rl;
1311
+ const observe = (chunk) => {
1312
+ for (const char of chunk.toString()) {
1313
+ if (char === "\r")
1314
+ terminators++;
1315
+ else if (char === "\n" && !previousCR)
1316
+ terminators++;
1317
+ previousCR = char === "\r";
1318
+ }
1319
+ };
1320
+ const finish = (result) => {
1321
+ if (settled)
1322
+ return;
1323
+ settled = true;
1324
+ input.removeListener("data", observe);
1325
+ input.removeListener("end", eof);
1326
+ input.removeListener("close", eof);
1327
+ input.removeListener("error", eof);
1328
+ rl?.removeListener("close", eof);
1329
+ rl?.close();
1330
+ resolve(result);
1331
+ };
1332
+ const eof = () => finish({ kind: "eof" });
1333
+ input.prependListener("data", observe);
1334
+ input.once("end", eof);
1335
+ input.once("close", eof);
1336
+ input.once("error", eof);
1337
+ rl = readline.createInterface({ input, output });
1338
+ rl.once("close", eof);
1339
+ if (settled || readable.readableEnded || readable.destroyed) {
1340
+ rl.close();
1341
+ eof();
1342
+ return;
1343
+ }
1344
+ rl.question(promptText, (value) => {
1345
+ finish(terminators > 0 ? { kind: "answer", value } : { kind: "eof" });
1346
+ });
1347
+ });
1348
+ }
1238
1349
  /**
1239
1350
  * Default pre-warm spawner (BAPI-451 W3). Spawns array-based (`shell: false`) so
1240
1351
  * no argument is ever shell-interpreted, ignores all child stdio (node-gyp /
@@ -1328,6 +1439,7 @@ export function createDefaultInstallBridgeDeps() {
1328
1439
  return {
1329
1440
  env: process.env,
1330
1441
  cwd: process.cwd(),
1442
+ realpath: async (folder) => realpath(folder),
1331
1443
  platform: process.platform,
1332
1444
  homedir: os.homedir,
1333
1445
  isTTY,
@@ -1349,6 +1461,7 @@ export function createDefaultInstallBridgeDeps() {
1349
1461
  randomBytes: (size) => cryptoRandomBytes(size),
1350
1462
  promptSecret: isTTY ? promptSecretViaReadline : undefined,
1351
1463
  promptLine: isTTY ? promptLineViaReadline : undefined,
1464
+ promptGuidanceLine: isTTY ? promptGuidanceLineViaReadline : undefined,
1352
1465
  promptMultiSelect: isTTY ? promptMultiSelectViaReadline : undefined,
1353
1466
  vendor: createDefaultVendorProcessDeps(spawn),
1354
1467
  fetch: productionFetch,
@@ -1643,6 +1756,77 @@ function defaultShellFreeRunnerWithEnv(executable, args, extraEnv) {
1643
1756
  child.on("close", (code) => resolve({ exitCode: code ?? 1, stdout, stderr }));
1644
1757
  });
1645
1758
  }
1759
+ /**
1760
+ * BAPI-1405 — the owner's own `gh`, shell-free, for the epic-branch CI step.
1761
+ *
1762
+ * Same inherited environment as {@link defaultShellFreeRunnerWithEnv} (the
1763
+ * customer's own login is the point), plus an optional stdin body so a ruleset
1764
+ * update can use `gh api --input -` and keep JSON out of argv. A spawn failure
1765
+ * (no `gh` on PATH) resolves as exit 127 rather than throwing.
1766
+ */
1767
+ export function defaultUserGhRunner(args, opts) {
1768
+ return new Promise((resolve) => {
1769
+ const child = spawn("gh", args, {
1770
+ shell: false,
1771
+ stdio: [opts?.input === undefined ? "ignore" : "pipe", "pipe", "pipe"],
1772
+ env: { ...process.env, GH_PROMPT_DISABLED: "1", GH_NO_UPDATE_NOTIFIER: "1" },
1773
+ });
1774
+ let stdout = "";
1775
+ let stderr = "";
1776
+ child.stdout?.on("data", (chunk) => {
1777
+ stdout += chunk.toString("utf-8");
1778
+ });
1779
+ child.stderr?.on("data", (chunk) => {
1780
+ stderr += chunk.toString("utf-8");
1781
+ });
1782
+ child.on("error", () => resolve({ exitCode: 127, stdout: "", stderr: "gh could not be started" }));
1783
+ child.on("close", (code) => resolve({ exitCode: code ?? 1, stdout, stderr }));
1784
+ if (opts?.input !== undefined) {
1785
+ child.stdin?.on("error", () => undefined);
1786
+ child.stdin?.end(opts.input);
1787
+ }
1788
+ });
1789
+ }
1790
+ /**
1791
+ * BAPI-1405 — the production epic-branch CI seams for one repository root.
1792
+ *
1793
+ * Provider detection reads `origin` with the owner's own git; only a canonical
1794
+ * github.com remote counts as GitHub (the same parser the merge-workflow
1795
+ * installer uses). The example epic branch is a local remote-tracking
1796
+ * `origin/epic/*` ref when one exists — a cheap, offline read — and otherwise
1797
+ * the gate module's never-created probe name.
1798
+ */
1799
+ export function buildEpicBranchCiDeps(cwd) {
1800
+ const git = async (args) => {
1801
+ try {
1802
+ const r = await defaultShellFreeRunner("git", ["-C", cwd, ...args]);
1803
+ return r.exitCode === 0 ? r.stdout.trim() : null;
1804
+ }
1805
+ catch {
1806
+ return null;
1807
+ }
1808
+ };
1809
+ return {
1810
+ fs: createNodeGateDiscoveryFs(cwd),
1811
+ runGh: defaultUserGhRunner,
1812
+ detectProvider: async () => {
1813
+ const remoteUrl = await git(["remote", "get-url", "origin"]);
1814
+ if (!remoteUrl)
1815
+ return { kind: "unknown" };
1816
+ const ownerRepo = parseGithubRemote(remoteUrl);
1817
+ return ownerRepo ? { kind: "github", ownerRepo } : { kind: "not_github" };
1818
+ },
1819
+ findExampleEpicBranch: async () => {
1820
+ const ref = await git([
1821
+ "for-each-ref",
1822
+ "--count=1",
1823
+ "--format=%(refname:strip=3)",
1824
+ "refs/remotes/origin/epic/",
1825
+ ]);
1826
+ return ref ? ref.split("\n")[0].trim() || null : null;
1827
+ },
1828
+ };
1829
+ }
1646
1830
  /** Presence probe: `<tool> --version` exiting 0 means the tool resolves on PATH. */
1647
1831
  async function defaultExecutablePresenceProbe(name) {
1648
1832
  try {
@@ -1954,7 +2138,15 @@ const INSTALL_BRIDGE_SIGNUP_EMAIL_SHAPE = /^[^@\s]+@[^@\s]+$/;
1954
2138
  /** The one hint line printed after a blank/malformed email, before the single re-prompt. */
1955
2139
  export const INSTALL_BRIDGE_SIGNUP_EMAIL_HINT = "Enter a valid email address (e.g. you@example.com).";
1956
2140
  export function buildInstallBridgeSignupPrivacyNotice(baseUrl) {
1957
- return `Privacy Policy: ${baseUrl.replace(/\/+$/, "")}/privacy`;
2141
+ return `Your email is used to verify signup and set up your Bridge workspace. Privacy Policy — how data is handled: ${baseUrl.replace(/\/+$/, "")}/privacy`;
2142
+ }
2143
+ export function buildInstallBridgeMaintenanceHint() {
2144
+ return [
2145
+ `Bridge MCP installer package: ${MCP_PACKAGE_NAME}@${VERSION}`,
2146
+ "From the project root, use /upgrade-bridge or run:",
2147
+ `npx -y ${MCP_PACKAGE_NAME}@latest --upgrade`,
2148
+ "Persistent host registrations remain pinned until an explicit upgrade; the editor may need to reconnect to use the new server.",
2149
+ ];
1958
2150
  }
1959
2151
  export async function resolveSignupEmail(options, deps, baseUrl = DEFAULT_BAPI_BASE_URL) {
1960
2152
  // BAPI-757 (R-3 / AC-9): self-serve signup now REQUIRES an interactive terminal,
@@ -2385,9 +2577,14 @@ const OPTIMIZATION_POSTURE_MAX_CORRECTIONS = 20;
2385
2577
  */
2386
2578
  function buildOptimizationPostureMenu(primaryPlatform) {
2387
2579
  return ("How should Bridge optimize its work on this repository?\n" +
2388
- " 1. Quality\n" +
2389
- " 2. Speed\n" +
2390
- ` 3. ${buildOptimizationPostureOptionThree(primaryPlatform)}\n` +
2580
+ " 1. Quality — fuller planning and review policy.\n" +
2581
+ " 2. Speed — a combined planning pass and normally one review round.\n" +
2582
+ ` 3. ${buildOptimizationPostureOptionThree(primaryPlatform)} — leaner planning/review defaults\n` +
2583
+ " and lower harness model tiers where supported.\n" +
2584
+ "These are active defaults, subject to explicit policy/model overrides.\n" +
2585
+ `Your ${optimizationPostureSubscriptionNounPhrase(primaryPlatform)} is separate from Bridge credits.\n` +
2586
+ "Enter keeps an existing saved preference; projects without one use Quality.\n" +
2587
+ "You can change this later with /teach-bridge.\n" +
2391
2588
  `${OPTIMIZATION_POSTURE_CORRECTION} `);
2392
2589
  }
2393
2590
  /**
@@ -2496,7 +2693,7 @@ const OPTIMIZATION_POSTURE_UNCHANGED_MESSAGE = ` Optimization posture was not c
2496
2693
  */
2497
2694
  function reportOptimizationPostureOutcome(log, result, value, primaryPlatform) {
2498
2695
  if (result.kind === "success") {
2499
- log(` Preference saved for later activation — we'll optimize for ${optimizationPostureConfirmationPhrase(value, primaryPlatform)}.`);
2696
+ log(` Preference saved — we'll optimize for ${optimizationPostureConfirmationPhrase(value, primaryPlatform)}.`);
2500
2697
  log(` ${OPTIMIZATION_POSTURE_BLOG_URL}`);
2501
2698
  return;
2502
2699
  }
@@ -3386,6 +3583,7 @@ async function removeUnprovenBridgeConfig(deps, createdText) {
3386
3583
  * ONCE for the whole batch, so selecting both does not print it twice.
3387
3584
  */
3388
3585
  async function provisionSelectedGlobalTargets(deps, platforms, entry, needKey) {
3586
+ let complete = true;
3389
3587
  const logLines = [];
3390
3588
  const duplicateRegistrations = [];
3391
3589
  let anyManualRequired = false;
@@ -3418,14 +3616,17 @@ async function provisionSelectedGlobalTargets(deps, platforms, entry, needKey) {
3418
3616
  logLines.push(` configured ${target.label} (${outcome.displayPath})`);
3419
3617
  break;
3420
3618
  case "manual-required":
3619
+ complete = false;
3421
3620
  anyManualRequired = true;
3422
3621
  logLines.push(` ${target.label}: add the ${MCP_SERVER_NAME} MCP server manually to ${outcome.displayPath} ` +
3423
3622
  "(the API key is redacted in printed instructions).");
3424
3623
  break;
3425
3624
  case "skipped-invalid":
3625
+ complete = false;
3426
3626
  logLines.push(` ${target.label}: skipped ${outcome.displayPath} — existing config is not valid; left untouched.`);
3427
3627
  break;
3428
3628
  case "skipped-duplicate-registration":
3629
+ complete = false;
3429
3630
  // BAPI-807: NOT an ordinary vendor-fallback condition. The target was
3430
3631
  // left byte-identical and the caller must fail loudly rather than report
3431
3632
  // a configured host.
@@ -3433,6 +3634,7 @@ async function provisionSelectedGlobalTargets(deps, platforms, entry, needKey) {
3433
3634
  logLines.push(` ${target.label}: skipped ${outcome.displayPath} — ${outcome.detail}`);
3434
3635
  break;
3435
3636
  case "failed":
3637
+ complete = false;
3436
3638
  logLines.push(` ${target.label}: could not be configured automatically; configure it manually.`);
3437
3639
  break;
3438
3640
  }
@@ -3440,7 +3642,7 @@ async function provisionSelectedGlobalTargets(deps, platforms, entry, needKey) {
3440
3642
  if (needKey && anyManualRequired) {
3441
3643
  logLines.push(` ${formatNeedKeyCredentialStoreLine(needKey.repoName, needKey.credentialStorePath)}`);
3442
3644
  }
3443
- return { logLines, duplicateRegistrations };
3645
+ return { logLines, duplicateRegistrations, complete };
3444
3646
  }
3445
3647
  /** Build the `/jira/ping` URL exactly like the MCP `ping` tool / buildGetUrl. */
3446
3648
  export function buildPingUrl(baseUrl, repoName) {
@@ -3642,8 +3844,20 @@ export function generateBootstrapKeySecret(randomBytes) {
3642
3844
  * the identity guard in both shapes, and prepare / repoint / promote / discard all
3643
3845
  * still match on it.
3644
3846
  */
3645
- export function fingerprintBootstrapInvite(token) {
3646
- return createHash("sha256").update(token, "utf-8").digest("hex");
3847
+ export function fingerprintBootstrapInvite(token, baseUrl) {
3848
+ if (baseUrl === undefined)
3849
+ return createHash("sha256").update(token, "utf-8").digest("hex");
3850
+ return fingerprintBootstrapFields("bridge-bootstrap-invite-v1", [baseUrl, token]);
3851
+ }
3852
+ function fingerprintBootstrapFields(domain, fields) {
3853
+ const hash = createHash("sha256").update(domain + "\0", "utf-8");
3854
+ for (const field of fields) {
3855
+ const bytes = Buffer.from(field, "utf-8");
3856
+ const length = Buffer.alloc(4);
3857
+ length.writeUInt32BE(bytes.length);
3858
+ hash.update(length).update(bytes);
3859
+ }
3860
+ return hash.digest("hex");
3647
3861
  }
3648
3862
  /** The exchange endpoint: `<base>/setup/bootstrap`. No query string — ever. */
3649
3863
  export function buildBootstrapExchangeUrl(baseUrl) {
@@ -3659,15 +3873,15 @@ export function buildBootstrapExchangeUrl(baseUrl) {
3659
3873
  * the legacy three-field one, byte for byte.
3660
3874
  */
3661
3875
  export async function exchangeBootstrapInvite(deps, baseUrl, token, repoName, keySecret, email) {
3876
+ const modern = typeof repoName !== "string";
3877
+ const identity = modern ? { repo_display_name: validateBootstrapDisplayName(repoName.repoDisplayName) } : { repo_name: repoName };
3662
3878
  const url = buildBootstrapExchangeUrl(baseUrl);
3663
3879
  let resp;
3664
3880
  try {
3665
3881
  resp = await deps.fetch(url, {
3666
3882
  method: "POST",
3667
3883
  headers: { "Content-Type": "application/json" },
3668
- body: JSON.stringify(email
3669
- ? { token, repo_name: repoName, key_secret: keySecret, email }
3670
- : { token, repo_name: repoName, key_secret: keySecret }),
3884
+ body: JSON.stringify({ token, ...identity, key_secret: keySecret, ...(email ? { email } : {}) }),
3671
3885
  signal: AbortSignal.timeout(10_000),
3672
3886
  });
3673
3887
  }
@@ -3683,13 +3897,17 @@ export async function exchangeBootstrapInvite(deps, baseUrl, token, repoName, ke
3683
3897
  ok: false,
3684
3898
  kind: "failed",
3685
3899
  message: `Could not reach the Bridge API at ${baseUrl} to redeem the bootstrap invite. Check ` +
3686
- "BAPI_BASE_URL and your network. The invite has not been used.",
3900
+ "BAPI_BASE_URL and your network. Creation outcome is unknown; preserve proof and rerun the same attempt.",
3687
3901
  };
3688
3902
  }
3689
3903
  if (resp.ok) {
3690
3904
  let repo;
3691
3905
  try {
3692
3906
  const body = (await resp.json());
3907
+ if (modern) {
3908
+ const identity = validateBootstrapServerIdentity(body);
3909
+ return { ok: true, repoName: identity.repo_name, displayName: identity.repo_display_name };
3910
+ }
3693
3911
  repo = body?.repo_name;
3694
3912
  }
3695
3913
  catch {
@@ -3711,7 +3929,7 @@ export async function exchangeBootstrapInvite(deps, baseUrl, token, repoName, ke
3711
3929
  }
3712
3930
  return { ok: true, repoName: validated.value };
3713
3931
  }
3714
- if (resp.status === 409) {
3932
+ if (resp.status === 409 && !modern) {
3715
3933
  return {
3716
3934
  ok: false,
3717
3935
  kind: "repo-name-taken",
@@ -3744,7 +3962,7 @@ export async function exchangeBootstrapInvite(deps, baseUrl, token, repoName, ke
3744
3962
  return {
3745
3963
  ok: false,
3746
3964
  kind: "failed",
3747
- message: `The bootstrap exchange failed (HTTP ${resp.status}). Verify BAPI_BASE_URL and try again.`,
3965
+ message: `The bootstrap exchange failed (HTTP ${resp.status}). Preserve proof and rerun the same attempt; verify server compatibility and contact installation support if it persists.`,
3748
3966
  };
3749
3967
  }
3750
3968
  /** The invite-email prompt (BAPI-1263), shown once after an `email_required` refusal. */
@@ -4699,7 +4917,7 @@ function buildAgentNotesPreviewLines(notesTargets, platformLabels) {
4699
4917
  `Step 4c — optional: Bridge MCP guidance for ${subject} (SKIPPED in --dry-run): if the`,
4700
4918
  ` terminal is interactive, offer to write guidance so ${subject} know${plural ? "" : "s"} when to reach`,
4701
4919
  ` for Bridge's MCP tools, in a Bridge-owned marker region in ${notesTargets.join(" and ")}; defaults`,
4702
- " to No, and existing content outside that region is always preserved. No notes files are",
4920
+ " to Yes [Y/n] on submitted Enter; EOF skips. Existing content outside that region is preserved. No notes files are",
4703
4921
  " written in --dry-run.",
4704
4922
  ];
4705
4923
  }
@@ -5177,6 +5395,10 @@ export function isMinttyNonInteractiveInstall(platform, env, isTTY) {
5177
5395
  }
5178
5396
  export async function runInstallBridgeCli(argv, overrides = {}) {
5179
5397
  const deps = { ...createDefaultInstallBridgeDeps(), ...overrides };
5398
+ if (Object.prototype.hasOwnProperty.call(overrides, "promptLine") &&
5399
+ !Object.prototype.hasOwnProperty.call(overrides, "promptGuidanceLine")) {
5400
+ deps.promptGuidanceLine = undefined;
5401
+ }
5180
5402
  // When the caller overrode only `fetch` (the common test seam), bind the
5181
5403
  // default resolver to the FINAL merged fetch so server resolution still routes
5182
5404
  // through the override. An explicit `resolveRepoViaServer` override is honored.
@@ -5315,6 +5537,20 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
5315
5537
  // Built from the FINAL merged deps for the same reason (BAPI-775): a test
5316
5538
  // that overrides only `readFile` or only `cwd` must see its override here.
5317
5539
  localProvisioning: buildConductorLocalDeps(deps),
5540
+ // BAPI-1391: the read-only discovery filesystem over THIS working tree, and
5541
+ // the short-lived qualifying executor (the existing executor CLI with a
5542
+ // `runExecutor` override limiting it to `gate_qualification` jobs).
5543
+ gate: deps.conductorGate ?? {
5544
+ discoveryFs: createNodeGateDiscoveryFs(deps.cwd),
5545
+ // BAPI-1405: epic-branch CI inspection over this working tree, with the
5546
+ // owner's own shell-free `gh`.
5547
+ epicBranchCi: buildEpicBranchCiDeps(deps.cwd),
5548
+ runQualificationExecutor: createQualificationExecutorRunner({
5549
+ runExecutorCli,
5550
+ runExecutor,
5551
+ errorLog: deps.errorLog,
5552
+ }),
5553
+ },
5318
5554
  };
5319
5555
  return runInstallBridgeConductorCli(argv.slice(1), conductorDeps);
5320
5556
  }
@@ -5330,6 +5566,29 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
5330
5566
  return 1;
5331
5567
  }
5332
5568
  const options = parsed.options;
5569
+ if (options.displayName !== undefined && (options.repo !== undefined || deps.env.BAPI_REPO_NAME?.trim())) {
5570
+ errorLog("Error: --display-name conflicts with --repo or BAPI_REPO_NAME. Use a friendly label for creation or a canonical selector for legacy creation/reconnect.");
5571
+ return 1;
5572
+ }
5573
+ if (options.displayName !== undefined && !options.inviteMode && !options.email && !deps.env.BAPI_INVITE?.trim() && !deps.env.BAPI_SIGNUP_EMAIL?.trim() && (options.apiKey !== undefined || deps.env.BAPI_API_KEY?.trim())) {
5574
+ errorLog("Error: --display-name applies only to new creation. Existing keys use canonical --repo selection or single-project resolution.");
5575
+ return 1;
5576
+ }
5577
+ // ---- BAPI-1398: --remove-bridge-permissions — removal only, nothing else ----
5578
+ // Ahead of the base-URL check, onboarding, every prompt, every credential read,
5579
+ // and every network call: it touches only the two settings files and Bridge's
5580
+ // ownership manifest.
5581
+ if (options.removeBridgePermissions) {
5582
+ return runBridgePermissionsRemoval({
5583
+ cwd: deps.cwd,
5584
+ homeDir: deps.homedir(),
5585
+ readFile: deps.readFile,
5586
+ writeFile: (p, data) => deps.writeFile(p, data, { encoding: "utf-8" }),
5587
+ mkdir: (p, o) => deps.mkdir(p, o),
5588
+ log,
5589
+ errorLog,
5590
+ });
5591
+ }
5333
5592
  // Deliberately still ahead of the onboarding branch, any prompt, any network
5334
5593
  // call, and the --dry-run preview: a scheme-less value (the classic error) used
5335
5594
  // to throw ERR_INVALID_URL deep inside `buildPingUrl` — even in --dry-run — often
@@ -5667,6 +5926,56 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
5667
5926
  // 404 is the BAPI-616 feature-detect for an older or self-hosted server, and
5668
5927
  // `error` covers a transient network failure or a 401. Collapsing these into
5669
5928
  // the hard stop would make a blip unrecoverable.
5929
+ if (!bootstrapInviteMode && options.displayName !== undefined) {
5930
+ errorLog("Error: --display-name applies only to creation; existing-key registration uses canonical selection.");
5931
+ return 1;
5932
+ }
5933
+ let modernAttempt;
5934
+ if (options.tools?.length === 0 && !options.agentName) {
5935
+ log(buildManualInstallBridgeContinuation("empty-selection", []));
5936
+ return 0;
5937
+ }
5938
+ if (bootstrapInviteMode && resolveConfiguredRepoName(options, deps.env) === undefined) {
5939
+ if (!selfServeSignupMode) {
5940
+ const legacy = await lookupLegacyBootstrapInvite(fingerprintBootstrapInvite(inviteToken), modernWriteDeps(deps));
5941
+ if (!legacy.ok) {
5942
+ errorLog(legacy.error);
5943
+ return 1;
5944
+ }
5945
+ if (legacy.repoName)
5946
+ options.repo = legacy.repoName;
5947
+ }
5948
+ else {
5949
+ // Historical default creation used the folder basename as its canonical name.
5950
+ const oldName = validateRepoName(path.basename(deps.cwd));
5951
+ if (oldName.ok) {
5952
+ const legacy = await deps.lookupSelfServeBootstrapPending({ repoName: oldName.value, now: deps.now() }, modernWriteDeps(deps));
5953
+ if (!legacy.ok) {
5954
+ errorLog(legacy.error);
5955
+ return 1;
5956
+ }
5957
+ if (legacy.state === "resumable" || legacy.state === "challenge")
5958
+ options.repo = oldName.value;
5959
+ if (legacy.state === "challenge-expired") {
5960
+ errorLog("Saved signup expired. Preserve proof and contact installation support.");
5961
+ return 1;
5962
+ }
5963
+ }
5964
+ }
5965
+ if (options.repo === undefined) {
5966
+ try {
5967
+ modernAttempt = await redeemModernInstallAttempt(deps, options, baseUrl, inviteToken, signupEmail, selfServeSignupMode);
5968
+ apiKey = modernAttempt.record.keySecret;
5969
+ options.repo = modernAttempt.repoName;
5970
+ bootstrapInviteMode = false;
5971
+ deps.log(`Project '${modernAttempt.displayName}' recovered; completing installation.`);
5972
+ }
5973
+ catch (error) {
5974
+ errorLog(error instanceof ModernInstallRecoveryError ? error.message : "Installation could not complete. Preserve recovery proof and resume from the same folder and invite/signup; contact installation support if needed.");
5975
+ return 1;
5976
+ }
5977
+ }
5978
+ }
5670
5979
  let repoName;
5671
5980
  /** The resolver's OUTCOME (C-4), or undefined when it was never called. */
5672
5981
  let serverRepoResolutionStatus;
@@ -5798,6 +6107,24 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
5798
6107
  // before any of the credential/repo/host resolution above — so by this point
5799
6108
  // the run is unconditionally the LIVE path. See the early return above for
5800
6109
  // the inert preview.
6110
+ // One advisory per live interactive handoff. Never construct probe dependencies
6111
+ // on dry-run/noninteractive paths, and never change install/consent outcomes.
6112
+ let advisoryShown = false;
6113
+ const showFirstBuildAdvisory = async (agent) => {
6114
+ if (advisoryShown || !deps.isTTY || options.dryRun)
6115
+ return;
6116
+ advisoryShown = true;
6117
+ try {
6118
+ const advisory = await collectFirstBuildAdvisory({
6119
+ decision: agent ? { kind: "spawn", agent } : { kind: "manual", reason: "manual-continuation" },
6120
+ configuredHosts: labelsForPlatforms(selectedPlatforms),
6121
+ }, { ...deps.startTicketsDeps, platform: deps.platform, env: deps.env, cwd: deps.cwd });
6122
+ log(formatFirstBuildAdvisory(advisory));
6123
+ }
6124
+ catch {
6125
+ log(`First-build prerequisites: diagnostic availability unknown. Run ${INSTALL_BRIDGE_DOCTOR_COMMAND} for read-only follow-up.`);
6126
+ }
6127
+ };
5801
6128
  // ---- Resolved-empty selection: a handled no-op (exit 0), never a Claude fallback ----
5802
6129
  // AC-3: with zero configured tools there is nothing to write and no session to open.
5803
6130
  // Return BEFORE any config/credential write, materialization, prewarm, or spawn, so a
@@ -5805,6 +6132,7 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
5805
6132
  if (planLaunch.kind === "manual" && planLaunch.reason === "empty-selection") {
5806
6133
  // Explicit empty selection: no labels to name, so the generic fallback is
5807
6134
  // intentional here — pass [] rather than `labelsForPlatforms(selectedPlatforms)`.
6135
+ await showFirstBuildAdvisory(null);
5808
6136
  log(buildManualInstallBridgeContinuation("empty-selection", []));
5809
6137
  return 0;
5810
6138
  }
@@ -6467,7 +6795,7 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
6467
6795
  log(currentStep);
6468
6796
  }
6469
6797
  // BAPI-668: a bootstrap-minted key is verified WITHOUT source attribution.
6470
- const ping = await verifyConnectivity(deps, baseUrl, repoName, apiKey, bootstrapInviteMode ? undefined : apiKeySource);
6798
+ const ping = modernAttempt ? { ok: true } : await verifyConnectivity(deps, baseUrl, repoName, apiKey, bootstrapInviteMode ? undefined : apiKeySource);
6471
6799
  if (!ping.ok) {
6472
6800
  // BAPI-667: in either need-key mode the exchange already succeeded and the
6473
6801
  // pending record survives, so this failure IS resumable — but only with the
@@ -6482,6 +6810,11 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
6482
6810
  // file. Advisory: it warns on any problem and never fails the install.
6483
6811
  unprovenBridgeConfigPending = false;
6484
6812
  await reconcileBridgeConfigAfterPing(deps, repoName, scaffoldedBridgeConfigText);
6813
+ if (modernAttempt) {
6814
+ const reconciled = parseBridgeConfigToml(await deps.readFile(bridgeConfigPath(deps.cwd)));
6815
+ if (!reconciled.ok || reconciled.manifest.repoName !== repoName)
6816
+ return fatal("Installation incomplete: canonical configuration read-back failed. Preserve proof and resume from this folder.");
6817
+ }
6485
6818
  // ---- BAPI-1221 (S2): persist the captured posture, now that connectivity
6486
6819
  // is validated and `repoName`/`apiKey` are both final. Overwrites any
6487
6820
  // existing value unconditionally — the old value is never read first. A
@@ -6535,6 +6868,21 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
6535
6868
  trackedState.set(target.relPath, await isTrackedProjectConfig(deps, target.relPath));
6536
6869
  }
6537
6870
  const writeResult = await writeHostConfigs(deps, targets, { real: entry, secretFree: secretFreeEntry }, trackedState, { repoName, credentialStorePath });
6871
+ if (modernAttempt) {
6872
+ if (writeResult.skipped.length || writeResult.written.length !== targets.length) {
6873
+ return fatal("Installation incomplete: selected host configuration was declined or failed. Recovery proof is preserved.");
6874
+ }
6875
+ for (const item of writeResult.written) {
6876
+ const config = JSON.parse(await deps.readFile(path.join(deps.cwd, item.relPath)));
6877
+ const target = targets.find(target => target.relPath === item.relPath);
6878
+ const binding = target ? config[target.topLevelKey]?.[item.registrationKey] : undefined;
6879
+ const keyMatches = item.mode === "secret-free"
6880
+ ? binding?.env && !Object.prototype.hasOwnProperty.call(binding.env, "BAPI_API_KEY")
6881
+ : binding?.env?.BAPI_API_KEY === apiKey;
6882
+ if (binding?.env?.BAPI_REPO_NAME !== repoName || !keyMatches)
6883
+ return fatal("Installation incomplete: host binding read-back failed. Recovery proof is preserved.");
6884
+ }
6885
+ }
6538
6886
  // BAPI-807: a both-key project config is a FAIL-LOUD condition, not an
6539
6887
  // ordinary skip. Nothing was written (the pre-write scan returned before the
6540
6888
  // first target), and continuing would report a successful install over a
@@ -6572,6 +6920,8 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
6572
6920
  const globalProvisioning = await provisionSelectedGlobalTargets(deps, selectedPlatforms, entry, needKeyManual);
6573
6921
  for (const line of globalProvisioning.logLines)
6574
6922
  log(line);
6923
+ if (modernAttempt && !globalProvisioning.complete)
6924
+ return fatal("Installation incomplete: a selected host requires completion. Recovery proof is preserved.");
6575
6925
  // BAPI-807: a global target that refused because it already carries both
6576
6926
  // registrations is fail-loud, exactly like the project-config case above —
6577
6927
  // never an ordinary vendor fallback the run can shrug off.
@@ -6653,7 +7003,21 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
6653
7003
  // in bootstrap mode the exchange loop above is the ONLY way to reach here, so the
6654
7004
  // two are equivalent — but keying promotion to the same state that justifies the
6655
7005
  // catch block's resume guidance makes that invariant explicit and un-driftable.
6656
- if (bootstrapExchangeSucceeded) {
7006
+ if (modernAttempt) {
7007
+ currentStep = INSTALL_BRIDGE_STEP_LABELS.persistCredential;
7008
+ log(currentStep);
7009
+ let promoted = await promoteModernBootstrapCredential(modernAttempt.scope, modernAttempt.record, credentialWriteDeps, options.force);
7010
+ if (!promoted.ok && promoted.needsConsent && deps.isTTY && deps.promptLine) {
7011
+ const consent = (await deps.promptLine("Replace the existing canonical project credential? [y/N]: ")).trim().toLowerCase();
7012
+ if (consent === "y" || consent === "yes")
7013
+ promoted = await promoteModernBootstrapCredential(modernAttempt.scope, modernAttempt.record, credentialWriteDeps, true, promoted.observedKey);
7014
+ }
7015
+ if (!promoted.ok)
7016
+ return fatal("Installation incomplete: credential promotion failed. Preserve proof and resume from the same folder.");
7017
+ log(formatProjectCredentialStoreLine(repoName, credentialStorePath));
7018
+ log(`Project '${modernAttempt.displayName}' installed successfully.`);
7019
+ }
7020
+ else if (bootstrapExchangeSucceeded) {
6657
7021
  // FAIL-CLOSED (unlike normal mode below). The pending secret IS the minted admin
6658
7022
  // key — if it never lands under bapi:<repo>, the user has a live key they cannot
6659
7023
  // resolve. Promotion removes the pending record and writes the credential in one
@@ -6759,6 +7123,7 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
6759
7123
  for (const file of notesResult.files) {
6760
7124
  if (file.status === "failed") {
6761
7125
  errorLog(` warning: could not update ${file.target} with agent notes (${file.reason}).`);
7126
+ errorLog(` Resolve the reported issue in ${file.target}, then rerun an interactive install to retry guidance.`);
6762
7127
  }
6763
7128
  }
6764
7129
  }
@@ -6772,6 +7137,31 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
6772
7137
  const notesSummaryLine = buildAgentNotesSummaryLine(notesOffer, notesResult, notesWriteFailed, notesPlan.unsupportedPlatforms);
6773
7138
  if (notesSummaryLine)
6774
7139
  log(notesSummaryLine);
7140
+ // ---- BAPI-1398: Bridge permission rules for /drive-epic-run (Claude Code) ----
7141
+ // After the durable install work and the other offers, before the session
7142
+ // opens. Project rules are written as part of the normal install; the user
7143
+ // settings change asks once and is skipped without a yes or a terminal. Every
7144
+ // failure here is advisory: a malformed settings file is reported and skipped,
7145
+ // and the install still completes.
7146
+ if (selectedPlatforms.includes("claude-code")) {
7147
+ try {
7148
+ await (deps.installBridgePermissions ?? runBridgePermissionsInstallStep)({
7149
+ cwd: deps.cwd,
7150
+ homeDir: deps.homedir(),
7151
+ env: deps.env,
7152
+ promptLine: deps.isTTY ? deps.promptLine : undefined,
7153
+ readFile: deps.readFile,
7154
+ writeFile: (p, data) => deps.writeFile(p, data, { encoding: "utf-8" }),
7155
+ mkdir: (p, o) => deps.mkdir(p, o),
7156
+ log,
7157
+ errorLog,
7158
+ ensureGitignored: (relPath) => ensureGitignoredShared(deps.cwd, relPath, gitignoreDeps),
7159
+ });
7160
+ }
7161
+ catch {
7162
+ errorLog(" warning: could not write Bridge's permission rules (unexpected error; non-fatal).");
7163
+ }
7164
+ }
6775
7165
  // ---- Step 3b (await) — settle the background pre-warm before the session opens ----
6776
7166
  // The warm was kicked off (only on a spawn-capable path) right after the
6777
7167
  // launch-command guards; await it HERE so the bucket is settled before the spawned
@@ -6794,6 +7184,9 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
6794
7184
  `The first MCP launch may pay a one-time cold install and could be slow. ${MCP_TIMEOUT_GUIDANCE}`);
6795
7185
  }
6796
7186
  }
7187
+ for (const line of buildInstallBridgeMaintenanceHint())
7188
+ log(line);
7189
+ await showFirstBuildAdvisory(finalAgentName);
6797
7190
  // ---- Step 5 — consent-gate, then spawn the selected tool (or print continuation) ----
6798
7191
  // AC-7: for a spawn-capable path, ask Y/N first (`requestInstallBridgeLaunchConsent`
6799
7192
  // resolves to no-spawn without prompting on a non-TTY run). A decline, a no-launchable
@@ -6908,3 +7301,117 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
6908
7301
  }
6909
7302
  }
6910
7303
  }
7304
+ class ModernInstallRecoveryError extends Error {
7305
+ }
7306
+ function modernWriteDeps(deps) {
7307
+ return { env: deps.env, homedir: deps.homedir, platform: deps.platform, readFile: deps.readFile,
7308
+ mkdir: deps.mkdir, writeFile: deps.writeFile, rename: deps.rename, chmod: deps.chmod, unlink: deps.unlink, open: deps.open };
7309
+ }
7310
+ async function redeemModernInstallAttempt(deps, options, baseUrl, token, signupEmail, selfServe) {
7311
+ if (!deps.realpath)
7312
+ throw new ModernInstallRecoveryError("Canonical folder resolution is unavailable. Contact installation support.");
7313
+ const folder = await deps.realpath(deps.cwd);
7314
+ const folderDigest = fingerprintBootstrapFields("bridge-bootstrap-folder-v1", [baseUrl, folder]);
7315
+ const signupLocator = `bootstrap-pending:signup:${folderDigest}`;
7316
+ const makeScope = (locator) => ({ baseUrl, folder, locator, folderReference: `bootstrap-folder:${folderDigest}` });
7317
+ let fingerprint = token ? fingerprintBootstrapInvite(token, baseUrl) : "";
7318
+ let scope = makeScope(selfServe ? signupLocator : `bootstrap-pending:invite:${fingerprint}`);
7319
+ const writeDeps = modernWriteDeps(deps);
7320
+ const loaded = await lookupModernBootstrapRecord(scope, writeDeps);
7321
+ if (!loaded.ok)
7322
+ throw new Error(loaded.error);
7323
+ let record = loaded.record;
7324
+ const resumed = record !== null;
7325
+ let label = record?.label ?? options.displayName;
7326
+ if (label === undefined) {
7327
+ if (!deps.isTTY || !deps.promptLine)
7328
+ throw new ModernInstallRecoveryError("Modern invite creation requires --display-name in a non-interactive terminal.");
7329
+ let suggestion = "";
7330
+ try {
7331
+ suggestion = validateBootstrapDisplayName(path.basename(folder));
7332
+ }
7333
+ catch { /* explicit label required */ }
7334
+ const entered = await deps.promptLine(suggestion ? `Project display name [${suggestion}]: ` : "Project display name: ");
7335
+ label = validateBootstrapDisplayName(entered || suggestion);
7336
+ }
7337
+ if (selfServe) {
7338
+ if (!deps.isTTY || !deps.promptLine)
7339
+ throw new ModernInstallRecoveryError("Self-serve email verification requires an interactive terminal.");
7340
+ if (!record) {
7341
+ const intent = await prepareModernBootstrapSignup(scope, label, signupEmail, () => generateBootstrapKeySecret(deps.randomBytes), writeDeps);
7342
+ if (!intent.ok || !intent.record)
7343
+ throw new Error(intent.ok ? "Signup proof missing." : intent.error);
7344
+ record = intent.record;
7345
+ const requested = await requestSelfServeChallenge(deps, baseUrl, signupEmail);
7346
+ if (!requested.ok)
7347
+ throw new ModernInstallRecoveryError("Signup handle could not be established. Preserve recovery proof; contact installation support before restarting signup.");
7348
+ const saved = await mutateModernBootstrapRecord(scope, writeDeps, current => {
7349
+ if (!current)
7350
+ throw new ModernInstallRecoveryError("Signup proof missing.");
7351
+ return { ...current, challengeHandle: requested.handle, challengeExpiresAt: requested.expiresAt };
7352
+ });
7353
+ if (!saved.ok || !saved.record)
7354
+ throw new Error(saved.ok ? "Signup proof missing." : saved.error);
7355
+ record = saved.record;
7356
+ }
7357
+ if (record.replayToken && record.fingerprint) {
7358
+ token = record.replayToken;
7359
+ fingerprint = record.fingerprint;
7360
+ scope = makeScope(`bootstrap-pending:invite:${fingerprint}`);
7361
+ }
7362
+ else {
7363
+ if (!record.challengeHandle || !record.challengeExpiresAt)
7364
+ throw new ModernInstallRecoveryError("The previous signup response is unresolved. Preserve proof and contact installation support before restarting.");
7365
+ if (Date.parse(record.challengeExpiresAt) <= deps.now())
7366
+ throw new ModernInstallRecoveryError("The saved verification challenge expired. Preserve proof and contact installation support.");
7367
+ const code = await promptForVerificationCode(deps, deps.log);
7368
+ if (!code)
7369
+ throw new ModernInstallRecoveryError("Verification incomplete. Resume the same signup from this folder.");
7370
+ const verified = await verifySelfServeChallenge(deps, baseUrl, record.challengeHandle, code);
7371
+ if (!verified.ok)
7372
+ throw new ModernInstallRecoveryError("Verification did not complete. Preserve proof and resume the same signup.");
7373
+ token = verified.token;
7374
+ fingerprint = fingerprintBootstrapInvite(token, baseUrl);
7375
+ const inviteScope = makeScope(`bootstrap-pending:invite:${fingerprint}`);
7376
+ const transitioned = await transitionModernBootstrapSignup(makeScope(signupLocator), inviteScope, fingerprint, token, record.challengeHandle, writeDeps);
7377
+ if (!transitioned.ok || !transitioned.record)
7378
+ throw new Error(transitioned.ok ? "Signup proof missing." : transitioned.error);
7379
+ record = transitioned.record;
7380
+ scope = inviteScope;
7381
+ }
7382
+ }
7383
+ else {
7384
+ const prepared = await prepareModernBootstrapInvite(scope, fingerprint, label, () => generateBootstrapKeySecret(deps.randomBytes), writeDeps);
7385
+ if (!prepared.ok || !prepared.record)
7386
+ throw new Error(prepared.ok ? "Invite proof missing." : prepared.error);
7387
+ record = prepared.record;
7388
+ }
7389
+ if (!record)
7390
+ throw new ModernInstallRecoveryError("Recovery proof missing.");
7391
+ if (resumed)
7392
+ deps.log("Resuming the original installation; changing the entered label does not rename its project.");
7393
+ if (record.canonical && record.serverLabel) {
7394
+ const ping = await verifyConnectivity(deps, baseUrl, record.canonical, record.keySecret);
7395
+ if (!ping.ok)
7396
+ throw new ModernInstallRecoveryError("Canonical connectivity failed. Preserve proof and resume the same attempt.");
7397
+ return { scope, record, repoName: record.canonical, displayName: record.serverLabel };
7398
+ }
7399
+ let exchange = await exchangeBootstrapInvite(deps, baseUrl, token, { repoDisplayName: record.label }, record.keySecret);
7400
+ if (!exchange.ok && exchange.kind === "email-required") {
7401
+ const email = deps.isTTY ? await promptForBoundedEmail(deps, INVITE_EMAIL_PROMPT) : undefined;
7402
+ if (!email)
7403
+ throw new ModernInstallRecoveryError("An email is required; resume the same invite in an interactive terminal.");
7404
+ exchange = await exchangeBootstrapInvite(deps, baseUrl, token, { repoDisplayName: record.label }, record.keySecret, email);
7405
+ }
7406
+ if (!exchange.ok && exchange.message.includes("Creation outcome is unknown"))
7407
+ throw new ModernInstallRecoveryError("Creation outcome is unknown. Preserve recovery proof and rerun the same attempt from the original folder and invite/signup.");
7408
+ if (!exchange.ok || !exchange.displayName)
7409
+ throw new ModernInstallRecoveryError("Creation or replay did not complete. Preserve proof and rerun from this folder with the same invite/signup; verify server compatibility or contact installation support.");
7410
+ const checkpoint = await checkpointModernBootstrapIdentity(scope, record, exchange.repoName, exchange.displayName, writeDeps);
7411
+ if (!checkpoint.ok || !checkpoint.record)
7412
+ throw new Error(checkpoint.ok ? "Checkpoint missing." : checkpoint.error);
7413
+ const ping = await verifyConnectivity(deps, baseUrl, exchange.repoName, record.keySecret);
7414
+ if (!ping.ok)
7415
+ throw new ModernInstallRecoveryError("Canonical connectivity failed. Preserve proof and resume the same attempt.");
7416
+ return { scope, record: checkpoint.record, repoName: exchange.repoName, displayName: exchange.displayName };
7417
+ }