@bridge_gpt/mcp-server 0.2.69 → 0.2.70

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 (151) hide show
  1. package/README.md +140 -292
  2. package/build/agent-launchers/claude-executor-adapter.js +17 -4
  3. package/build/agents.generated.js +4 -4
  4. package/build/artifact-diff.js +110 -0
  5. package/build/bin-entrypoint.js +36 -0
  6. package/build/bridge-permissions.js +23 -4
  7. package/build/claude-review-workflow-drift.js +3 -3
  8. package/build/codex-bridge-profile.js +1 -1
  9. package/build/commands.generated.js +6 -6
  10. package/build/conductor/bridge-api-client.js +598 -19
  11. package/build/conductor/bring-up-facts.js +9 -9
  12. package/build/conductor/doctor.js +466 -18
  13. package/build/conductor/epic-child-continuation.js +54 -3
  14. package/build/conductor/epic-implementer-cli.js +132 -34
  15. package/build/conductor/epic-runtime.js +2 -2
  16. package/build/conductor/epic-state.js +1 -1
  17. package/build/conductor/hook-runtime-qualification.js +1 -1
  18. package/build/conductor/install-doctor.js +4 -2
  19. package/build/conductor/launch-need-probes.js +337 -0
  20. package/build/conductor/local-merge.js +241 -56
  21. package/build/conductor/merge-credential.js +172 -0
  22. package/build/conductor/merge-execution-eligibility.js +1 -1
  23. package/build/conductor/merged-child-check.js +95 -0
  24. package/build/conductor/plan-trim.js +30 -0
  25. package/build/conductor/pr-ci-producer.js +1 -1
  26. package/build/conductor/readiness-cli.js +11 -6
  27. package/build/conductor/readiness-prerequisites.js +41 -0
  28. package/build/conductor/readiness-sections.js +345 -55
  29. package/build/conductor/readiness.js +99 -87
  30. package/build/conductor/recovery-cli.js +606 -21
  31. package/build/conductor/recovery-operations.js +306 -7
  32. package/build/conductor/repair-contract.js +47 -2
  33. package/build/conductor/repository-workflow-remedy.js +8 -6
  34. package/build/conductor/stored-merge-credential.js +76 -0
  35. package/build/conductor/stored-merge-token-probe.js +28 -0
  36. package/build/conductor/temporary-plan.js +50 -0
  37. package/build/conductor/wait-run-cli.js +759 -0
  38. package/build/conductor/worker-database-doctor-inspector.js +49 -0
  39. package/build/conductor-bin.js +2 -2
  40. package/build/conductor-claude-hook-bin.js +2 -2
  41. package/build/conductor-claude-hook-removed-stub-bin.js +2 -5
  42. package/build/conductor-removed-stub-bin.js +2 -5
  43. package/build/credential-store.js +356 -13
  44. package/build/docs.generated.js +26 -13
  45. package/build/doctor.js +94 -32
  46. package/build/drive-epic-help.js +212 -0
  47. package/build/drive-epic.js +1840 -373
  48. package/build/epic-ci-workflow-drift.js +77 -0
  49. package/build/epic-ci-workflow-install.js +183 -0
  50. package/build/epic-ci-workflow.js +157 -0
  51. package/build/epic-contract-context.js +326 -0
  52. package/build/epic-implementer-bin.js +57 -41
  53. package/build/epic-implementer-claude-hook-bin.js +2 -2
  54. package/build/escalation-reachability.js +134 -0
  55. package/build/executor/cleanup-report.js +99 -0
  56. package/build/executor/cli.js +12 -1
  57. package/build/executor/completion-contract.js +66 -11
  58. package/build/executor/completion-supervision.js +99 -0
  59. package/build/executor/delivery-outcome.js +179 -0
  60. package/build/executor/deps.js +3 -0
  61. package/build/executor/env.js +71 -0
  62. package/build/executor/failure-reason-codes.js +126 -0
  63. package/build/executor/heartbeat.js +38 -0
  64. package/build/executor/http-client.js +72 -0
  65. package/build/executor/integration-job.js +298 -5
  66. package/build/executor/job-errors.js +36 -6
  67. package/build/executor/job-runner.js +720 -36
  68. package/build/executor/likely-tests.js +36 -2
  69. package/build/executor/merge-job.js +145 -2
  70. package/build/executor/observation.js +73 -4
  71. package/build/executor/permissions.js +193 -97
  72. package/build/executor/preflight.js +14 -3
  73. package/build/executor/process-containment.js +28 -2
  74. package/build/executor/prompt-spec.js +50 -5
  75. package/build/executor/rebase-finalization.js +38 -3
  76. package/build/executor/reconcile-overlap-job.js +35 -7
  77. package/build/executor/required-work-ledger.js +236 -81
  78. package/build/executor/resume-pre-spawn.js +38 -1
  79. package/build/executor/runner.js +91 -3
  80. package/build/executor/test-clock.js +8 -0
  81. package/build/executor/types.js +37 -2
  82. package/build/executor/worker-cli-launcher.js +259 -0
  83. package/build/executor/worker-command.js +41 -4
  84. package/build/executor/worker-database-target.js +91 -0
  85. package/build/executor/worker-database.js +908 -0
  86. package/build/executor/worker-finalization.js +106 -0
  87. package/build/executor/worker-guard-hook.js +203 -0
  88. package/build/executor/worker-resources.js +47 -0
  89. package/build/executor/worker-verification-guidance.js +15 -2
  90. package/build/executor/worktree.js +303 -15
  91. package/build/executor-launch.js +11 -1
  92. package/build/generated-assets-doctor.js +9 -1
  93. package/build/generated-assets-health.js +13 -4
  94. package/build/generated-assets-legacy.js +39 -0
  95. package/build/generated-assets-paths.js +12 -1
  96. package/build/generated-assets-repair.js +4 -1
  97. package/build/generated-assets-test-support.js +12 -0
  98. package/build/github-remote.js +33 -0
  99. package/build/github-sign-in.js +62 -0
  100. package/build/index.js +1067 -536
  101. package/build/init.js +39 -1
  102. package/build/install-bridge-conductor-gate.js +78 -0
  103. package/build/install-bridge-conductor-setup.js +413 -0
  104. package/build/install-bridge-conductor.js +1226 -267
  105. package/build/install-bridge.js +376 -60
  106. package/build/mcp-install-state.js +8 -2
  107. package/build/merge-pull-request.js +6 -0
  108. package/build/merge-workflow-install.js +763 -345
  109. package/build/phase-result-artifacts.js +75 -20
  110. package/build/pipeline-orchestrator.js +247 -4
  111. package/build/pipelines.generated.js +35 -35
  112. package/build/plan-discovery.js +15 -2
  113. package/build/plan-execution-ledger.js +306 -17
  114. package/build/plan-phase-routing.js +48 -13
  115. package/build/plane/cli.js +105 -51
  116. package/build/plane/defaults.js +3 -0
  117. package/build/plane/plane-freshness.js +294 -0
  118. package/build/plane/schema-compatibility.js +137 -0
  119. package/build/plane/shutdown-deps.js +38 -0
  120. package/build/plane/shutdown.js +83 -8
  121. package/build/plane/terminal-run-cleanup.js +303 -0
  122. package/build/printed-remedy-commands.js +26 -4
  123. package/build/readiness-check.js +6 -2
  124. package/build/readiness-prerequisites.generated.js +532 -0
  125. package/build/readme.generated.js +1 -1
  126. package/build/setup-epic.js +400 -30
  127. package/build/shared-resources.generated.js +5 -0
  128. package/build/start-tickets.js +28 -15
  129. package/build/test-command-detection.js +247 -0
  130. package/build/test-command-validation.js +73 -0
  131. package/build/testing-config-client.js +150 -0
  132. package/build/ticket-delivery-artifacts.js +258 -0
  133. package/build/ticket-delivery-enrollment.js +306 -0
  134. package/build/ticket-delivery.js +205 -0
  135. package/build/ticket-review-summary.js +14 -0
  136. package/build/upgrade-cli.js +13 -0
  137. package/build/v2-readiness-requirements.js +552 -0
  138. package/build/version.generated.js +3 -3
  139. package/build/worker-guard-hook-bin.js +2 -2
  140. package/build/worktree-core.js +22 -7
  141. package/docs/BRIDGE-DIRECTORY-README.md +48 -0
  142. package/docs/EPIC-IMPLEMENTER-INSTALL.md +12 -867
  143. package/docs/EPIC-IMPLEMENTER.md +104 -63
  144. package/docs/epic-implementer/getting-started.md +131 -0
  145. package/docs/epic-implementer/self-hosting.md +133 -0
  146. package/package.json +3 -3
  147. package/pipelines/idea-to-ticket.json +1 -1
  148. package/pipelines/implement-ticket.json +8 -8
  149. package/pipelines/review-ticket.json +2 -2
  150. package/design-assets/favicon/favicon-32x32.png +0 -0
  151. package/design-assets/just-logo-rough-draft.png +0 -0
@@ -1,5 +1,5 @@
1
1
  import { validateBootstrapDisplayName, validateBootstrapServerIdentity } from "./bootstrap-project-identity.js";
2
- import { lookupModernBootstrapRecord, mutateModernBootstrapRecord, prepareModernBootstrapInvite, prepareModernBootstrapSignup, transitionModernBootstrapSignup, lookupLegacyBootstrapInvite, checkpointModernBootstrapIdentity, promoteModernBootstrapCredential } from "./credential-store.js";
2
+ import { BRIDGE_SUPPORT_EMAIL, archiveAndReplaceModernBootstrapSignup, archiveExpiredLegacySignupAndInstallModern, lookupModernBootstrapRecord, mutateModernBootstrapRecord, prepareModernBootstrapInvite, prepareModernBootstrapSignup, transitionModernBootstrapSignup, lookupLegacyBootstrapInvite, checkpointModernBootstrapIdentity, promoteModernBootstrapCredential } from "./credential-store.js";
3
3
  import { collectFirstBuildAdvisory, formatFirstBuildAdvisory } from "./first-build-advisory.js";
4
4
  /**
5
5
  * install-bridge — the one-command Bridge API project bootstrap (BAPI-429).
@@ -145,6 +145,14 @@ import { spawn } from "child_process";
145
145
  import { randomBytes as cryptoRandomBytes, createHash } from "crypto";
146
146
  import os from "os";
147
147
  import path from "path";
148
+ import { detectClaudeLogin } from "./claude-login.js";
149
+ import { checkGithubSignIn } from "./github-sign-in.js";
150
+ import { readLocalGithubRemote } from "./conductor/stored-merge-credential.js";
151
+ import { probeStoredMergeToken } from "./conductor/stored-merge-token-probe.js";
152
+ import { readGithubMergeCredential, removeGithubMergeCredential, upsertGithubMergeCredential, } from "./credential-store.js";
153
+ import { createBridgeApiUrls } from "./bridge-api-urls.js";
154
+ import { installEpicCiWorkflow } from "./epic-ci-workflow-install.js";
155
+ import { fetchParseStatus, triggerRepositoryParse, } from "./conductor/bridge-api-client.js";
148
156
  import readline from "readline";
149
157
  import { runInit, InitPreflightError, buildBridgeApiEntry, buildBridgeConfigManifest, mergeBridgeApiProfileToken, resolveInitScaffoldAssets, refreshBridgeApiPackageSpec, currentBridgePackageSpec, classifyTrackedProbeExitCode, decideExistingProjectRoot, formatUndeterminedRootReplacedWarning, } from "./init.js";
150
158
  import { hasProjectRootMarker as sharedHasProjectRootMarker } from "./project-root.js";
@@ -1467,6 +1475,19 @@ export function createDefaultInstallBridgeDeps() {
1467
1475
  // Read-only service-state probe (BAPI-779). Typed to return a state,
1468
1476
  // never a runner, so the doctor cannot start/enable/reload anything.
1469
1477
  inspectExecutorServiceState: params.inspectExecutorServiceState,
1478
+ // BAPI-1461: only the converging setup passes these; without them the
1479
+ // conductor doctor is built exactly as before.
1480
+ ...(params.skipDenyProbe !== undefined || params.storedMergeCredential !== undefined
1481
+ ? {
1482
+ conductorDoctorDeps: {
1483
+ env: params.env,
1484
+ ...(params.skipDenyProbe === true ? { skipDenyProbe: true } : {}),
1485
+ ...(params.storedMergeCredential !== undefined
1486
+ ? { storedMergeCredential: params.storedMergeCredential }
1487
+ : {}),
1488
+ },
1489
+ }
1490
+ : {}),
1470
1491
  installDoctorDeps: {
1471
1492
  env: params.env,
1472
1493
  cwd: params.cwd,
@@ -1632,6 +1653,54 @@ export function getMergeWorkflowInstallUsage() {
1632
1653
  * `shell: false` — and `gh` additionally receives the non-interactive pins so a
1633
1654
  * prompt can never hang an install. The resulting environment is never printed.
1634
1655
  */
1656
+ /**
1657
+ * BAPI-1461 — the production seams for the converging `install conductor`.
1658
+ *
1659
+ * Each one is a thin adapter over an implementation that already exists: the
1660
+ * Claude login reader, one bounded `gh auth status`, the credential store's
1661
+ * typed merge-token functions (locked, durable writes), the shared capability
1662
+ * probe, the stored-test-command client, the CI workflow artifact-set installer
1663
+ * (sharing the merge-workflow installer's git/gh runners), and the parse routes.
1664
+ */
1665
+ export function buildConductorSetupDeps(deps, log, errorLog) {
1666
+ const installerDeps = buildMergeWorkflowInstallDeps(deps, log, errorLog);
1667
+ const storeDeps = modernWriteDeps(deps);
1668
+ return {
1669
+ detectClaudeLogin: () => detectClaudeLogin({ homedir: deps.homedir, readFile: deps.readFile }),
1670
+ // An injected installer runner (tests) is used for these reads too, so a
1671
+ // harness never reaches the real `gh` or `git`.
1672
+ checkGithubSignIn: () => deps.mergeWorkflowRunGh
1673
+ ? checkGithubSignIn(async (args) => ({ exitCode: (await deps.mergeWorkflowRunGh(args)).status }))
1674
+ : checkGithubSignIn(),
1675
+ readLocalGithubRemote: () => readLocalGithubRemote(deps.cwd, deps.mergeWorkflowRunGit
1676
+ ? async () => {
1677
+ const r = await deps.mergeWorkflowRunGit(["remote", "get-url", "origin"]);
1678
+ return r.status === 0 ? r.stdout : null;
1679
+ }
1680
+ : undefined),
1681
+ ...(deps.isTTY && deps.promptSecret ? { promptSecret: deps.promptSecret } : {}),
1682
+ readMergeToken: (identity) => readGithubMergeCredential(identity, storeDeps),
1683
+ storeMergeToken: (identity, token) => upsertGithubMergeCredential(identity, token, storeDeps),
1684
+ removeMergeToken: (identity) => removeGithubMergeCredential(identity, storeDeps),
1685
+ probeMergeToken: (args) => probeStoredMergeToken({ ...args, env: deps.env }),
1686
+ testingConfig: (access) => ({
1687
+ repoName: access.repoName,
1688
+ urls: createBridgeApiUrls(access.baseUrl),
1689
+ fetch: deps.fetch,
1690
+ getGetHeaders: async () => ({ "X-API-Key": access.apiKey }),
1691
+ getPostHeaders: async () => ({ "X-API-Key": access.apiKey, "Content-Type": "application/json" }),
1692
+ }),
1693
+ readRepositoryFile: (relPath) => deps.readFile(path.join(deps.cwd, relPath)),
1694
+ installCiWorkflow: (testCommand, options, confirmWrite) => installEpicCiWorkflow(testCommand, { dryRun: options.dryRun }, {
1695
+ ...installerDeps,
1696
+ ...(options.quiet === true ? { log: () => { }, errorLog: () => { } } : {}),
1697
+ confirmWrite,
1698
+ }),
1699
+ runGh: installerDeps.runGh,
1700
+ fetchParseStatus: (access) => fetchParseStatus(access, deps.fetch),
1701
+ triggerParse: (access) => triggerRepositoryParse(access, deps.fetch),
1702
+ };
1703
+ }
1635
1704
  export function buildMergeWorkflowInstallDeps(deps, log, errorLog) {
1636
1705
  const toResult = (r) => ({
1637
1706
  status: r.exitCode,
@@ -3865,7 +3934,7 @@ export async function exchangeBootstrapInvite(deps, baseUrl, token, repoName, ke
3865
3934
  return {
3866
3935
  ok: false,
3867
3936
  kind: "failed",
3868
- 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.`,
3937
+ message: `The bootstrap exchange failed (HTTP ${resp.status}). Preserve proof and rerun the same attempt; verify server compatibility and contact Bridge support at ${BRIDGE_SUPPORT_EMAIL} if it persists.`,
3869
3938
  };
3870
3939
  }
3871
3940
  /** The invite-email prompt (BAPI-1263), shown once after an `email_required` refusal. */
@@ -4179,6 +4248,95 @@ export const INSTALL_BRIDGE_SIGNUP_BLOCKED = "Error: Bridge signup is not availa
4179
4248
  export function formatSelfServeUnexpectedStatus(status) {
4180
4249
  return `Error: Bridge signup returned an unexpected response (status ${status}). Try again in a few minutes.`;
4181
4250
  }
4251
+ // ===================================================================
4252
+ // BAPI-1449 — modern-path verification recovery copy (S1–S7).
4253
+ //
4254
+ // The wording is settled; only the sender comes from the shared
4255
+ // INSTALL_BRIDGE_VERIFICATION_SENDER constant. None of these says WHY a server
4256
+ // rejection happened: the word "expired" appears only in S4a, which is chosen from
4257
+ // the locally stored expiry and never from a server reply.
4258
+ // ===================================================================
4259
+ /** The existing end-of-run line for a prompt that gave up or timed out empty. */
4260
+ export const INSTALL_BRIDGE_VERIFICATION_INCOMPLETE = "Verification incomplete. Resume the same signup from this folder.";
4261
+ /** S1 — the first empty Enter at the modern code prompt. */
4262
+ export const INSTALL_BRIDGE_EMPTY_CODE_GUIDANCE = [
4263
+ "No code yet? Type r to send a new one, or check spam for mail from",
4264
+ `${INSTALL_BRIDGE_VERIFICATION_SENDER}.`,
4265
+ "Wrong address? Press Ctrl-C and re-run install with a different email.",
4266
+ "Have a teammate on Bridge? Ask them for an invite and choose",
4267
+ '"I already have a token".',
4268
+ ].join("\n");
4269
+ /** S2 — after a rejected code while resends may remain. */
4270
+ export const INSTALL_BRIDGE_REJECTED_RESEND_HINT = "Type r to send a new code, or enter the code again.";
4271
+ /** S3 — `r` after the server reported no resends left. */
4272
+ export const INSTALL_BRIDGE_NO_MORE_CODES = [
4273
+ "Bridge cannot send more codes for this signup.",
4274
+ "Enter a code from an email you already received.",
4275
+ ].join("\n");
4276
+ /** S4a — the saved challenge is past its locally stored expiry. */
4277
+ export const INSTALL_BRIDGE_RECOVERY_REASON_EXPIRED = "The code for this signup has expired.";
4278
+ /** S4b — the saved signup has no challenge handle (or expiry). */
4279
+ export const INSTALL_BRIDGE_RECOVERY_REASON_NO_CODE = "The last signup in this folder did not finish, so no code is waiting.";
4280
+ /** S4c — this run's email differs from the saved signup's email. */
4281
+ export const INSTALL_BRIDGE_RECOVERY_REASON_DIFFERENT_EMAIL = "This folder has a signup waiting for a different email address.";
4282
+ /** S5 — the rejection cap was reached (state-neutral). */
4283
+ export const INSTALL_BRIDGE_RECOVERY_REASON_CAP = "That signup could not be verified. Nothing has been created.";
4284
+ /** S6 — the user declined a fresh signup. */
4285
+ export const INSTALL_BRIDGE_FRESH_SIGNUP_DECLINED = [
4286
+ "Nothing has been created. Your saved signup is kept.",
4287
+ "Re-run install from this folder when you are ready.",
4288
+ ].join("\n");
4289
+ /** S7 — the user accepted; printed after the old attempt is archived. */
4290
+ export const INSTALL_BRIDGE_FRESH_SIGNUP_STARTING = "Starting a new signup. The old attempt was set aside, not deleted.";
4291
+ /**
4292
+ * The default-No question after S4a/S4b/S4c/S5. Names only the MASKED address of
4293
+ * THIS run's email; the full address is never printed.
4294
+ */
4295
+ export function formatFreshSignupQuestion(thisRunEmail) {
4296
+ return `Start a new signup? We will email a new code to ${maskSignupEmail(thisRunEmail)}. [y/N]: `;
4297
+ }
4298
+ /** The legacy "could not be requested" line for a rejected email (HTTP 400/422). */
4299
+ export const INSTALL_BRIDGE_SELF_SERVE_REQUEST_INVALID = "Error: Self-serve setup could not be requested. Check the email value and try again.";
4300
+ /**
4301
+ * Map a failed challenge REQUEST to its user-facing line, with the legacy loop's
4302
+ * category mapping. Carries no response body, email, or exception text.
4303
+ */
4304
+ export function formatSelfServeChallengeRequestFailure(failure) {
4305
+ switch (failure.category) {
4306
+ case "rate-limited":
4307
+ return `Error: ${INSTALL_BRIDGE_RESEND_THROTTLED}`;
4308
+ case "unavailable":
4309
+ case "network-error":
4310
+ return INSTALL_BRIDGE_SIGNUP_UNAVAILABLE;
4311
+ case "invalid":
4312
+ return INSTALL_BRIDGE_SELF_SERVE_REQUEST_INVALID;
4313
+ case "blocked":
4314
+ return INSTALL_BRIDGE_SIGNUP_BLOCKED;
4315
+ case "unexpected":
4316
+ return formatSelfServeUnexpectedStatus(failure.status ?? 0);
4317
+ default:
4318
+ return INSTALL_BRIDGE_SIGNUP_UPGRADE_REQUIRED;
4319
+ }
4320
+ }
4321
+ /**
4322
+ * Map a failed VERIFY (other than the uniform `rejected`) to its user-facing line,
4323
+ * with the legacy loop's mapping. Carries no response body, code, or handle.
4324
+ */
4325
+ export function formatSelfServeVerifyFailure(failure) {
4326
+ switch (failure.category) {
4327
+ case "rate-limited":
4328
+ return `Error: ${INSTALL_BRIDGE_VERIFY_THROTTLED}`;
4329
+ case "server-error":
4330
+ case "network-error":
4331
+ return INSTALL_BRIDGE_VERIFY_SERVER_ERROR;
4332
+ case "blocked":
4333
+ return INSTALL_BRIDGE_SIGNUP_BLOCKED;
4334
+ case "unexpected":
4335
+ return formatSelfServeUnexpectedStatus(failure.status ?? 0);
4336
+ default:
4337
+ return INSTALL_BRIDGE_SIGNUP_UPGRADE_REQUIRED;
4338
+ }
4339
+ }
4182
4340
  /**
4183
4341
  * Mask an address for display: first character, then the domain.
4184
4342
  *
@@ -4220,23 +4378,10 @@ export function formatChallengeRemaining(expiresAtIso, nowMs) {
4220
4378
  const hours = Math.floor(minutes / 60);
4221
4379
  return `This code is valid for about another ${hours} hours.`;
4222
4380
  }
4223
- /**
4224
- * Prompt for the verification code, validating its shape LOCALLY before any
4225
- * network call.
4226
- *
4227
- * Local validation is a courtesy, not a security control — the server re-validates
4228
- * everything — but it keeps a mistyped code from consuming one of the five attempts
4229
- * that permanently kill a challenge. The user gets the prompt back instead.
4230
- *
4231
- * Pasted input is trimmed: copying a code out of a mail client routinely brings
4232
- * whitespace with it, and refusing that would be an avoidable dead end.
4233
- *
4234
- * The entered value is returned and never echoed, never logged, and never written
4235
- * to disk. `null` means the user gave up (an empty line, or an unreadable prompt).
4236
- */
4237
- export async function promptForVerificationCode(deps, log, maxAttempts = 3) {
4381
+ export async function promptForVerificationCode(deps, log, maxAttempts = 3, options = {}) {
4382
+ const allowResend = options.allowResend ?? false;
4238
4383
  if (!deps.promptLine)
4239
- return null;
4384
+ return { kind: "give-up" };
4240
4385
  for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
4241
4386
  let entered;
4242
4387
  try {
@@ -4245,17 +4390,19 @@ export async function promptForVerificationCode(deps, log, maxAttempts = 3) {
4245
4390
  catch {
4246
4391
  // Secret-free by construction: the caught value is never surfaced or
4247
4392
  // inspected, because a readline failure can carry the partial input.
4248
- return null;
4393
+ return { kind: "give-up" };
4249
4394
  }
4250
4395
  if (entered.length === 0)
4251
- return null;
4396
+ return { kind: "empty" };
4397
+ if (allowResend && entered.toLowerCase() === "r")
4398
+ return { kind: "resend" };
4252
4399
  if (new RegExp(`^[0-9]{${SIGNUP_VERIFICATION_CODE_LENGTH}}$`).test(entered)) {
4253
- return entered;
4400
+ return { kind: "code", code: entered };
4254
4401
  }
4255
4402
  // Describes the SHAPE, never the value the user typed.
4256
4403
  log(` That does not look like a code — it is ${SIGNUP_VERIFICATION_CODE_LENGTH} digits.`);
4257
4404
  }
4258
- return null;
4405
+ return { kind: "give-up" };
4259
4406
  }
4260
4407
  /*
4261
4408
  * `mintSelfServeInvite` and `SelfServeMintResult` were REMOVED in BAPI-757.
@@ -5457,6 +5604,9 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
5457
5604
  // Built from the FINAL merged deps for the same reason (BAPI-775): a test
5458
5605
  // that overrides only `readFile` or only `cwd` must see its override here.
5459
5606
  localProvisioning: buildConductorLocalDeps(deps),
5607
+ // BAPI-1461: the converging setup's seams (sign-ins, the stored merge token,
5608
+ // the stored test command, the CI workflow installer, and indexing).
5609
+ setup: deps.conductorSetup ?? buildConductorSetupDeps(deps, log, errorLog),
5460
5610
  // BAPI-1391: the read-only discovery filesystem over THIS working tree, and
5461
5611
  // the short-lived qualifying executor (the existing executor CLI with a
5462
5612
  // `runExecutor` override limiting it to `gate_qualification` jobs).
@@ -5856,6 +6006,9 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
5856
6006
  return 1;
5857
6007
  }
5858
6008
  let modernAttempt;
6009
+ // BAPI-1449: an expired basename-keyed signup is offered a fresh modern signup by
6010
+ // the modern path (consent first), instead of ending the run here.
6011
+ let legacyExpired;
5859
6012
  if (options.tools?.length === 0 && !options.agentName) {
5860
6013
  log(buildManualInstallBridgeContinuation("empty-selection", []));
5861
6014
  return 0;
@@ -5881,22 +6034,22 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
5881
6034
  }
5882
6035
  if (legacy.state === "resumable" || legacy.state === "challenge")
5883
6036
  options.repo = oldName.value;
5884
- if (legacy.state === "challenge-expired") {
5885
- errorLog("Saved signup expired. Preserve proof and contact installation support.");
5886
- return 1;
5887
- }
6037
+ if (legacy.state === "challenge-expired")
6038
+ legacyExpired = { repoName: oldName.value, snapshot: legacy.snapshot };
5888
6039
  }
5889
6040
  }
5890
6041
  if (options.repo === undefined) {
5891
6042
  try {
5892
- modernAttempt = await redeemModernInstallAttempt(deps, options, baseUrl, inviteToken, signupEmail, selfServeSignupMode);
6043
+ modernAttempt = await redeemModernInstallAttempt(deps, options, baseUrl, inviteToken, signupEmail, selfServeSignupMode, legacyExpired);
5893
6044
  apiKey = modernAttempt.record.keySecret;
5894
6045
  options.repo = modernAttempt.repoName;
5895
6046
  bootstrapInviteMode = false;
5896
6047
  deps.log(`Project '${modernAttempt.displayName}' recovered; completing installation.`);
5897
6048
  }
5898
6049
  catch (error) {
5899
- 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.");
6050
+ if (error instanceof ModernInstallStopped)
6051
+ return 1;
6052
+ errorLog(error instanceof ModernInstallRecoveryError ? error.message : `Installation could not complete. Preserve recovery proof and resume from the same folder and invite/signup; contact Bridge support at ${BRIDGE_SUPPORT_EMAIL} if needed.`);
5900
6053
  return 1;
5901
6054
  }
5902
6055
  }
@@ -6290,7 +6443,7 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
6290
6443
  reportFatal(INSTALL_BRIDGE_SIGNUP_UNAVAILABLE);
6291
6444
  }
6292
6445
  else if (requested.category === "invalid") {
6293
- reportFatal("Error: Self-serve setup could not be requested. Check the email value and try again.");
6446
+ reportFatal(INSTALL_BRIDGE_SELF_SERVE_REQUEST_INVALID);
6294
6447
  }
6295
6448
  else if (requested.category === "blocked") {
6296
6449
  // BAPI-1171: the request was refused before signup logic ran — an
@@ -6346,12 +6499,15 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
6346
6499
  // never succeed, with Ctrl-C as the only way out.
6347
6500
  let rejections = 0;
6348
6501
  for (;;) {
6349
- const code = await promptForVerificationCode(deps, log);
6350
- if (code === null) {
6502
+ // Resend recognition stays off here: on the legacy path `r` is still malformed
6503
+ // input, and an empty line or give-up ends the run as it always has.
6504
+ const prompted = await promptForVerificationCode(deps, log);
6505
+ if (prompted.kind !== "code") {
6351
6506
  reportFatal("Error: No verification code entered — nothing has been set up. Re-run this command " +
6352
6507
  "to continue; the code already emailed to you stays valid until it expires.");
6353
6508
  return false;
6354
6509
  }
6510
+ const code = prompted.code;
6355
6511
  const verified = await verifySelfServeChallenge(deps, baseUrl, challengeHandle, code);
6356
6512
  if (verified.ok) {
6357
6513
  inviteToken = verified.token;
@@ -7259,13 +7415,23 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
7259
7415
  }
7260
7416
  class ModernInstallRecoveryError extends Error {
7261
7417
  }
7418
+ /** The run already printed its own output (for example S6) and stops with exit 1. */
7419
+ class ModernInstallStopped extends Error {
7420
+ }
7421
+ /**
7422
+ * A record that may hold redeemed proof. Presence, not truthiness: an empty-string
7423
+ * field still counts. Such a record is never offered a fresh signup.
7424
+ */
7425
+ function isProtectedModernRecord(record) {
7426
+ return ["fingerprint", "replayToken", "canonical"].some(field => Object.prototype.hasOwnProperty.call(record, field));
7427
+ }
7262
7428
  function modernWriteDeps(deps) {
7263
7429
  return { env: deps.env, homedir: deps.homedir, platform: deps.platform, readFile: deps.readFile,
7264
7430
  mkdir: deps.mkdir, writeFile: deps.writeFile, rename: deps.rename, chmod: deps.chmod, unlink: deps.unlink, open: deps.open };
7265
7431
  }
7266
- async function redeemModernInstallAttempt(deps, options, baseUrl, token, signupEmail, selfServe) {
7432
+ async function redeemModernInstallAttempt(deps, options, baseUrl, token, signupEmail, selfServe, legacyExpired) {
7267
7433
  if (!deps.realpath)
7268
- throw new ModernInstallRecoveryError("Canonical folder resolution is unavailable. Contact installation support.");
7434
+ throw new ModernInstallRecoveryError(`Canonical folder resolution is unavailable. Contact Bridge support at ${BRIDGE_SUPPORT_EMAIL}.`);
7269
7435
  const folder = await deps.realpath(deps.cwd);
7270
7436
  const folderDigest = fingerprintBootstrapFields("bridge-bootstrap-folder-v1", [baseUrl, folder]);
7271
7437
  const signupLocator = `bootstrap-pending:signup:${folderDigest}`;
@@ -7277,9 +7443,12 @@ async function redeemModernInstallAttempt(deps, options, baseUrl, token, signupE
7277
7443
  if (!loaded.ok)
7278
7444
  throw new Error(loaded.error);
7279
7445
  let record = loaded.record;
7280
- const resumed = record !== null;
7446
+ let resumed = record !== null;
7281
7447
  let label = record?.label ?? options.displayName;
7282
- if (label === undefined) {
7448
+ // BAPI-1449: an expired legacy basename signup asks for consent BEFORE any label
7449
+ // question, so its label is derived only after the user accepts.
7450
+ const deferLabelForLegacy = selfServe && record === null && legacyExpired !== undefined;
7451
+ const deriveLabel = async () => {
7283
7452
  if (!deps.isTTY || !deps.promptLine)
7284
7453
  throw new ModernInstallRecoveryError("Modern invite creation requires --display-name in a non-interactive terminal.");
7285
7454
  // BAPI-1448: a valid folder name is the label, with no question. The prompt
@@ -7289,24 +7458,35 @@ async function redeemModernInstallAttempt(deps, options, baseUrl, token, signupE
7289
7458
  suggestion = validateBootstrapDisplayName(path.basename(folder));
7290
7459
  }
7291
7460
  catch { /* explicit label required */ }
7292
- if (suggestion) {
7293
- label = suggestion;
7294
- }
7295
- else {
7296
- label = validateBootstrapDisplayName(await deps.promptLine("Project display name: "));
7297
- }
7298
- }
7461
+ if (suggestion)
7462
+ return suggestion;
7463
+ return validateBootstrapDisplayName(await deps.promptLine("Project display name: "));
7464
+ };
7465
+ if (label === undefined && !deferLabelForLegacy)
7466
+ label = await deriveLabel();
7299
7467
  if (selfServe) {
7300
7468
  if (!deps.isTTY || !deps.promptLine)
7301
7469
  throw new ModernInstallRecoveryError("Self-serve email verification requires an interactive terminal.");
7302
- if (!record) {
7303
- const intent = await prepareModernBootstrapSignup(scope, label, signupEmail, () => generateBootstrapKeySecret(deps.randomBytes), writeDeps);
7304
- if (!intent.ok || !intent.record)
7305
- throw new Error(intent.ok ? "Signup proof missing." : intent.error);
7306
- record = intent.record;
7470
+ // At most one fresh signup per invocation, whichever route accepted it.
7471
+ let freshSignupAccepted = false;
7472
+ const generateKeySecret = () => generateBootstrapKeySecret(deps.randomBytes);
7473
+ const generateArchiveSuffix = () => deps.randomBytes(16).toString("hex");
7474
+ // Print the reason, then ask the default-No question. A decline (or EOF, an empty
7475
+ // line, a thrown prompt) prints S6 and stops with the credential store untouched.
7476
+ const askFreshSignup = async (reason) => {
7477
+ deps.log(reason);
7478
+ if (!(await requestCredentialRouteSwitchConfirmation(deps, formatFreshSignupQuestion(signupEmail)))) {
7479
+ deps.log(INSTALL_BRIDGE_FRESH_SIGNUP_DECLINED);
7480
+ throw new ModernInstallStopped();
7481
+ }
7482
+ };
7483
+ // Request a challenge for THIS run's email, then persist its handle and expiry
7484
+ // BEFORE the prompt. The request happens outside the credential-store lock. On
7485
+ // any failure the saved record stays and is never replaced again in this run.
7486
+ const requestAndSaveChallenge = async () => {
7307
7487
  const requested = await requestSelfServeChallenge(deps, baseUrl, signupEmail);
7308
7488
  if (!requested.ok)
7309
- throw new ModernInstallRecoveryError("Signup handle could not be established. Preserve recovery proof; contact installation support before restarting signup.");
7489
+ throw new ModernInstallRecoveryError(formatSelfServeChallengeRequestFailure(requested));
7310
7490
  const saved = await mutateModernBootstrapRecord(scope, writeDeps, current => {
7311
7491
  if (!current)
7312
7492
  throw new ModernInstallRecoveryError("Signup proof missing.");
@@ -7315,25 +7495,161 @@ async function redeemModernInstallAttempt(deps, options, baseUrl, token, signupE
7315
7495
  if (!saved.ok || !saved.record)
7316
7496
  throw new Error(saved.ok ? "Signup proof missing." : saved.error);
7317
7497
  record = saved.record;
7498
+ };
7499
+ // Archive the observed record, install a fresh signup for this run's email, then
7500
+ // (only after that is durable) print S7 and request the new challenge.
7501
+ const replaceWithFreshSignup = async (observed) => {
7502
+ const replaced = await archiveAndReplaceModernBootstrapSignup({ scope, observed, email: signupEmail, generateKeySecret, generateArchiveSuffix }, writeDeps);
7503
+ if (!replaced.ok)
7504
+ throw new ModernInstallRecoveryError(replaced.error);
7505
+ freshSignupAccepted = true;
7506
+ resumed = false;
7507
+ record = replaced.record;
7508
+ deps.log(INSTALL_BRIDGE_FRESH_SIGNUP_STARTING);
7509
+ await requestAndSaveChallenge();
7510
+ };
7511
+ if (!record) {
7512
+ if (legacyExpired) {
7513
+ // The basename-keyed legacy signup is the "no modern record" case: ask, derive
7514
+ // the label from the folder basename, then archive it and install the modern
7515
+ // signup in one write.
7516
+ await askFreshSignup(INSTALL_BRIDGE_RECOVERY_REASON_EXPIRED);
7517
+ label = options.displayName ?? await deriveLabel();
7518
+ const replaced = await archiveExpiredLegacySignupAndInstallModern({ repoName: legacyExpired.repoName, observed: legacyExpired.snapshot, now: deps.now(), scope, label, email: signupEmail, generateKeySecret, generateArchiveSuffix }, writeDeps);
7519
+ if (!replaced.ok)
7520
+ throw new ModernInstallRecoveryError(replaced.error);
7521
+ freshSignupAccepted = true;
7522
+ record = replaced.record;
7523
+ deps.log(INSTALL_BRIDGE_FRESH_SIGNUP_STARTING);
7524
+ await requestAndSaveChallenge();
7525
+ }
7526
+ else {
7527
+ const intent = await prepareModernBootstrapSignup(scope, label, signupEmail, generateKeySecret, writeDeps);
7528
+ if (!intent.ok || !intent.record)
7529
+ throw new Error(intent.ok ? "Signup proof missing." : intent.error);
7530
+ record = intent.record;
7531
+ await requestAndSaveChallenge();
7532
+ }
7318
7533
  }
7534
+ else if (!isProtectedModernRecord(record)) {
7535
+ // A saved, unresolved signup. Reasons are checked in this order, and the
7536
+ // expiry is read from the stored value only, never from a server reply.
7537
+ // A malformed stored expiry is treated as unexpired.
7538
+ let reason;
7539
+ if (typeof record.signupEmail === "string" && record.signupEmail.trim().toLowerCase() !== signupEmail.trim().toLowerCase()) {
7540
+ reason = INSTALL_BRIDGE_RECOVERY_REASON_DIFFERENT_EMAIL;
7541
+ }
7542
+ else if (!record.challengeHandle || !record.challengeExpiresAt) {
7543
+ reason = INSTALL_BRIDGE_RECOVERY_REASON_NO_CODE;
7544
+ }
7545
+ else if (Date.parse(record.challengeExpiresAt) <= deps.now()) {
7546
+ reason = INSTALL_BRIDGE_RECOVERY_REASON_EXPIRED;
7547
+ }
7548
+ if (reason !== undefined) {
7549
+ await askFreshSignup(reason);
7550
+ await replaceWithFreshSignup(record);
7551
+ }
7552
+ }
7553
+ if (!record)
7554
+ throw new ModernInstallRecoveryError("Recovery proof missing.");
7319
7555
  if (record.replayToken && record.fingerprint) {
7320
7556
  token = record.replayToken;
7321
7557
  fingerprint = record.fingerprint;
7322
7558
  scope = makeScope(`bootstrap-pending:invite:${fingerprint}`);
7323
7559
  }
7324
7560
  else {
7561
+ // Only a protected record can reach these two guards unresolved: an unprotected
7562
+ // one was either offered a fresh signup above or already holds a live challenge.
7325
7563
  if (!record.challengeHandle || !record.challengeExpiresAt)
7326
- throw new ModernInstallRecoveryError("The previous signup response is unresolved. Preserve proof and contact installation support before restarting.");
7327
- if (Date.parse(record.challengeExpiresAt) <= deps.now())
7328
- throw new ModernInstallRecoveryError("The saved verification challenge expired. Preserve proof and contact installation support.");
7329
- logVerificationCodeLeadIn(deps.log, record.signupEmail ?? signupEmail);
7330
- const code = await promptForVerificationCode(deps, deps.log);
7331
- if (!code)
7332
- throw new ModernInstallRecoveryError("Verification incomplete. Resume the same signup from this folder.");
7333
- const verified = await verifySelfServeChallenge(deps, baseUrl, record.challengeHandle, code);
7334
- if (!verified.ok)
7335
- throw new ModernInstallRecoveryError("Verification did not complete. Preserve proof and resume the same signup.");
7336
- token = verified.token;
7564
+ throw new ModernInstallRecoveryError(`The previous signup response is unresolved. Preserve proof and contact Bridge support at ${BRIDGE_SUPPORT_EMAIL} before restarting.`);
7565
+ if (isProtectedModernRecord(record) && Date.parse(record.challengeExpiresAt) <= deps.now())
7566
+ throw new ModernInstallRecoveryError(`The saved verification challenge expired. Preserve proof and contact Bridge support at ${BRIDGE_SUPPORT_EMAIL}.`);
7567
+ // One pass per challenge. Everything challenge-local resets only when a new
7568
+ // challenge begins; the loop leaves it on a verified code or by throwing.
7569
+ let verifiedToken;
7570
+ for (;;) {
7571
+ const handle = record.challengeHandle;
7572
+ logVerificationCodeLeadIn(deps.log, record.signupEmail ?? signupEmail);
7573
+ let rejections = 0;
7574
+ let consecutiveEmpty = 0;
7575
+ let emptyGuidanceShown = false;
7576
+ // Unknown until a resend in THIS session reports it, including on a resumed
7577
+ // challenge: the first `r` then makes a real request.
7578
+ let resendsRemaining;
7579
+ let capReached = false;
7580
+ // Count a rejection; true when the shared cap was just reached.
7581
+ const countRejection = () => {
7582
+ deps.errorLog(INSTALL_BRIDGE_VERIFICATION_REJECTED);
7583
+ rejections += 1;
7584
+ capReached = rejections >= INSTALL_BRIDGE_MAX_CODE_REJECTIONS;
7585
+ return capReached;
7586
+ };
7587
+ while (!capReached && verifiedToken === undefined) {
7588
+ const prompted = await promptForVerificationCode(deps, deps.log, 3, { allowResend: true });
7589
+ if (prompted.kind === "give-up")
7590
+ throw new ModernInstallRecoveryError(INSTALL_BRIDGE_VERIFICATION_INCOMPLETE);
7591
+ if (prompted.kind === "empty") {
7592
+ consecutiveEmpty += 1;
7593
+ if (consecutiveEmpty >= 2)
7594
+ throw new ModernInstallRecoveryError(INSTALL_BRIDGE_VERIFICATION_INCOMPLETE);
7595
+ if (!emptyGuidanceShown) {
7596
+ deps.log(INSTALL_BRIDGE_EMPTY_CODE_GUIDANCE);
7597
+ emptyGuidanceShown = true;
7598
+ }
7599
+ continue;
7600
+ }
7601
+ consecutiveEmpty = 0;
7602
+ if (prompted.kind === "resend") {
7603
+ if (resendsRemaining === 0) {
7604
+ deps.log(INSTALL_BRIDGE_NO_MORE_CODES);
7605
+ continue;
7606
+ }
7607
+ // The server enforces the cooldown and the resend budget; there is no
7608
+ // client timer or cap. The stored expiry is never touched.
7609
+ const resent = await resendSelfServeChallenge(deps, baseUrl, handle);
7610
+ if (resent.ok) {
7611
+ deps.log(` ${INSTALL_BRIDGE_RESEND_SENT}`);
7612
+ resendsRemaining = resent.resendsRemaining;
7613
+ }
7614
+ else if (resent.category === "cooldown") {
7615
+ deps.log(` ${INSTALL_BRIDGE_RESEND_COOLDOWN}`);
7616
+ }
7617
+ else if (resent.category === "rejected") {
7618
+ // The uniform 401 also covers "no resends left", so the challenge may
7619
+ // still be alive: count it, but do not end the run on its own.
7620
+ countRejection();
7621
+ }
7622
+ else {
7623
+ deps.log(` ${INSTALL_BRIDGE_RESEND_THROTTLED}`);
7624
+ }
7625
+ continue;
7626
+ }
7627
+ const verified = await verifySelfServeChallenge(deps, baseUrl, handle, prompted.code);
7628
+ if (verified.ok) {
7629
+ // BAPI-1173: only an EXPLICIT false prints.
7630
+ if (verified.trialGranted === false)
7631
+ deps.log(` ${INSTALL_BRIDGE_TRIAL_ALREADY_USED}`);
7632
+ verifiedToken = verified.token;
7633
+ break;
7634
+ }
7635
+ if (verified.category === "rejected") {
7636
+ if (!countRejection() && resendsRemaining !== 0)
7637
+ deps.log(INSTALL_BRIDGE_REJECTED_RESEND_HINT);
7638
+ continue;
7639
+ }
7640
+ // Every other category ends the run and keeps the saved record.
7641
+ throw new ModernInstallRecoveryError(formatSelfServeVerifyFailure(verified));
7642
+ }
7643
+ if (verifiedToken !== undefined)
7644
+ break;
7645
+ // Rejection cap: offer one fresh signup (S5), unless one was already taken.
7646
+ if (freshSignupAccepted) {
7647
+ throw new ModernInstallRecoveryError(`${INSTALL_BRIDGE_VERIFICATION_GIVING_UP}\n\nRun it again from this same folder.`);
7648
+ }
7649
+ await askFreshSignup(INSTALL_BRIDGE_RECOVERY_REASON_CAP);
7650
+ await replaceWithFreshSignup(record);
7651
+ }
7652
+ token = verifiedToken;
7337
7653
  fingerprint = fingerprintBootstrapInvite(token, baseUrl);
7338
7654
  const inviteScope = makeScope(`bootstrap-pending:invite:${fingerprint}`);
7339
7655
  const transitioned = await transitionModernBootstrapSignup(makeScope(signupLocator), inviteScope, fingerprint, token, record.challengeHandle, writeDeps);
@@ -7369,7 +7685,7 @@ async function redeemModernInstallAttempt(deps, options, baseUrl, token, signupE
7369
7685
  if (!exchange.ok && exchange.message.includes("Creation outcome is unknown"))
7370
7686
  throw new ModernInstallRecoveryError("Creation outcome is unknown. Preserve recovery proof and rerun the same attempt from the original folder and invite/signup.");
7371
7687
  if (!exchange.ok || !exchange.displayName)
7372
- 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.");
7688
+ 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 Bridge support at ${BRIDGE_SUPPORT_EMAIL}.`);
7373
7689
  const checkpoint = await checkpointModernBootstrapIdentity(scope, record, exchange.repoName, exchange.displayName, writeDeps);
7374
7690
  if (!checkpoint.ok || !checkpoint.record)
7375
7691
  throw new Error(checkpoint.ok ? "Checkpoint missing." : checkpoint.error);
@@ -17,6 +17,7 @@
17
17
  * unit-testable with in-memory fakes.
18
18
  */
19
19
  import { validPackagedAssetInventory } from "./generated-assets-context.js";
20
+ import { currentDestinationForLegacyDoc } from "./generated-assets-legacy.js";
20
21
  import { executorIdFromLaunchdFilename, executorIdFromSystemdFilename, } from "./executor/service-unit.js";
21
22
  import { HOST_PLATFORM_ORDER, isHostPlatformId, } from "./mcp-host-targets.js";
22
23
  /** Current on-disk schema version. */
@@ -503,12 +504,17 @@ export async function recordPackagedAssetInventory(cwd, inventory, deps) {
503
504
  return { ok: false, error: "Existing installation state cannot be safely preserved." };
504
505
  const state = prior.status === "valid" ? prior.state : undefined;
505
506
  const records = new Map();
507
+ // BAPI-1470: a record whose id or destination is one of the six moved procedure
508
+ // paths is dropped from both sides, so future inventories name only current ones.
509
+ const isMovedRecord = (a) => currentDestinationForLegacyDoc(a.id) !== undefined || currentDestinationForLegacyDoc(a.destination) !== undefined;
506
510
  if (state?.packagedAssets?.rootBinding === inventory.rootBinding) {
507
511
  for (const a of state.packagedAssets.assets)
508
- records.set(`${a.host}:${a.id}`, a);
512
+ if (!isMovedRecord(a))
513
+ records.set(`${a.host}:${a.id}`, a);
509
514
  }
510
515
  for (const a of inventory.assets)
511
- records.set(`${a.host}:${a.id}`, a);
516
+ if (!isMovedRecord(a))
517
+ records.set(`${a.host}:${a.id}`, a);
512
518
  return writeMcpInstallState(cwd, { selectedPlatforms: state?.selectedPlatforms ?? [],
513
519
  projectConfigPaths: state?.projectConfigPaths ?? [], packagedAssets: { ...inventory, assets: [...records.values()] } }, deps);
514
520
  }
@@ -689,6 +689,12 @@ const LOCAL_GH_HINTS = {
689
689
  machine_merge_credential_invalid: `The configured ${LOCAL_MERGE_CREDENTIAL_ENV_KEYS.machine_token} was rejected by GitHub. Rotate it and relaunch this MCP server.`,
690
690
  local_merge_credential_unpermitted: `The configured ${LOCAL_MERGE_CREDENTIAL_ENV_KEYS.local} authenticates but cannot write to this repository. Grant it merge permission.`,
691
691
  machine_merge_credential_unpermitted: `The configured ${LOCAL_MERGE_CREDENTIAL_ENV_KEYS.machine_token} authenticates but cannot write to this repository. Grant the machine user merge permission.`,
692
+ // BAPI-1452: GitHub gave no usable answer about the credential's access. Not
693
+ // proof of a missing permission, but a merge still refuses without evidence.
694
+ local_merge_probe_unavailable: `Could not confirm that ${LOCAL_MERGE_CREDENTIAL_ENV_KEYS.local} can push to this repository and read its pull requests ` +
695
+ "(GitHub returned no usable answer). Retry shortly; if it persists, check the token's repository access and GitHub's status.",
696
+ machine_merge_probe_unavailable: `Could not confirm that ${LOCAL_MERGE_CREDENTIAL_ENV_KEYS.machine_token} can push to this repository and read its pull requests ` +
697
+ "(GitHub returned no usable answer). Retry shortly; if it persists, check the machine user's repository access and GitHub's status.",
692
698
  };
693
699
  /**
694
700
  * Read the repository's LEGACY merge execution mode.