@bridge_gpt/mcp-server 0.2.66 → 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 (171) hide show
  1. package/README.md +421 -83
  2. package/build/agent-capabilities/probes.js +15 -5
  3. package/build/agent-launchers/claude-executor-adapter.js +70 -16
  4. package/build/agent-launchers/executor-adapter.js +7 -4
  5. package/build/agents.generated.js +22 -2
  6. package/build/bitbucket-workspace-remote.js +136 -0
  7. package/build/bootstrap-project-identity.js +24 -0
  8. package/build/bounded-wait.js +67 -10
  9. package/build/bridge-permissions.js +829 -0
  10. package/build/claude-settings.js +18 -0
  11. package/build/cli-help.js +21 -0
  12. package/build/command-assets-doctor.js +24 -70
  13. package/build/command-provisioning.js +29 -5
  14. package/build/commands.generated.js +12 -12
  15. package/build/conductor/bridge-api-client.js +857 -1
  16. package/build/conductor/bring-up-facts.js +1 -1
  17. package/build/conductor/command-drift-production.js +92 -0
  18. package/build/conductor/command-drift.js +89 -0
  19. package/build/conductor/doctor.js +72 -29
  20. package/build/conductor/epic-branch-ci-inspection.js +1375 -0
  21. package/build/conductor/epic-implementer-cli.js +98 -28
  22. package/build/conductor/errors.js +4 -3
  23. package/build/conductor/gate-discovery.js +890 -0
  24. package/build/conductor/guided-repair.js +6 -2
  25. package/build/conductor/hook-runtime-qualification.js +1 -1
  26. package/build/conductor/install-doctor.js +15 -3
  27. package/build/conductor/json-output.js +63 -0
  28. package/build/conductor/merge-execution-contract.js +27 -8
  29. package/build/conductor/merge-execution-eligibility.js +3 -2
  30. package/build/conductor/pr-ci-producer.js +1 -1
  31. package/build/conductor/readiness-cli.js +113 -12
  32. package/build/conductor/readiness-sections.js +410 -9
  33. package/build/conductor/readiness.js +388 -33
  34. package/build/conductor/recovery-cli.js +310 -12
  35. package/build/conductor/recovery-operations.js +167 -3
  36. package/build/conductor/repair-contract.js +54 -3
  37. package/build/conductor/repository-workflow-remedy.js +125 -0
  38. package/build/conductor/tools.js +2 -2
  39. package/build/conductor-bundle-artifacts.js +8 -0
  40. package/build/connect-bitbucket-api.js +41 -6
  41. package/build/connect-bitbucket.js +106 -33
  42. package/build/credential-store.js +207 -47
  43. package/build/docs.generated.js +62 -3
  44. package/build/doctor.js +92 -1
  45. package/build/drive-epic-key.js +6 -5
  46. package/build/drive-epic.js +129 -61
  47. package/build/epic-coherence-input.js +2 -1
  48. package/build/epic-coherence-input.test-fixtures.js +4 -3
  49. package/build/epic-implementer-bin.js +1379 -28
  50. package/build/epic-implementer-bundle-cli.js +2 -2
  51. package/build/epic-workarounds-path.js +39 -0
  52. package/build/executor/cli.js +33 -2
  53. package/build/executor/completion-contract.js +66 -10
  54. package/build/executor/completion-supervision.js +4 -0
  55. package/build/executor/deps.js +4 -1
  56. package/build/executor/env.js +19 -0
  57. package/build/executor/gate-qualification-job.js +499 -0
  58. package/build/executor/guarded-fast-forward-push.js +129 -0
  59. package/build/executor/heartbeat.js +1 -1
  60. package/build/executor/http-client.js +20 -0
  61. package/build/executor/integration-job.js +122 -8
  62. package/build/executor/job-errors.js +33 -1
  63. package/build/executor/job-runner.js +813 -87
  64. package/build/executor/likely-tests.js +290 -0
  65. package/build/executor/live-worker-registry.js +1 -1
  66. package/build/executor/observation.js +37 -3
  67. package/build/executor/packaged-recipe.js +106 -0
  68. package/build/executor/permissions.js +115 -28
  69. package/build/executor/platform.js +1 -1
  70. package/build/executor/preflight.js +112 -26
  71. package/build/executor/process-containment.js +36 -12
  72. package/build/executor/process.js +15 -7
  73. package/build/executor/prompt-spec.js +33 -1
  74. package/build/executor/recently-failed-tests-file.js +71 -0
  75. package/build/executor/reconcile-overlap-job.js +41 -23
  76. package/build/executor/required-verification.js +1 -1
  77. package/build/executor/required-work-ledger.js +111 -18
  78. package/build/executor/resume-pre-spawn.js +2 -0
  79. package/build/executor/retirement.js +69 -0
  80. package/build/executor/runner.js +122 -13
  81. package/build/executor/service-unit.js +7 -1
  82. package/build/executor/test-clock.js +15 -0
  83. package/build/executor/types.js +32 -0
  84. package/build/executor/viewer-tabs.js +58 -8
  85. package/build/executor/worker-command.js +23 -7
  86. package/build/executor/worker-diagnostics.js +37 -0
  87. package/build/executor/worker-finalization.js +113 -34
  88. package/build/executor/worker-guard-hook.js +316 -58
  89. package/build/executor/worker-log.js +4 -3
  90. package/build/executor/worker-verification-guidance.js +37 -0
  91. package/build/executor/worktree.js +166 -8
  92. package/build/first-build-advisory.js +97 -0
  93. package/build/generated-assets-advisory.js +14 -0
  94. package/build/generated-assets-catalog.js +66 -0
  95. package/build/generated-assets-context.js +46 -0
  96. package/build/generated-assets-dependencies.js +26 -0
  97. package/build/generated-assets-doctor.js +50 -0
  98. package/build/generated-assets-health.js +65 -0
  99. package/build/generated-assets-host.js +17 -0
  100. package/build/generated-assets-inventory.js +17 -0
  101. package/build/generated-assets-paths.js +60 -0
  102. package/build/generated-assets-publisher.js +133 -0
  103. package/build/generated-assets-remedy.js +25 -0
  104. package/build/generated-assets-repair.js +72 -0
  105. package/build/generated-assets-session.js +71 -0
  106. package/build/generated-assets-test-support.js +35 -0
  107. package/build/index.js +1224 -1054
  108. package/build/init.js +54 -177
  109. package/build/install-bridge-conductor-gate.js +1372 -0
  110. package/build/install-bridge-conductor.js +296 -41
  111. package/build/install-bridge.js +547 -40
  112. package/build/mcp-install-state.js +50 -4
  113. package/build/merge-workflow-install.js +3 -4
  114. package/build/phase-context-git-probe.js +68 -0
  115. package/build/phase-result-artifacts.js +173 -5
  116. package/build/pipeline-utils.js +6 -2
  117. package/build/pipelines.generated.js +34 -160
  118. package/build/plan-execution-ledger.js +2 -32
  119. package/build/plane/alembic-head.js +12 -3
  120. package/build/plane/base-worktree-guard.js +147 -0
  121. package/build/plane/cli.js +217 -17
  122. package/build/plane/code-snapshot.js +468 -0
  123. package/build/plane/defaults.js +48 -3
  124. package/build/plane/endpoint.js +41 -0
  125. package/build/plane/manifest.js +136 -2
  126. package/build/plane/member-roster.js +94 -19
  127. package/build/plane/preflight.js +83 -2
  128. package/build/plane/process-cwd.js +67 -0
  129. package/build/plane/restart.js +542 -0
  130. package/build/plane/shutdown.js +31 -0
  131. package/build/plane/status.js +143 -8
  132. package/build/plane/supervisor.js +586 -179
  133. package/build/plane/types.js +54 -2
  134. package/build/printed-remedy-commands.js +52 -0
  135. package/build/readiness-check.js +1 -1
  136. package/build/readme.generated.js +1 -1
  137. package/build/repair-assets.js +93 -0
  138. package/build/review-generation.js +22 -0
  139. package/build/run-unit-tests-launcher.js +130 -49
  140. package/build/secret-redaction.js +33 -0
  141. package/build/setup-epic.js +152 -22
  142. package/build/sfcc/config.js +9 -0
  143. package/build/sfcc/credentials.js +100 -38
  144. package/build/sfcc/hostname.js +67 -0
  145. package/build/sfcc/permissions.js +17 -13
  146. package/build/sfcc/register.js +1 -1
  147. package/build/sfcc/tool-wrapper.js +39 -43
  148. package/build/sfcc/write-grants.js +17 -11
  149. package/build/sfcc/write-result.js +4 -3
  150. package/build/sfcc/writes-site-preference.js +20 -8
  151. package/build/start-tickets-prereqs.js +70 -16
  152. package/build/start-tickets.js +208 -36
  153. package/build/tool-error-envelope.js +61 -0
  154. package/build/tool-surface-gating.js +36 -0
  155. package/build/upgrade-cli.js +55 -1
  156. package/build/version.generated.js +3 -3
  157. package/build/visual-diff.js +22 -1
  158. package/build/worker-guard-hook-bin.js +4 -6
  159. package/build/worker-guard-hook-main.js +47 -0
  160. package/build/worktree-core.js +145 -6
  161. package/docs/EPIC-IMPLEMENTER-INSTALL.md +867 -0
  162. package/docs/EPIC-IMPLEMENTER.md +728 -0
  163. package/docs/install/mcp-tool-integrations.md +24 -15
  164. package/docs/install/sfcc-integration.md +12 -4
  165. package/package.json +3 -3
  166. package/pipelines/greenfield-setup.json +4 -128
  167. package/pipelines/implement-ticket.json +3 -2
  168. package/pipelines/plan-epic.json +0 -5
  169. package/pipelines/review-ticket.json +1 -1
  170. package/docs/CONDUCTOR.md +0 -605
  171. package/pipelines/idea-to-pr.json +0 -49
@@ -1,3 +1,4 @@
1
+ import { recordProvisionedAssets } from "./generated-assets-inventory.js";
1
2
  /**
2
3
  * start-tickets — packaged CLI subcommand of the bridge-api-mcp-server bin.
3
4
  *
@@ -136,7 +137,7 @@ export function getStartTicketsUsage() {
136
137
  " --host claude-code|cursor|codex|copilot-vscode|windsurf Current host when --agent is omitted; Claude Code, Cursor, and Codex can launch sessions",
137
138
  " Cursor with --auto launches headlessly after a bounded readiness check; without --auto it stays interactive for the review gate.",
138
139
  " --workflow implement|review-and-implement Command each spawned worktree runs (default: implement). review-and-implement runs review-ticket then, after a per-ticket halt gate, hands off to a fresh implement-ticket session; --auto applies to both phases.",
139
- " --tier cheap|basic|premium Strict manual model-routing override: bypasses the per-ticket difficulty/tier lookup and applies this tier to every ticket exactly as supplied, with no per-session posture adjustment. Claude/Cursor use registry aliases and repository overrides; Codex uses the authenticated Bridge catalog. It is NOT a raw --model alias and never carries an API key or credential. A malformed value requests premium routing when available. Mutually exclusive with --auto-tier.",
140
+ " --tier cheap|basic|premium Strict manual model-routing override: bypasses the per-ticket difficulty/tier lookup and applies this tier to every ticket exactly as supplied, with no per-session posture adjustment. Claude/Cursor use registry aliases and repository overrides; Codex uses the authenticated Bridge catalog. It is NOT a raw --model alias and never carries an API key or credential. A malformed value requests premium routing when available. With --workflow review-and-implement a valid value is also forwarded to the spawned command so the review tab and the fresh implementation session both run on it. Mutually exclusive with --auto-tier.",
140
141
  " --auto-tier cheap|basic|premium Internal/advanced machine-carried raw backend tier for chained automation handoffs (e.g. /review-and-implement): bypasses the per-ticket lookup like --tier, but remains subject to per-session optimization-posture adjustment (may be downshifted under maximize_subscription, unless the server reports configured thresholds). Not the strict-override mechanism — use --tier for a human override. Mutually exclusive with --tier.",
141
142
  " --rounds 1|2 Review round count forwarded to the review phase; review-only, valid only with --workflow review-and-implement",
142
143
  " --terminal terminal|iterm Override the macOS terminal app (default: auto-detect via $TERM_PROGRAM); honored on macOS only",
@@ -619,7 +620,9 @@ resumeMode = false, workflow = "implement", reviewRounds,
619
620
  // conductor runs always, and (BAPI-801/BAPI-847) for every plain run whose base
620
621
  // resolved, `main` included — and threaded (BAPI-593) into the spawned workflow
621
622
  // command.
622
- baseBranch, bridgeApiBaseUrlOverride) {
623
+ baseBranch, bridgeApiBaseUrlOverride,
624
+ // BAPI-1365: a recognized operator `--tier`, forwarded to the review prompt.
625
+ manualTier) {
623
626
  if (!isSupportedStartTicketsPlatform(deps.platform)) {
624
627
  return { ok: false, error: unsupportedPlatformMessage(deps.platform) };
625
628
  }
@@ -640,7 +643,7 @@ baseBranch, bridgeApiBaseUrlOverride) {
640
643
  // falls back to the basename-derived repo name (the 403 root cause), and
641
644
  // (BAPI-586/BAPI-801) the run base so the spawned worker opens its PR
642
645
  // against it. The base assignment stays outermost.
643
- buildAgentShellCommand: (key, worktreePath, modelAlias, launchArgs) => prependBaseBranchEnvAssignment(prependRepoNameEnvAssignment(buildAgentShellCommand(agent, key, worktreePath, platform, autoApprove, modelAlias, conductorEnabled, resumeMode, workflow, reviewRounds, baseBranch, launchArgs, bridgeApiBaseUrl), repoName, platform), prBaseBranch, platform),
646
+ buildAgentShellCommand: (key, worktreePath, modelAlias, launchArgs) => prependBaseBranchEnvAssignment(prependRepoNameEnvAssignment(buildAgentShellCommand(agent, key, worktreePath, platform, autoApprove, modelAlias, conductorEnabled, resumeMode, workflow, reviewRounds, baseBranch, launchArgs, bridgeApiBaseUrl, manualTier), repoName, platform), prBaseBranch, platform),
644
647
  spawnTerminalTab: deps.spawnTerminalTab,
645
648
  },
646
649
  };
@@ -834,6 +837,7 @@ export function createDefaultStartTicketsDeps() {
834
837
  encoding: "utf-8",
835
838
  // Optional bounded timeout (e.g. cursor-agent --list-models probes).
836
839
  timeout: options?.timeoutMs,
840
+ signal: options?.signal,
837
841
  }, (error, stdout, stderr) => {
838
842
  const exitCode = error && typeof error.code === "number"
839
843
  ? (error.code)
@@ -844,6 +848,7 @@ export function createDefaultStartTicketsDeps() {
844
848
  stdout: stdout ?? "",
845
849
  stderr: stderr ?? "",
846
850
  exitCode,
851
+ ...(error && typeof error.code === "string" ? { unavailable: true } : {}),
847
852
  });
848
853
  });
849
854
  });
@@ -1072,6 +1077,19 @@ export async function refreshBaseBranch(deps, options) {
1072
1077
  // `<baseBranch>` is checked out somewhere: fast-forward it in place. git refuses to
1073
1078
  // force-move a checked-out branch ref, so we must go through merge.
1074
1079
  if (basePath) {
1080
+ // BAPI-1362: a live plane member may be running out of this very checkout.
1081
+ // Fail closed — an unreadable or ambiguous record protects it. When
1082
+ // protected, nothing local is moved: the caller cuts from the fetched
1083
+ // `origin/<base>` SHA through the same fetch-only resolution the
1084
+ // non-mutating path uses.
1085
+ const decision = await evaluateGuardSafely(deps, basePath);
1086
+ if (decision.kind === "protect") {
1087
+ (deps.baseWorktreeGuardWarningLog ?? defaultGuardWarningLog)(formatBaseWorktreeGuardWarning(decision, baseBranch));
1088
+ const resolved = await fetchAndResolveBaseSha(deps, baseBranch);
1089
+ if (!resolved.ok)
1090
+ return { ok: false, error: resolved.error };
1091
+ return { ok: true, protectedBaseSha: resolved.base_sha };
1092
+ }
1075
1093
  const merge = await deps.runCommand("git", ["merge", "--ff-only", originRef], { cwd: basePath });
1076
1094
  if (!commandSucceeded(merge)) {
1077
1095
  return {
@@ -1103,6 +1121,25 @@ export async function refreshBaseBranch(deps, options) {
1103
1121
  }
1104
1122
  return { ok: true };
1105
1123
  }
1124
+ function defaultGuardWarningLog(message) {
1125
+ // stderr, never stdout: stdout carries the workflow's normal output.
1126
+ process.stderr.write(`${message}\n`);
1127
+ }
1128
+ /** Run the guard; any failure of the guard itself is a `protect` (fail closed). */
1129
+ async function evaluateGuardSafely(deps, basePath) {
1130
+ try {
1131
+ if (deps.evaluateBaseWorktreeGuard)
1132
+ return await deps.evaluateBaseWorktreeGuard(basePath);
1133
+ return await evaluateBaseWorktreeGuard(basePath, await createBaseWorktreeGuardDeps());
1134
+ }
1135
+ catch {
1136
+ return {
1137
+ kind: "protect",
1138
+ reason: "manifest-unreadable",
1139
+ manifestLocation: ".bridge/plane/plane.json",
1140
+ };
1141
+ }
1142
+ }
1106
1143
  /**
1107
1144
  * Compatibility wrapper around {@link refreshBaseBranch} that preserves the
1108
1145
  * historical `refreshMainBranch(deps, { refreshMain })` signature for callers
@@ -1195,6 +1232,7 @@ async function emitClaudeReviewWorkflowDriftAdvisory(deps, baseBranch, overrides
1195
1232
  // import the VALUES here (so internal uses keep working) and re-export the same
1196
1233
  // references so existing importers of `./start-tickets.js` (index.ts,
1197
1234
  // review-tickets.ts) and the pinned start-tickets tests are unaffected.
1235
+ import { evaluateBaseWorktreeGuard, createBaseWorktreeGuardDeps, formatBaseWorktreeGuardWarning, } from "./plane/base-worktree-guard.js";
1198
1236
  import { validateBranchName, fetchAndResolveBaseSha,
1199
1237
  // BAPI-941: both live in the same leaf module so the interactive refresh path
1200
1238
  // and the non-mutating conductor path share one drift implementation.
@@ -1339,8 +1377,9 @@ export async function resumeWorktrees(deps, options) {
1339
1377
  * E1/F3b proved a clean TEXTUAL merge can still hide a SEMANTIC break (branch A
1340
1378
  * adds a helper; branch B adds a contract test over all helpers), which only the
1341
1379
  * FULL suite catches — so this instruction requires the worker to run the full
1342
- * project test suite (the same gate the advisory pre-push hook enforces) and only
1343
- * push/complete on green, escalating otherwise.
1380
+ * project test suite (every unit suite the project defines, the same suites its
1381
+ * pull request CI runs) and only push/complete on green, escalating otherwise.
1382
+ * The wording stays project-neutral because this CLI runs in other repositories.
1344
1383
  *
1345
1384
  * Single line (no newlines, no `;`, no quotes/apostrophes) so it stays safe when
1346
1385
  * single-quoted into a shell command and embedded in terminal-launcher args.
@@ -1351,8 +1390,8 @@ export function buildResumeModeRemediationFinalizeInstruction() {
1351
1390
  "the current base branch and resolve the merge conflicts. A clean textual merge can " +
1352
1391
  "still break behavior, so inspect for semantic conflicts even when there are no textual " +
1353
1392
  "conflict markers. Before you push or mark the ticket complete, run the full test suite " +
1354
- "for the project (the full unit suite, the same gate enforced by the advisory pre-push " +
1355
- "hook described in CLAUDE.md under the CI cost model and advisory pre-push hook section) " +
1393
+ "for the project (the full unit suite: every unit test suite the project defines, the " +
1394
+ "same suites its pull request CI runs) " +
1356
1395
  "and do not rely on targeted subsets as your only verification. Push and mark the ticket " +
1357
1396
  "complete only after the full suite is green. If you cannot make the full suite pass, " +
1358
1397
  "report the ticket blocked and escalate rather than pushing a green-looking but broken merge.");
@@ -1385,6 +1424,8 @@ export function buildAgentPrompt(key, opts = {}) {
1385
1424
  // Review-only arguments (`--rounds`, non-default `--base-branch`) must never
1386
1425
  // leak into the legacy `/implement-ticket` prompt.
1387
1426
  if (workflow === "review-and-implement") {
1427
+ if (opts.manualTier !== undefined)
1428
+ command += ` --tier=${opts.manualTier}`;
1388
1429
  if (opts.agentName === "cursor-agent")
1389
1430
  command += " --agent=cursor-agent";
1390
1431
  if (opts.reviewRounds !== undefined) {
@@ -1457,13 +1498,13 @@ export function buildAgentInvocation(agent, prompt, quote, modelAlias, launchArg
1457
1498
  }
1458
1499
  }
1459
1500
  /** POSIX agent shell command: `cd '<path>' && <agent> [--model '<alias>'] '<prompt>'`. */
1460
- export function buildPosixAgentShellCommand(agent, key, worktreePath, autoApprove = false, modelAlias, conductorEnabled = false, resumeMode = false, workflow = "implement", reviewRounds, baseBranch, launchArgs = [], bridgeApiBaseUrl) {
1461
- const invocation = buildAgentInvocation(agent, buildAgentPrompt(key, { autoApprove, conductorEnabled, resumeMode, workflow, reviewRounds, baseBranch, agentName: agent.name }), (p) => `'${shSquoteInner(p)}'`, modelAlias, agent.name === "cursor-agent" && autoApprove ? worktreePath : launchArgs);
1501
+ export function buildPosixAgentShellCommand(agent, key, worktreePath, autoApprove = false, modelAlias, conductorEnabled = false, resumeMode = false, workflow = "implement", reviewRounds, baseBranch, launchArgs = [], bridgeApiBaseUrl, manualTier) {
1502
+ const invocation = buildAgentInvocation(agent, buildAgentPrompt(key, { autoApprove, conductorEnabled, resumeMode, workflow, reviewRounds, baseBranch, agentName: agent.name, manualTier }), (p) => `'${shSquoteInner(p)}'`, modelAlias, agent.name === "cursor-agent" && autoApprove ? worktreePath : launchArgs);
1462
1503
  return `cd '${shSquoteInner(worktreePath)}' && ${prependBridgeApiBaseUrlEnvAssignment(invocation, bridgeApiBaseUrl, "darwin")}`;
1463
1504
  }
1464
1505
  /** PowerShell agent shell command: `Set-Location -LiteralPath '<path>'; <agent> [--model '<alias>'] '<prompt>'`. */
1465
- export function buildPowerShellAgentShellCommand(agent, key, worktreePath, autoApprove = false, modelAlias, conductorEnabled = false, resumeMode = false, workflow = "implement", reviewRounds, baseBranch, launchArgs = [], bridgeApiBaseUrl) {
1466
- const invocation = buildAgentInvocation(agent, buildAgentPrompt(key, { autoApprove, conductorEnabled, resumeMode, workflow, reviewRounds, baseBranch, agentName: agent.name }), powershellSquote, modelAlias, agent.name === "cursor-agent" && autoApprove ? worktreePath : launchArgs);
1506
+ export function buildPowerShellAgentShellCommand(agent, key, worktreePath, autoApprove = false, modelAlias, conductorEnabled = false, resumeMode = false, workflow = "implement", reviewRounds, baseBranch, launchArgs = [], bridgeApiBaseUrl, manualTier) {
1507
+ const invocation = buildAgentInvocation(agent, buildAgentPrompt(key, { autoApprove, conductorEnabled, resumeMode, workflow, reviewRounds, baseBranch, agentName: agent.name, manualTier }), powershellSquote, modelAlias, agent.name === "cursor-agent" && autoApprove ? worktreePath : launchArgs);
1467
1508
  return `Set-Location -LiteralPath ${powershellSquote(worktreePath)}; ${prependBridgeApiBaseUrlEnvAssignment(invocation, bridgeApiBaseUrl, "win32")}`;
1468
1509
  }
1469
1510
  /**
@@ -1474,10 +1515,10 @@ export function buildPowerShellAgentShellCommand(agent, key, worktreePath, autoA
1474
1515
  * injected as `--model` at the spawn boundary. `conductorEnabled` appends the
1475
1516
  * BAPI-586 PR-base contract instruction to the prompt (opt-in via `--conductor`).
1476
1517
  */
1477
- export function buildAgentShellCommand(agent, key, worktreePath, platform = "darwin", autoApprove = false, modelAlias, conductorEnabled = false, resumeMode = false, workflow = "implement", reviewRounds, baseBranch, launchArgs = [], bridgeApiBaseUrl) {
1518
+ export function buildAgentShellCommand(agent, key, worktreePath, platform = "darwin", autoApprove = false, modelAlias, conductorEnabled = false, resumeMode = false, workflow = "implement", reviewRounds, baseBranch, launchArgs = [], bridgeApiBaseUrl, manualTier) {
1478
1519
  if (platform === "win32")
1479
- return buildPowerShellAgentShellCommand(agent, key, worktreePath, autoApprove, modelAlias, conductorEnabled, resumeMode, workflow, reviewRounds, baseBranch, launchArgs, bridgeApiBaseUrl);
1480
- return buildPosixAgentShellCommand(agent, key, worktreePath, autoApprove, modelAlias, conductorEnabled, resumeMode, workflow, reviewRounds, baseBranch, launchArgs, bridgeApiBaseUrl);
1520
+ return buildPowerShellAgentShellCommand(agent, key, worktreePath, autoApprove, modelAlias, conductorEnabled, resumeMode, workflow, reviewRounds, baseBranch, launchArgs, bridgeApiBaseUrl, manualTier);
1521
+ return buildPosixAgentShellCommand(agent, key, worktreePath, autoApprove, modelAlias, conductorEnabled, resumeMode, workflow, reviewRounds, baseBranch, launchArgs, bridgeApiBaseUrl, manualTier);
1481
1522
  }
1482
1523
  /**
1483
1524
  * Build the shell command run inside a spawned tab/session for an ARBITRARY
@@ -1605,7 +1646,7 @@ export async function spawnMacOSTerminalTab(deps, terminal, shellCommand, contex
1605
1646
  : buildTerminalAppleScript(shellCommand, title);
1606
1647
  const result = await deps.runCommand("osascript", ["-e", script]);
1607
1648
  if (commandSucceeded(result))
1608
- return { ok: true };
1649
+ return { ok: true, destination: terminal === "iterm" ? "iTerm tab" : "macOS Terminal tab" };
1609
1650
  const reason = (result.stderr || result.stdout || "").trim();
1610
1651
  return {
1611
1652
  ok: false,
@@ -1693,7 +1734,7 @@ export async function spawnWindowsTerminalTab(deps, _terminal, shellCommand, con
1693
1734
  const args = buildWindowsTerminalArgs(worktreePath, shellCommand, title);
1694
1735
  const result = await deps.runCommand(WINDOWS_TERMINAL_COMMAND, args);
1695
1736
  if (commandSucceeded(result))
1696
- return { ok: true };
1737
+ return { ok: true, destination: "Windows Terminal tab" };
1697
1738
  const reason = combineCommandOutput(result);
1698
1739
  return {
1699
1740
  ok: false,
@@ -1716,7 +1757,7 @@ export async function spawnWindowsTerminalTab(deps, _terminal, shellCommand, con
1716
1757
  fallback,
1717
1758
  ]);
1718
1759
  if (commandSucceeded(result))
1719
- return { ok: true };
1760
+ return { ok: true, destination: "PowerShell fallback window" };
1720
1761
  const reason = combineCommandOutput(result);
1721
1762
  return {
1722
1763
  ok: false,
@@ -1820,7 +1861,7 @@ export async function spawnLinuxTmuxTerminalTab(deps, _terminal, shellCommand, c
1820
1861
  : buildTmuxNewSessionArgs(session, window, worktreePath, paneCommand);
1821
1862
  const result = await deps.runCommand(TMUX_COMMAND, args);
1822
1863
  if (commandSucceeded(result))
1823
- return { ok: true };
1864
+ return { ok: true, destination: "detached tmux session" };
1824
1865
  const reason = combineCommandOutput(result);
1825
1866
  return {
1826
1867
  ok: false,
@@ -2080,7 +2121,7 @@ export async function spawnTabsForCreatedWorktrees(deps, rows, terminal, buildSh
2080
2121
  worktreePath: row.path,
2081
2122
  });
2082
2123
  if (result.ok) {
2083
- out.push({ ...row, status: "spawned" });
2124
+ out.push({ ...row, status: "spawned", ...(result.destination ? { sessionDestination: result.destination } : {}) });
2084
2125
  }
2085
2126
  else {
2086
2127
  out.push({ ...row, status: "spawn-failed", error: result.error });
@@ -2105,7 +2146,7 @@ export function buildDryRunResults(keys, overrides) {
2105
2146
  * PowerShell on Windows, `wt` + POSIX on macOS/Linux, and a non-throwing `wt` +
2106
2147
  * POSIX fallback for unsupported platforms.
2107
2148
  */
2108
- export function getDryRunPlatformDetails(agent, platform = process.platform, env = process.env, autoApprove = false, conductorEnabled = false, repoName = null, workflow = "implement", reviewRounds, baseBranch) {
2149
+ export function getDryRunPlatformDetails(agent, platform = process.platform, env = process.env, autoApprove = false, conductorEnabled = false, repoName = null, workflow = "implement", reviewRounds, baseBranch, manualTier) {
2109
2150
  return {
2110
2151
  worktrunkBinary: resolveWorktrunkBinary(platform, env),
2111
2152
  // The builder accepts an optional resolved modelAlias; the dry-run caller
@@ -2118,7 +2159,7 @@ export function getDryRunPlatformDetails(agent, platform = process.platform, env
2118
2159
  // buildAgentShellCommand/buildAgentPrompt path as a real spawn. A Codex
2119
2160
  // dry-run has no created worktree to bind, so its preview omits the
2120
2161
  // process-local -C/-c MCP scope that the real spawn resolves later.
2121
- buildAgentShellCommand: (key, worktreePath, modelAlias, launchArgs) => prependBaseBranchEnvAssignment(prependRepoNameEnvAssignment(buildAgentShellCommand(agent, key, worktreePath, platform, autoApprove, modelAlias, conductorEnabled, false, workflow, reviewRounds, baseBranch, launchArgs, resolveSpawnBridgeApiBaseUrl(env)), repoName, platform), resolvePrBaseBranchEnvValue(conductorEnabled, baseBranch), platform),
2162
+ buildAgentShellCommand: (key, worktreePath, modelAlias, launchArgs) => prependBaseBranchEnvAssignment(prependRepoNameEnvAssignment(buildAgentShellCommand(agent, key, worktreePath, platform, autoApprove, modelAlias, conductorEnabled, false, workflow, reviewRounds, baseBranch, launchArgs, resolveSpawnBridgeApiBaseUrl(env), manualTier), repoName, platform), resolvePrBaseBranchEnvValue(conductorEnabled, baseBranch), platform),
2122
2163
  };
2123
2164
  }
2124
2165
  /**
@@ -2157,8 +2198,8 @@ export function buildDryRunMcpProvisioningLines(worktreePath, platform = process
2157
2198
  * the secret-free MCP provisioning preview. Pure platform formatting only — no
2158
2199
  * preflight, no routing failures.
2159
2200
  */
2160
- export function buildDryRunDetailLines(agent, key, branch, platform = process.platform, env = process.env, baseBranch = "main", autoApprove = false, modelAlias = null, conductorEnabled = false, repoName = null, mcpServerInvocation, workflow = "implement", reviewRounds) {
2161
- const { worktrunkBinary, buildAgentShellCommand: build } = getDryRunPlatformDetails(agent, platform, env, autoApprove, conductorEnabled, repoName, workflow, reviewRounds, baseBranch);
2201
+ export function buildDryRunDetailLines(agent, key, branch, platform = process.platform, env = process.env, baseBranch = "main", autoApprove = false, modelAlias = null, conductorEnabled = false, repoName = null, mcpServerInvocation, workflow = "implement", reviewRounds, manualTier) {
2202
+ const { worktrunkBinary, buildAgentShellCommand: build } = getDryRunPlatformDetails(agent, platform, env, autoApprove, conductorEnabled, repoName, workflow, reviewRounds, baseBranch, manualTier);
2162
2203
  const wtArgs = buildWtSwitchArgs(branch, false, baseBranch);
2163
2204
  const agentInvocation = build(key, "<worktree-path>", modelAlias);
2164
2205
  // BAPI-801/BAPI-847: one concise secondary detail naming the PR base, emitted
@@ -2182,6 +2223,23 @@ export function buildDryRunDetailLines(agent, key, branch, platform = process.pl
2182
2223
  * ` path=PATH` when available) followed by warning lines for any create/spawn
2183
2224
  * failures.
2184
2225
  */
2226
+ /** Explain only actual successful sessions; summary serialization stays stable. */
2227
+ export function formatSpawnedSessionExplanation(rows, context) {
2228
+ const spawned = rows.filter((row) => row.status === "spawned");
2229
+ if (!spawned.length)
2230
+ return "";
2231
+ const lines = ["Spawned sessions:"];
2232
+ for (const row of spawned) {
2233
+ lines.push(` ${row.key}: ${context.agent} runs in ${row.path ?? "a worktree whose path was not reported"} on branch ${row.branch}; ${row.sessionDestination ?? "terminal destination not reported"}.`);
2234
+ }
2235
+ lines.push("Work on each branch from its reported folder: it is already checked out there and normally cannot also be checked out in the original folder.");
2236
+ lines.push("Monitor the reported terminal session and its agent output; use the attach commands below on Linux. Session startup is not review or implementation completion.");
2237
+ if (context.workflow === "review-and-implement")
2238
+ lines.push("Each session reviews independently, then opens a fresh implementation session only after its review halt gate permits it.");
2239
+ if (spawned.length > 1)
2240
+ lines.push("Coordinate ownership across these independent sessions; pause affected work if overlapping changes or conflicts appear.");
2241
+ return lines.join("\n");
2242
+ }
2185
2243
  export function formatSummaryReport(rows) {
2186
2244
  const lines = ["Summary:"];
2187
2245
  // Surface the single conductor run id near the top (first line stays
@@ -2285,6 +2343,7 @@ export function buildCommandProvisioningDeps(deps) {
2285
2343
  runCommand: deps.runCommand,
2286
2344
  platform: deps.platform,
2287
2345
  cwd: deps.cwd,
2346
+ ...(!deps.readFile && !deps.writeFile && !deps.mkdir ? { recordAssets: recordProvisionedAssets } : {}),
2288
2347
  };
2289
2348
  }
2290
2349
  /**
@@ -2383,6 +2442,12 @@ export function formatModelRoutingDiagnosticLine(d) {
2383
2442
  case "posture-unavailable":
2384
2443
  return ("model routing: optimization posture was unavailable; Quality routing was retained " +
2385
2444
  "(reason=optimization-posture-unavailable).");
2445
+ case "review-consumer-unrecognized":
2446
+ return (`model routing: the Bridge API server did not recognize the review consumer${ticketSuffix} ` +
2447
+ "(reason=consumer-unrecognized); the review tab uses the premium fallback. Upgrade the server.");
2448
+ case "review-threshold-legacy":
2449
+ return (`model routing: review thresholds are not configured${ticketSuffix} ` +
2450
+ "(reason=legacy); the review tab uses the premium fallback. Configure review model-tier thresholds.");
2386
2451
  default:
2387
2452
  return `model routing degraded: ${d.message} ${failOpen}`;
2388
2453
  }
@@ -3023,7 +3088,43 @@ export function formatModelRoutingDecisionDiagnosticLine(input) {
3023
3088
  return (`[model-routing] source=${input.source} baseline=${input.baseline} ` +
3024
3089
  `posture=${input.posture} adjustment=${input.adjustment} effective=${input.effective} ` +
3025
3090
  `alias=${input.alias ?? "unresolved"}` +
3026
- (input.threshold ? ` threshold=${input.threshold}` : ""));
3091
+ (input.threshold ? ` threshold=${input.threshold}` : "") +
3092
+ (input.phase === "review" ? " phase=review consumer=review" : ""));
3093
+ }
3094
+ /**
3095
+ * BAPI-1365: classify a successfully fetched review-consumer `/model-tier`
3096
+ * response. `consumer-unrecognized` = the server did not echo `consumer:
3097
+ * "review"` (its tier and marker are not a review decision and are never
3098
+ * interpreted); `legacy` = it echoed review but no stored review thresholds
3099
+ * decided the tier. `undefined` = a usable review decision, OR a
3100
+ * `source: "fallback"` response (the difficulty itself failed; callers keep the
3101
+ * existing lookup-failure handling for that).
3102
+ */
3103
+ function reviewTierFallbackCause(value) {
3104
+ if (value.consumer !== "review")
3105
+ return "consumer-unrecognized";
3106
+ if (value.source === "fallback")
3107
+ return undefined;
3108
+ if (!isThresholdDecidedTierResponse(value))
3109
+ return "legacy";
3110
+ return undefined;
3111
+ }
3112
+ /** BAPI-1365: the secret-free operator warning and diagnostic for a review fallback cause. */
3113
+ function reviewTierFallbackNotice(cause, repoName, ticketKey) {
3114
+ const kind = cause === "consumer-unrecognized" ? "review-consumer-unrecognized" : "review-threshold-legacy";
3115
+ const warning = cause === "consumer-unrecognized"
3116
+ ? `model routing: server did not recognize the review consumer for ${ticketKey} (reason=consumer-unrecognized)`
3117
+ : `model routing: review thresholds are not configured for ${ticketKey} (reason=legacy)`;
3118
+ return {
3119
+ warning,
3120
+ diagnostic: makeModelRoutingDiagnostic({
3121
+ kind,
3122
+ repoName,
3123
+ storeTarget: `bapi:${repoName}`,
3124
+ ticketKey,
3125
+ message: warning,
3126
+ }),
3127
+ };
3027
3128
  }
3028
3129
  /**
3029
3130
  * BAPI-1343 R-6: the threshold marker for a successfully fetched `/model-tier`
@@ -3032,11 +3133,16 @@ export function formatModelRoutingDecisionDiagnosticLine(input) {
3032
3133
  * (`configured` or `anomaly`). Every other case returns `undefined`, so the
3033
3134
  * caller keeps today's exact downshift and text.
3034
3135
  */
3035
- function thresholdSkipMarker(posture, tierResult) {
3136
+ function thresholdSkipMarker(posture, tierResult, reviewWorkflow = false) {
3036
3137
  if (posture !== "maximize_subscription")
3037
3138
  return undefined;
3038
3139
  if (!tierResult || !tierResult.ok)
3039
3140
  return undefined;
3141
+ // BAPI-1365: a review-consumer lookup counts only when the server echoed the
3142
+ // review consumer; an older server that ignored the parameter answered for
3143
+ // the implementation consumer, which says nothing about the review threshold.
3144
+ if (reviewWorkflow && tierResult.value.consumer !== "review")
3145
+ return undefined;
3040
3146
  if (!isThresholdDecidedTierResponse(tierResult.value))
3041
3147
  return undefined;
3042
3148
  return tierResult.value.thresholdMode;
@@ -3206,6 +3312,12 @@ errorLog = (message) => console.error(message)) {
3206
3312
  // produced it.
3207
3313
  const postureResolution = await resolveOptimizationPostureForStartTickets(access);
3208
3314
  const posture = postureResolution.posture;
3315
+ // BAPI-1365: the spawned tab of a `review-and-implement` launch is a REVIEW
3316
+ // tab, so its tier lookups ask for the review consumer. An ordinary
3317
+ // `implement` launch supplies no options and keeps the endpoint's default
3318
+ // implementation consumer. Derived once and shared by both fetch paths below.
3319
+ const isReviewWorkflow = options.workflow === "review-and-implement";
3320
+ const tierFetchOptions = isReviewWorkflow ? { consumer: "review" } : {};
3209
3321
  // BAPI-1225: one deterministic, best-effort diagnostic line per repository-level
3210
3322
  // resolution, emitted exactly here — outside the per-ticket row loop below, so a
3211
3323
  // multi-ticket invocation still logs it once for this canonical repository. Uses
@@ -3232,14 +3344,14 @@ errorLog = (message) => console.error(message)) {
3232
3344
  // hoisting pattern above.
3233
3345
  //
3234
3346
  // BAPI-1343 R-6: under `maximize_subscription` ONLY, each eligible row's
3235
- // `/model-tier` is fetched once (the shared bounded fetch, no `consumer`
3236
- // parameter) purely to read `threshold_mode`. A threshold-decided
3347
+ // `/model-tier` is fetched once (the shared bounded fetch; BAPI-1365: with
3348
+ // the workflow's consumer) purely to read `threshold_mode`. A threshold-decided
3237
3349
  // (`configured`/`anomaly`) response keeps the carried tier — stored
3238
3350
  // thresholds made it final. A fetch failure, `legacy`, or anything else
3239
3351
  // downshifts exactly as before. `quality`/`speed` never downshift, so they
3240
3352
  // make no extra request.
3241
3353
  const thresholdTierMap = posture === "maximize_subscription"
3242
- ? await fetchTicketModelTiersForRows(access, eligible, options.maxParallel, isEligible)
3354
+ ? await fetchTicketModelTiersForRows(access, eligible, options.maxParallel, isEligible, tierFetchOptions)
3243
3355
  : null;
3244
3356
  const aliasByEffectiveTier = new Map();
3245
3357
  const resolveAutoTierAlias = async (effectiveTier) => {
@@ -3269,7 +3381,14 @@ errorLog = (message) => console.error(message)) {
3269
3381
  autoOut.push(row);
3270
3382
  continue;
3271
3383
  }
3272
- const threshold = thresholdSkipMarker(posture, thresholdTierMap?.get(row.key));
3384
+ const markerResult = thresholdTierMap?.get(row.key);
3385
+ const threshold = thresholdSkipMarker(posture, markerResult, isReviewWorkflow);
3386
+ // BAPI-1365: for a review launch, say WHY a fetched marker did not decide
3387
+ // the tier (never a review-threshold claim). A failed fetch keeps the
3388
+ // existing silent downshift.
3389
+ const markerCause = isReviewWorkflow && markerResult && markerResult.ok
3390
+ ? reviewTierFallbackCause(markerResult.value)
3391
+ : undefined;
3273
3392
  const effectiveTier = posture === "maximize_subscription" && !threshold
3274
3393
  ? downshiftModelTier(autoTier)
3275
3394
  : autoTier;
@@ -3277,7 +3396,7 @@ errorLog = (message) => console.error(message)) {
3277
3396
  const lineageSuffix = threshold
3278
3397
  ? `; threshold:${threshold}`
3279
3398
  : posture === "maximize_subscription"
3280
- ? `; auto-tier:${autoTier}->maximize:${effectiveTier}`
3399
+ ? `; auto-tier:${autoTier}->maximize:${effectiveTier}${markerCause ? `; review-marker:${markerCause}` : ""}`
3281
3400
  : "";
3282
3401
  const { resolvedAlias, aliasWarning } = await resolveAutoTierAlias(effectiveTier);
3283
3402
  const reason = resolvedAlias
@@ -3294,7 +3413,14 @@ errorLog = (message) => console.error(message)) {
3294
3413
  ? { modelRoutingDiagnostic: codexEntitlementDiagnostic(access.repoName) }
3295
3414
  : {}),
3296
3415
  };
3297
- autoOut.push(aliasWarning ? appendSummaryRowWarning(baseRow, aliasWarning) : baseRow);
3416
+ let pushedRow = aliasWarning ? appendSummaryRowWarning(baseRow, aliasWarning) : baseRow;
3417
+ if (markerCause) {
3418
+ const notice = reviewTierFallbackNotice(markerCause, access.repoName, row.key);
3419
+ pushedRow = appendSummaryRowWarning(pushedRow, notice.warning);
3420
+ if (!pushedRow.modelRoutingDiagnostic)
3421
+ pushedRow = { ...pushedRow, modelRoutingDiagnostic: notice.diagnostic };
3422
+ }
3423
+ autoOut.push(pushedRow);
3298
3424
  // BAPI-1236: one `automatic_snapshot` diagnostic per eligible row — the
3299
3425
  // carried baseline, the newly resolved posture, the exact adjustment token,
3300
3426
  // the calculated effective tier, and the validated alias (or `unresolved`).
@@ -3306,13 +3432,14 @@ errorLog = (message) => console.error(message)) {
3306
3432
  effective: effectiveTier,
3307
3433
  alias: resolvedAlias,
3308
3434
  ...(threshold ? { threshold } : {}),
3435
+ ...(isReviewWorkflow ? { phase: "review" } : {}),
3309
3436
  });
3310
3437
  }
3311
3438
  out = autoOut;
3312
3439
  }
3313
3440
  else {
3314
3441
  // Ordinary per-ticket difficulty-lookup path.
3315
- const tierMap = await fetchTicketModelTiersForRows(access, eligible, options.maxParallel, isEligible);
3442
+ const tierMap = await fetchTicketModelTiersForRows(access, eligible, options.maxParallel, isEligible, tierFetchOptions);
3316
3443
  // Lazily resolve+validate the premium fallback alias at most once for this run
3317
3444
  // (the alias is identical across rows; cursor-agent validation spawns a probe).
3318
3445
  let premiumApplier = null;
@@ -3351,6 +3478,40 @@ errorLog = (message) => console.error(message)) {
3351
3478
  continue;
3352
3479
  }
3353
3480
  const { difficulty, tier, source } = tierResult.value;
3481
+ // BAPI-1365: a review-launch response is a review-threshold decision only
3482
+ // when the server echoed `consumer: "review"` AND stored thresholds decided
3483
+ // it. Anything else takes the existing premium fallback with a closed
3484
+ // reason — the returned tier and marker are never interpreted.
3485
+ if (isReviewWorkflow) {
3486
+ const cause = reviewTierFallbackCause(tierResult.value);
3487
+ if (cause) {
3488
+ const notice = reviewTierFallbackNotice(cause, access.repoName, row.key);
3489
+ if (agent.name === "codex") {
3490
+ loopOut.push(codexDefaultRows([row], () => true, notice.warning, notice.diagnostic)[0]);
3491
+ continue;
3492
+ }
3493
+ loopOut.push(await applyPremiumFallback(row, notice.warning, notice.diagnostic));
3494
+ continue;
3495
+ }
3496
+ if (source === "fallback") {
3497
+ // The server could not obtain a difficulty: its premium tier is a
3498
+ // guess, not a review decision — keep the lookup-failure handling.
3499
+ const warning = `model routing: tier lookup failed for ${row.key}; using agent default model`;
3500
+ const diagnostic = makeModelRoutingDiagnostic({
3501
+ kind: "tier-fetch-unavailable",
3502
+ repoName: access.repoName,
3503
+ storeTarget: `bapi:${access.repoName}`,
3504
+ ticketKey: row.key,
3505
+ message: `tier lookup failed for ${row.key}`,
3506
+ });
3507
+ if (agent.name === "codex") {
3508
+ loopOut.push(codexDefaultRows([row], () => true, warning, diagnostic)[0]);
3509
+ continue;
3510
+ }
3511
+ loopOut.push(await applyPremiumFallback(row, warning, diagnostic));
3512
+ continue;
3513
+ }
3514
+ }
3354
3515
  if (!tier) {
3355
3516
  // Backend reported no usable difficulty — default to premium/Opus. (The
3356
3517
  // backend now returns a premium fallback itself, so this is defensive.)
@@ -3396,7 +3557,7 @@ errorLog = (message) => console.error(message)) {
3396
3557
  // diagnostic carry a distinct `threshold` marker instead. The harness-
3397
3558
  // local premium fallback above is untouched.
3398
3559
  const difficultyTier = tier;
3399
- const threshold = thresholdSkipMarker(posture, tierResult);
3560
+ const threshold = thresholdSkipMarker(posture, tierResult, isReviewWorkflow);
3400
3561
  const effectiveTier = posture === "maximize_subscription" && !threshold
3401
3562
  ? downshiftModelTier(difficultyTier)
3402
3563
  : difficultyTier;
@@ -3406,7 +3567,10 @@ errorLog = (message) => console.error(message)) {
3406
3567
  : posture === "maximize_subscription"
3407
3568
  ? `; difficulty:${difficultyTier}->maximize:${effectiveTier}`
3408
3569
  : "";
3409
- const thresholdField = threshold ? { threshold } : {};
3570
+ const thresholdField = {
3571
+ ...(threshold ? { threshold } : {}),
3572
+ ...(isReviewWorkflow ? { phase: "review" } : {}),
3573
+ };
3410
3574
  // BAPI-1236: stamp `difficulty_lookup` rather than copying the backend
3411
3575
  // cache/computation source (`cached`/`computed`/`fallback`) into the
3412
3576
  // summary's own routing-provenance field — that backend source is
@@ -3758,7 +3922,7 @@ export async function orchestrateStartTickets(deps, options, overrides = {}) {
3758
3922
  // epic.base_branch override applied above) so the spawned worker gets
3759
3923
  // BAPI_BASE_BRANCH — every conductor worker, and a plain worker whose base
3760
3924
  // is non-default.
3761
- options.baseBranch, bridgeApiBaseUrl);
3925
+ options.baseBranch, bridgeApiBaseUrl, isModelTier(options.injectedTier) ? options.injectedTier : undefined);
3762
3926
  if (!platformConfig.ok)
3763
3927
  return { ok: false, error: platformConfig.error };
3764
3928
  // BAPI-527: resolve the base start point new worktrees are cut from BEFORE any
@@ -3787,6 +3951,11 @@ export async function orchestrateStartTickets(deps, options, overrides = {}) {
3787
3951
  });
3788
3952
  if (!refresh.ok)
3789
3953
  return { ok: false, error: refresh.error };
3954
+ // BAPI-1362: a live plane member runs from the base worktree, so the local
3955
+ // branch was left where it was; cut from the fetched remote SHA instead.
3956
+ if (refresh.protectedBaseSha !== undefined) {
3957
+ effectiveBaseStartPoint = refresh.protectedBaseSha;
3958
+ }
3790
3959
  }
3791
3960
  // BAPI-941: the `claude-review` workflow-drift advisory, emitted ONCE per
3792
3961
  // worker spawn at the one choke point both dispatch paths pass through, after
@@ -4023,7 +4192,7 @@ export async function runStartTicketsCli(argv, overrides = {}) {
4023
4192
  const branch = resolveBranchForTicket(key, options.branchOverrides);
4024
4193
  const routedRow = routedByKey.get(key);
4025
4194
  const modelAlias = routedRow?.modelAlias ?? null;
4026
- for (const line of buildDryRunDetailLines(agent, key, branch, deps.platform, deps.env, options.baseBranch, options.autoApprove, modelAlias, options.conductorEnabled ?? false, dryRunRepoName, dryRunMcpInvocation, options.workflow, options.reviewRounds)) {
4195
+ for (const line of buildDryRunDetailLines(agent, key, branch, deps.platform, deps.env, options.baseBranch, options.autoApprove, modelAlias, options.conductorEnabled ?? false, dryRunRepoName, dryRunMcpInvocation, options.workflow, options.reviewRounds, isModelTier(options.injectedTier) ? options.injectedTier : undefined)) {
4027
4196
  log(line);
4028
4197
  }
4029
4198
  log(`DRY-RUN: model routing: ${formatModelRoutingLine(routedRow ?? { key, branch, status: "dry-run" }, agent, { workflow: options.workflow })}`);
@@ -4065,6 +4234,9 @@ export async function runStartTicketsCli(argv, overrides = {}) {
4065
4234
  return 1;
4066
4235
  }
4067
4236
  log(formatSummaryReport(result.rows));
4237
+ const explanation = formatSpawnedSessionExplanation(result.rows, { agent: options.agentName, workflow: options.workflow ?? "implement" });
4238
+ if (explanation)
4239
+ log(explanation);
4068
4240
  // Linux: each spawned ticket runs in a detached tmux session — tell the user
4069
4241
  // how to attach.
4070
4242
  const spawned = result.rows.filter((r) => r.status === "spawned");
@@ -7,6 +7,10 @@
7
7
  * "this operation has no local-backend equivalent" refusal, which the ticket
8
8
  * routes return as HTTP 409 with `detail.error = "UNSUPPORTED_IN_LOCAL_MODE"`.
9
9
  *
10
+ * BAPI-1382 adds a second trusted pair: the plan-entitlement denial, which the
11
+ * server returns as HTTP 403 with `detail.error = "ENTITLEMENT_REQUIRED"`. It
12
+ * tells a plan limit apart from a bad credential, which is also a 403.
13
+ *
10
14
  * Recognition is by the exact `(error, status)` PAIR — never by message text.
11
15
  * Message text is prose that gets reworded; matching on it would make a copy
12
16
  * edit silently change orchestration control flow.
@@ -18,6 +22,54 @@
18
22
  export const UNSUPPORTED_IN_LOCAL_MODE_ERROR = "UNSUPPORTED_IN_LOCAL_MODE";
19
23
  /** The HTTP status that marker is only ever valid with. */
20
24
  export const UNSUPPORTED_IN_LOCAL_MODE_STATUS = 409;
25
+ /** The plan-entitlement marker (BAPI-1382): the account's plan lacks a feature. */
26
+ export const ENTITLEMENT_REQUIRED_ERROR = "ENTITLEMENT_REQUIRED";
27
+ /** The HTTP status that marker is only ever valid with. */
28
+ export const ENTITLEMENT_REQUIRED_STATUS = 403;
29
+ /**
30
+ * The request-time too-little-ticket-detail refusal (BAPI-1382): the generate
31
+ * routes answer HTTP 422 with `detail.error = "INSUFFICIENT_TICKET_DETAIL"`
32
+ * plus the measured length, threshold, and description source.
33
+ */
34
+ export const INSUFFICIENT_TICKET_DETAIL_ERROR = "INSUFFICIENT_TICKET_DETAIL";
35
+ /** The HTTP status that marker is only ever valid with. */
36
+ export const INSUFFICIENT_TICKET_DETAIL_STATUS = 422;
37
+ /**
38
+ * The server `(error, status)` pairs the MCP relay keeps as received. Every
39
+ * other server-provided `error` is replaced by the relay's own status-derived
40
+ * code, so an arbitrary detail object can never forge a code.
41
+ */
42
+ export const TRUSTED_UPSTREAM_ERROR_PAIRS = [
43
+ [UNSUPPORTED_IN_LOCAL_MODE_ERROR, UNSUPPORTED_IN_LOCAL_MODE_STATUS],
44
+ [ENTITLEMENT_REQUIRED_ERROR, ENTITLEMENT_REQUIRED_STATUS],
45
+ [INSUFFICIENT_TICKET_DETAIL_ERROR, INSUFFICIENT_TICKET_DETAIL_STATUS],
46
+ ];
47
+ /** True only for an exact trusted `(error, status)` pair. */
48
+ export function isTrustedUpstreamErrorPair(error, status) {
49
+ return TRUSTED_UPSTREAM_ERROR_PAIRS.some(([code, code_status]) => error === code && status === code_status);
50
+ }
51
+ /**
52
+ * Add the typed `operation` to a serialized error envelope (BAPI-1382). A text
53
+ * that is not an envelope is wrapped into one with the given status, so a
54
+ * refused request is always parseable and never a prose-prefixed string.
55
+ */
56
+ export function withRequestOperation(envelopeText, operation, status) {
57
+ let parsed = null;
58
+ try {
59
+ parsed = JSON.parse(envelopeText);
60
+ }
61
+ catch {
62
+ parsed = null;
63
+ }
64
+ if (parsed !== null &&
65
+ typeof parsed === "object" &&
66
+ !Array.isArray(parsed) &&
67
+ typeof parsed.error === "string" &&
68
+ typeof parsed.status === "number") {
69
+ return JSON.stringify({ ...parsed, operation });
70
+ }
71
+ return JSON.stringify({ error: "UNKNOWN_ERROR", status, message: envelopeText, operation });
72
+ }
21
73
  /**
22
74
  * Parse `text` as a tool-error envelope, or return `null`.
23
75
  *
@@ -52,8 +104,17 @@ export function parseToolErrorEnvelope(text) {
52
104
  if (Array.isArray(obj.unsupported_fields)) {
53
105
  envelope.unsupported_fields = obj.unsupported_fields.filter((v) => typeof v === "string");
54
106
  }
107
+ if (typeof obj.operation === "string") {
108
+ envelope.operation = obj.operation;
109
+ }
55
110
  return envelope;
56
111
  }
112
+ /** True for the exact plan-entitlement contract: the marker AND HTTP 403. */
113
+ export function isEntitlementRequiredEnvelope(envelope) {
114
+ return (envelope !== null &&
115
+ envelope.error === ENTITLEMENT_REQUIRED_ERROR &&
116
+ envelope.status === ENTITLEMENT_REQUIRED_STATUS);
117
+ }
57
118
  /** True for the exact unsupported-local contract: the marker AND HTTP 409. */
58
119
  export function isUnsupportedInLocalModeEnvelope(envelope) {
59
120
  return (envelope !== null &&