@bridge_gpt/mcp-server 0.2.65 → 0.2.67

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 (132) hide show
  1. package/README.md +269 -82
  2. package/build/agent-capabilities/probes.js +15 -5
  3. package/build/agent-launchers/claude-executor-adapter.js +55 -34
  4. package/build/agent-launchers/executor-adapter.js +34 -4
  5. package/build/agents.generated.js +1 -1
  6. package/build/bounded-wait.js +67 -10
  7. package/build/bridge-config.js +104 -0
  8. package/build/codex-bridge-profile.js +10 -1
  9. package/build/commands.generated.js +12 -12
  10. package/build/conductor/bridge-api-client.js +372 -10
  11. package/build/conductor/bring-up-facts.js +1 -1
  12. package/build/conductor/doctor.js +3 -3
  13. package/build/conductor/epic-child-continuation.js +61 -34
  14. package/build/conductor/epic-implementer-cli.js +34 -6
  15. package/build/conductor/errors.js +4 -3
  16. package/build/conductor/guided-repair.js +327 -0
  17. package/build/conductor/hook-runtime-qualification.js +1 -1
  18. package/build/conductor/merge-execution-contract.js +27 -8
  19. package/build/conductor/merge-execution-eligibility.js +3 -2
  20. package/build/conductor/pr-ci-producer.js +1 -1
  21. package/build/conductor/readiness-cli.js +46 -9
  22. package/build/conductor/readiness-sections.js +41 -6
  23. package/build/conductor/readiness.js +204 -30
  24. package/build/conductor/recovery-cli.js +703 -20
  25. package/build/conductor/recovery-operations.js +392 -3
  26. package/build/conductor/repair-contract.js +112 -3
  27. package/build/conductor/repository-workflow-remedy.js +125 -0
  28. package/build/conductor/tools.js +2 -2
  29. package/build/conductor-bundle-artifacts.js +8 -0
  30. package/build/credential-store.js +5 -4
  31. package/build/diagnostic-text.js +36 -0
  32. package/build/docs.generated.js +8 -3
  33. package/build/doctor.js +70 -10
  34. package/build/drive-epic-key.js +6 -5
  35. package/build/drive-epic.js +479 -131
  36. package/build/epic-coherence-input.js +2 -1
  37. package/build/epic-implementer-bin.js +30 -26
  38. package/build/epic-implementer-bundle-cli.js +2 -2
  39. package/build/epic-launch-diagnostic.js +31 -0
  40. package/build/epic-launch-profile.js +44 -14
  41. package/build/executor/cli.js +3 -2
  42. package/build/executor/completion-contract.js +48 -5
  43. package/build/executor/completion-supervision.js +9 -0
  44. package/build/executor/correction-dispatch.js +60 -0
  45. package/build/executor/env.js +25 -0
  46. package/build/executor/heartbeat.js +1 -1
  47. package/build/executor/http-client.js +35 -0
  48. package/build/executor/integration-job.js +6 -1
  49. package/build/executor/job-errors.js +38 -4
  50. package/build/executor/job-log-registry.js +340 -73
  51. package/build/executor/job-runner.js +485 -74
  52. package/build/executor/job-types.js +8 -13
  53. package/build/executor/observation.js +6 -1
  54. package/build/executor/preflight.js +136 -10
  55. package/build/executor/prompt-spec.js +113 -9
  56. package/build/executor/required-work-ledger.js +6 -1
  57. package/build/executor/resume-pre-spawn.js +2 -0
  58. package/build/executor/runner.js +31 -2
  59. package/build/executor/service-unit.js +7 -1
  60. package/build/executor/stale-artifacts.js +5 -4
  61. package/build/executor/types.js +1 -0
  62. package/build/executor/watch-cli.js +35 -8
  63. package/build/executor/worker-command.js +43 -43
  64. package/build/executor/worker-finalization.js +161 -43
  65. package/build/executor/worker-guard-hook.js +152 -12
  66. package/build/executor/worker-log.js +137 -70
  67. package/build/executor/worktree-gc.js +30 -6
  68. package/build/executor/worktree.js +82 -1
  69. package/build/index.js +482 -660
  70. package/build/init.js +107 -4
  71. package/build/install-bridge-conductor.js +8 -6
  72. package/build/install-bridge.js +287 -23
  73. package/build/install-doctor.js +172 -30
  74. package/build/install-reexec.js +69 -7
  75. package/build/macos-terminal-permission-guidance.js +16 -0
  76. package/build/model-thresholds-tool.js +185 -0
  77. package/build/phase-context-git-probe.js +68 -0
  78. package/build/phase-result-artifacts.js +173 -5
  79. package/build/pipelines.generated.js +19 -25
  80. package/build/plane/alembic-head.js +12 -3
  81. package/build/plane/base-worktree-guard.js +147 -0
  82. package/build/plane/cli.js +217 -17
  83. package/build/plane/code-snapshot.js +468 -0
  84. package/build/plane/defaults.js +48 -3
  85. package/build/plane/endpoint.js +41 -0
  86. package/build/plane/manifest.js +120 -1
  87. package/build/plane/member-roster.js +94 -19
  88. package/build/plane/preflight.js +30 -2
  89. package/build/plane/process-cwd.js +67 -0
  90. package/build/plane/restart.js +542 -0
  91. package/build/plane/shutdown.js +31 -0
  92. package/build/plane/status.js +145 -6
  93. package/build/plane/supervisor.js +588 -177
  94. package/build/plane/types.js +54 -2
  95. package/build/plane/worker-exit-code.js +2 -0
  96. package/build/printed-remedy-commands.js +37 -0
  97. package/build/project-root.js +99 -0
  98. package/build/readiness-check.js +1 -1
  99. package/build/readme.generated.js +1 -1
  100. package/build/review-generation.js +22 -0
  101. package/build/review-tickets.js +156 -13
  102. package/build/setup-epic.js +224 -127
  103. package/build/sfcc/config.js +9 -0
  104. package/build/sfcc/credentials.js +100 -38
  105. package/build/sfcc/hostname.js +67 -0
  106. package/build/sfcc/permissions.js +17 -13
  107. package/build/sfcc/register.js +1 -1
  108. package/build/sfcc/setup-status.js +2 -0
  109. package/build/sfcc/tool-wrapper.js +41 -43
  110. package/build/sfcc/tools-configured.js +96 -0
  111. package/build/sfcc/write-grants.js +17 -11
  112. package/build/sfcc/write-result.js +4 -3
  113. package/build/sfcc/writes-site-preference.js +20 -8
  114. package/build/start-tickets.js +332 -79
  115. package/build/tool-error-envelope.js +61 -0
  116. package/build/upgrade-cli.js +109 -24
  117. package/build/version.generated.js +3 -3
  118. package/build/visual-diff.js +22 -1
  119. package/build/windows-npm-entry.js +103 -0
  120. package/build/worker-guard-hook-bin.js +3 -5
  121. package/build/worker-guard-hook-main.js +47 -0
  122. package/build/worktree-core.js +136 -9
  123. package/docs/EPIC-IMPLEMENTER-INSTALL.md +849 -0
  124. package/docs/EPIC-IMPLEMENTER.md +662 -0
  125. package/docs/install/mcp-tool-integrations.md +25 -15
  126. package/docs/install/sfcc-integration.md +21 -8
  127. package/package.json +1 -1
  128. package/pipelines/implement-ticket.json +1 -1
  129. package/pipelines/plan-epic.json +0 -5
  130. package/pipelines/review-ticket.json +1 -1
  131. package/docs/CONDUCTOR.md +0 -605
  132. package/pipelines/idea-to-pr.json +0 -49
@@ -1,10 +1,14 @@
1
1
  /**
2
- * `setup-epic` — one-command bootstrap for an Epic Conductor v2 run.
2
+ * `setup-epic` — one-command bootstrap for an Epic Implementer run.
3
3
  *
4
4
  * Creates the epic run, stores the plan DAG, and approves it. Before this
5
- * existed the only path was hand-rolled HTTP with a hand-computed plan hash,
6
- * which is why the shipped `store-and-approve-epic-plan` pipeline step told the
7
- * agent to "make a direct API call" — there was no tool to call.
5
+ * existed the only path was hand-rolled HTTP with a hand-computed plan hash —
6
+ * there was no tool to call.
7
+ *
8
+ * This is a separate run-creation workflow, not a `/plan-epic` pipeline step:
9
+ * `/plan-epic` authors the local sidecar and stops. An operator creates the child
10
+ * tickets, finalizes the plan, and then launches through the gated `drive-epic`
11
+ * entry point, which is what reaches this command.
8
12
  *
9
13
  * Ordering is load-bearing:
10
14
  *
@@ -78,14 +82,16 @@
78
82
  import { readFile as fsReadFile, stat as fsStat } from "node:fs/promises";
79
83
  import os from "node:os";
80
84
  import { createProductionEpicIntegrationGhRunner, ensureEpicIntegrationPullRequest, formatEpicIntegrationPullRequestOutcome, } from "./epic-integration-pr.js";
81
- import { approveEpicPlan, buildConductorJiraUrl, conductorGetHeaders, createEpicRunWithDisposition, decodeCanonicalProvenance, fetchConductorReadiness, fetchEpicRunState, replaceEpicRunPolicy, resolveConductorBridgeApiAccess, resolveEpicLaunchBridgeApiAccess, storeEpicPlan, validateEpicPlan, ConductorBridgeApiError, } from "./conductor/bridge-api-client.js";
85
+ import { approveEpicPlan, buildConductorJiraUrl, conductorGetHeaders, createEpicRunWithDisposition, decodeCanonicalProvenance, fetchConductorReadiness, fetchEpicRunState, replaceEpicRunPolicy, resolveConductorBridgeApiAccess, resolveEpicLaunchBridgeApiAccess, safeDiagnosticMessage, storeEpicPlan, validateEpicPlan, ConductorBridgeApiError, } from "./conductor/bridge-api-client.js";
86
+ import { formatEpicLaunchChannelIdentity, getPinnedEpicLaunchPolicy, readEpicLaunchProfile, } from "./epic-launch-profile.js";
82
87
  // BAPI-1133 (A2) — the single legacy-to-typed merge-execution-mode mapping and
83
88
  // its total legacy resolver live in `merge-pull-request.ts` (no import cycle:
84
89
  // that module never imports from `setup-epic.ts`). Reused here rather than
85
90
  // reimplemented so the mapping stays single-defined.
86
91
  import { mapLegacyMergeExecutionModeToPolicyMode, resolveMergeExecutionMode, } from "./merge-pull-request.js";
87
- import { MERGE_EXECUTION_INELIGIBLE, MERGE_EXECUTION_MODE_GITHUB_ACTIONS, MERGE_WORKFLOW_REMEDY_COMMAND, } from "./conductor/merge-execution-contract.js";
92
+ import { MERGE_EXECUTION_INELIGIBLE, MERGE_EXECUTION_MODE_GITHUB_ACTIONS, MERGE_WORKFLOW_CODEOWNER_INSTRUCTION, MERGE_WORKFLOW_REMEDY_COMMAND, } from "./conductor/merge-execution-contract.js";
88
93
  import { classifyMergeExecutionEligibility } from "./conductor/merge-execution-eligibility.js";
94
+ import { repositoryWorkflowRemedy } from "./conductor/repository-workflow-remedy.js";
89
95
  import { resolveTrackedPaths } from "./git-reachability.js";
90
96
  import { validateBranchName } from "./base-ref.js";
91
97
  // Reused from the implement-epic client on purpose: adding a second wrapper under
@@ -99,6 +105,7 @@ import { validateBranchName } from "./base-ref.js";
99
105
  import { getConfigFieldBaseBranch, getIndexScopeLifecycle, getParseStatus, } from "./epic-shared/bridge-client.js";
100
106
  import { boundScopeLifecycleLabel, createExecFileRunCommand, ensureCommitResolvableLocally, performExactIndexScopeCut, pollIndexScopeLifecycle, readRemoteBranchHead, runGit, SCOPE_BOOTSTRAP_POLL_INTERVAL_MS, SCOPE_LIFECYCLE_LABELS, } from "./epic-shared/cut-protocol.js";
101
107
  import { hashPlan } from "./conductor/plan.js";
108
+ import { PLACEHOLDER_NODE_KEY_RE } from "./conductor-bundle-artifacts.js";
102
109
  import { BRING_UP_REFUSAL_MESSAGES, firstBringUpRefusal, } from "./conductor/bring-up-facts.js";
103
110
  import { MCP_PACKAGE_NAME } from "./mcp-identity.js";
104
111
  // BAPI-872: local plane-manifest binding, so `plane down` can later stop the
@@ -189,7 +196,7 @@ export function getSetupEpicUsage() {
189
196
  return [
190
197
  "Usage: mcp-server setup-epic --epic-key <KEY> --plan-file <path> [options]",
191
198
  "",
192
- "Bootstraps an Epic Conductor v2 run: creates the run, stores the plan DAG,",
199
+ "Bootstraps an Epic Implementer run: creates the run, stores the plan DAG,",
193
200
  "and approves it. Idempotent — re-running reuses an existing live run.",
194
201
  "",
195
202
  "Branch strategy (default): a plan with TWO OR MORE nodes runs on a dedicated",
@@ -209,11 +216,12 @@ export function getSetupEpicUsage() {
209
216
  "that selects no epic branch (--into-base, or a single-node plan) is unchanged",
210
217
  "and composes no posture at all.",
211
218
  "",
212
- "Unattended runs have TWO repository prerequisites, refused by name before any",
213
- "run is created: repository consent (ON by default; the repository owner can",
219
+ "Unattended runs have THREE repository prerequisites, refused by name before",
220
+ "any run is created: repository consent (ON by default; the repository owner can",
214
221
  "switch 'Allow unattended Epic Implementer runs' off and on in the Bridge admin",
215
- "panel's Setup page) and a verified notify webhook default. `drive-epic`",
216
- "readiness lists both with their current state.",
222
+ "panel's Setup page); an escalation channel, either a verified notify webhook or",
223
+ "the account's email address; and confirmed review and CI workflows on the",
224
+ "default branch. `drive-epic` readiness lists all three with their current state.",
217
225
  "",
218
226
  "Required:",
219
227
  " --epic-key <KEY> Jira epic key (e.g. BAPI-405)",
@@ -278,7 +286,7 @@ export function getSetupEpicUsage() {
278
286
  " 10799; remediate / ci_fix 3599; rebase / smoke 2699;",
279
287
  " merge 1799); the server rejects a higher value with a",
280
288
  " 422 naming the offending field. Full table:",
281
- " docs/claude/epic-implementer-v2-operator-runbook.md §4.",
289
+ " docs/epic-implementer/operator-runbook.md §4.",
282
290
  " --replace-policy Authorize replacing a LIVE run's stored policy with the",
283
291
  " policy file, as a complete replacement. Without it, a",
284
292
  " divergent policy on a reused run is refused with a",
@@ -296,8 +304,8 @@ export function getSetupEpicUsage() {
296
304
  " never a substitute for authoritative validation.",
297
305
  "",
298
306
  "After setup, the server-side reconciler picks the run up within ~30s.",
299
- "To execute claimed jobs on this machine, run:",
300
- ` npx -y ${MCP_PACKAGE_NAME} executor --repo <name>`,
307
+ "To execute this run's claimed jobs on this machine, run:",
308
+ ` npx -y ${MCP_PACKAGE_NAME} executor --repo <name> --epic-run-id <run-id>`,
301
309
  ].join("\n");
302
310
  }
303
311
  function takeValue(argv, i, flag) {
@@ -987,6 +995,20 @@ export function validateEpicPlanSidecar(parsed) {
987
995
  `to get that scheduling protection (use [] when no files are predicted).`);
988
996
  }
989
997
  }
998
+ // BAPI-1366: refuse unfinalized `TBD-N` node keys before any graph, access, or
999
+ // network work. `/plan-epic` authors placeholders; finalization replaces them.
1000
+ // This runs here, not in the server lane, so an updated client refuses even
1001
+ // against an older server and in `--dry-run --local-only`.
1002
+ const placeholders = [...keys].filter((key) => PLACEHOLDER_NODE_KEY_RE.test(key)).sort();
1003
+ if (placeholders.length > 0) {
1004
+ return {
1005
+ ok: false,
1006
+ error: `Plan node keys ${placeholders.join(", ")} are placeholders, not tickets. ` +
1007
+ `Create the child tickets and run \`emit-epic-implementer-bundle finalize\` ` +
1008
+ `to replace every placeholder, then run this again (this invocation created ` +
1009
+ `no run, cut no branch, and incurred no charge).`,
1010
+ };
1011
+ }
990
1012
  // Build one adjacency in a consistent predecessor -> successor direction, the
991
1013
  // same way the server does. `depends_on` points at the predecessor; an edge's
992
1014
  // `from` IS the predecessor.
@@ -1117,6 +1139,24 @@ function findCycle(keys, adjacency) {
1117
1139
  }
1118
1140
  return null;
1119
1141
  }
1142
+ export function effectivePolicyFacts(policy, source) {
1143
+ const object = policy && typeof policy === "object" && !Array.isArray(policy)
1144
+ ? policy : {};
1145
+ const numbers = (value) => {
1146
+ if (!value || typeof value !== "object" || Array.isArray(value))
1147
+ return null;
1148
+ const entries = Object.entries(value).filter(([key, item]) => /^[a-z][a-z0-9_]{0,63}$/.test(key) && typeof item === "number" && Number.isFinite(item));
1149
+ return entries.length ? Object.fromEntries(entries) : null;
1150
+ };
1151
+ return {
1152
+ source,
1153
+ serial_execution: "serial",
1154
+ posture: object.posture === "unattended" ? "unattended" : "attended",
1155
+ caps: numbers(object.caps),
1156
+ job_timeouts: numbers(object.job_timeouts),
1157
+ escalation_channel: formatEpicLaunchChannelIdentity(source === "stored" ? "unavailable" : policyDeclaresNotifyWebhook(object) ? "configured webhook" : "account email"),
1158
+ };
1159
+ }
1120
1160
  /** Propose a conductor epic feature branch name for an epic key (BAPI-655). */
1121
1161
  export function proposeFeatureBranchName(epicKey) {
1122
1162
  return `epic/${epicKey}`;
@@ -1274,12 +1314,13 @@ export function evaluateSetupEpicUnattendedPrerequisites(readiness, requestedPol
1274
1314
  };
1275
1315
  }
1276
1316
  if (!unattended.repository_readiness_confirmed) {
1317
+ // BAPI-1359: the shared formatter names the specific missing item; the reason
1318
+ // code and the trailing sentence are unchanged.
1277
1319
  return {
1278
1320
  reason: "repository_readiness_unconfirmed",
1279
1321
  message: "the unattended default authorizes automatic merge, which requires the review and " +
1280
- "conductor-CI workflows to be CONFIRMED on the default branch, and they are not. " +
1281
- "Run `install-bridge conductor` to install them, or pass --attended to create an " +
1282
- "attended run. No run was created.",
1322
+ "epic-implementer-ci workflows to be CONFIRMED on the default branch, and they are not. " +
1323
+ `${repositoryWorkflowRemedy(readiness)} No run was created.`,
1283
1324
  };
1284
1325
  }
1285
1326
  return null;
@@ -1352,8 +1393,10 @@ export function evaluateSetupEpicMergeExecutionPrerequisites(readiness, requeste
1352
1393
  reason: MERGE_EXECUTION_INELIGIBLE,
1353
1394
  message: "this run declares merge_execution_mode: github_actions, and the Bridge API " +
1354
1395
  "conductor readiness report could not be read, so the repository's merge " +
1355
- "capability could not be confirmed. Retry once the server is reachable, or " +
1356
- `resolve it with \`${MERGE_WORKFLOW_REMEDY_COMMAND}\`. ${NO_RUN_WAS_CREATED}`,
1396
+ "capability could not be confirmed. Retry once the server is reachable. If " +
1397
+ "readiness then reports the merge workflow missing or out of date, resolve it " +
1398
+ `with \`${MERGE_WORKFLOW_REMEDY_COMMAND}\`. ${MERGE_WORKFLOW_CODEOWNER_INSTRUCTION} ` +
1399
+ NO_RUN_WAS_CREATED,
1357
1400
  };
1358
1401
  }
1359
1402
  const verdict = classifyMergeExecutionEligibility(readiness);
@@ -1366,7 +1409,8 @@ export function evaluateSetupEpicMergeExecutionPrerequisites(readiness, requeste
1366
1409
  reason: MERGE_EXECUTION_INELIGIBLE,
1367
1410
  message: "this run declares merge_execution_mode: github_actions, and this repository's " +
1368
1411
  `merge capability ${category}: ${verdict.detail}. Resolve it with ` +
1369
- `\`${MERGE_WORKFLOW_REMEDY_COMMAND}\`, then re-run. ${NO_RUN_WAS_CREATED}`,
1412
+ `\`${MERGE_WORKFLOW_REMEDY_COMMAND}\`, then re-run. ` +
1413
+ `${MERGE_WORKFLOW_CODEOWNER_INSTRUCTION} ${NO_RUN_WAS_CREATED}`,
1370
1414
  };
1371
1415
  }
1372
1416
  /**
@@ -1561,12 +1605,7 @@ composesDefaultPolicy = true) {
1561
1605
  `(pass --feature-branch to force ${proposeFeatureBranchName(opts.epicKey)} anyway)`);
1562
1606
  }
1563
1607
  function errorDetail(err) {
1564
- if (err instanceof ConductorBridgeApiError) {
1565
- const status = err.status !== undefined ? ` (HTTP ${err.status})` : "";
1566
- const preview = err.bodyPreview ? `: ${err.bodyPreview}` : "";
1567
- return `${err.message}${status}${preview}`;
1568
- }
1569
- return err instanceof Error ? err.message : String(err);
1608
+ return safeDiagnosticMessage(err, "operation failed");
1570
1609
  }
1571
1610
  /**
1572
1611
  * Reconcile a policy file against a REUSED run's stored policy, applying it only
@@ -1588,11 +1627,13 @@ async function applyReusedRunPolicy(args) {
1588
1627
  });
1589
1628
  if (outcome.kind === "noop") {
1590
1629
  args.result.policy_applied = "noop";
1630
+ args.result.effective_policy = effectivePolicyFacts(args.storedPolicy, "stored");
1591
1631
  args.say("Policy: policy unchanged");
1592
1632
  return true;
1593
1633
  }
1594
1634
  if (outcome.kind === "refused_divergent") {
1595
1635
  args.result.policy_applied = "refused_divergent";
1636
+ args.result.effective_policy = effectivePolicyFacts(args.storedPolicy, "stored");
1596
1637
  args.deps.errorLog(`Epic run ${args.epicRunId} is live and its stored policy differs from the ` +
1597
1638
  `policy file. setup-epic will not silently change a running epic's policy.\n` +
1598
1639
  `${outcome.diffLines.join("\n")}\n` +
@@ -1605,10 +1646,12 @@ async function applyReusedRunPolicy(args) {
1605
1646
  }
1606
1647
  catch (err) {
1607
1648
  args.result.policy_applied = "refused_divergent";
1649
+ args.result.effective_policy = effectivePolicyFacts(args.storedPolicy, "stored");
1608
1650
  args.deps.errorLog(`Failed to replace the run policy: ${errorDetail(err)}`);
1609
1651
  return false;
1610
1652
  }
1611
1653
  args.result.policy_applied = "replaced";
1654
+ args.result.effective_policy = effectivePolicyFacts(args.requestedPolicy, "explicit");
1612
1655
  args.say("Policy: policy applied: replaced");
1613
1656
  for (const line of outcome.diffLines)
1614
1657
  args.say(line);
@@ -1860,12 +1903,13 @@ function describeScopeTransition(state, status) {
1860
1903
  * {@link runSetupEpicCli} in every observable way — same output, same refusals,
1861
1904
  * same exit codes — and differs only in what it hands back.
1862
1905
  */
1863
- export async function runSetupEpicWorkflow(argv, overrides = {}) {
1906
+ export async function runSetupEpicWorkflow(argv, overrides = {}, handoff) {
1864
1907
  const sink = {};
1865
- const exitCode = await executeSetupEpic(argv, overrides, sink);
1908
+ const exitCode = await executeSetupEpic(argv, overrides, sink, handoff);
1866
1909
  return {
1867
1910
  exitCode,
1868
1911
  ...(sink.epicRunId !== undefined ? { epicRunId: sink.epicRunId } : {}),
1912
+ ...(sink.epicRunId !== undefined ? { runExists: true } : {}),
1869
1913
  };
1870
1914
  }
1871
1915
  /**
@@ -1875,7 +1919,7 @@ export async function runSetupEpicWorkflow(argv, overrides = {}) {
1875
1919
  export async function runSetupEpicCli(argv, overrides = {}) {
1876
1920
  return (await runSetupEpicWorkflow(argv, overrides)).exitCode;
1877
1921
  }
1878
- async function executeSetupEpic(argv, overrides, runIdSink) {
1922
+ async function executeSetupEpic(argv, overrides, runIdSink, handoff) {
1879
1923
  const deps = { ...createDefaultSetupEpicDeps(), ...overrides };
1880
1924
  const parsed = parseSetupEpicArgs(argv);
1881
1925
  if (parsed.status === "help") {
@@ -1891,6 +1935,14 @@ async function executeSetupEpic(argv, overrides, runIdSink) {
1891
1935
  const opts = parsed.options;
1892
1936
  // With --json, stdout carries the single result object and nothing else.
1893
1937
  const say = opts.json ? deps.errorLog : deps.log;
1938
+ if (handoff && (handoff.access.baseUrl !== handoff.selection.baseUrl ||
1939
+ handoff.access.repoName !== handoff.selection.repoName ||
1940
+ (opts.baseUrl !== undefined && opts.baseUrl !== handoff.selection.baseUrl) ||
1941
+ (opts.repo !== undefined && opts.repo !== handoff.selection.repoName) ||
1942
+ (opts.launchProfile !== undefined && opts.launchProfile !== handoff.selection.profileName))) {
1943
+ deps.errorLog("setup-epic REFUSED (selection-conflict): delegated target or repository disagrees with the pinned launch selection.");
1944
+ return 1;
1945
+ }
1894
1946
  // --- Read + validate the plan sidecar (no network yet) -------------------
1895
1947
  let raw;
1896
1948
  try {
@@ -1922,18 +1974,137 @@ async function executeSetupEpic(argv, overrides, runIdSink) {
1922
1974
  return 1;
1923
1975
  }
1924
1976
  const localHash = hashPlan(plan);
1977
+ // Resolve the selected target and live-run identity before choosing a policy.
1978
+ // The offline local-only path keeps its no-network contract.
1979
+ let access;
1980
+ let selectedProfilePolicyPath = handoff?.profile?.policyRef?.path;
1981
+ if (opts.localOnly && !handoff) {
1982
+ const profileName = opts.launchProfile ?? deps.env.BAPI_EPIC_LAUNCH_PROFILE?.trim();
1983
+ if (profileName) {
1984
+ const profileRead = await readEpicLaunchProfile(profileName, {
1985
+ env: deps.env,
1986
+ homedir: deps.homedir,
1987
+ readFile: deps.readFile,
1988
+ host: os.hostname(),
1989
+ });
1990
+ if (!profileRead.ok) {
1991
+ deps.errorLog(profileRead.message);
1992
+ return 1;
1993
+ }
1994
+ selectedProfilePolicyPath = profileRead.profile.policyRef?.path;
1995
+ }
1996
+ }
1997
+ let existingRunId = null;
1998
+ let existingStatus = null;
1999
+ let existingBaseBranch = null;
2000
+ let existingReviewPolicy = null;
2001
+ // BAPI-754: the live run's SERVER-REDACTED stored policy, kept for the reuse
2002
+ // comparison. Redacted is the only form a client can ever see.
2003
+ let existingPolicyJson = null;
2004
+ if (!opts.localOnly) {
2005
+ // --- Resolve access ------------------------------------------------------
2006
+ // BAPI-1260: a caller that selected the epic-launch contract (drive-epic
2007
+ // forwards --launch-profile / --base-url) gets the target-first resolver and
2008
+ // the bound credential; the legacy resolver is kept byte-for-byte otherwise.
2009
+ const accessDeps = {
2010
+ env: deps.env,
2011
+ cwd: deps.cwd,
2012
+ homedir: deps.homedir,
2013
+ platform: deps.platform,
2014
+ readFile: deps.readFile,
2015
+ stat: deps.stat,
2016
+ repoName: opts.repo,
2017
+ };
2018
+ const accessResult = handoff
2019
+ ? { ok: true, access: handoff.access, profile: handoff.profile }
2020
+ : opts.launchProfile !== undefined || opts.baseUrl !== undefined || Boolean(deps.env.BAPI_EPIC_LAUNCH_PROFILE?.trim())
2021
+ ? await resolveEpicLaunchBridgeApiAccess({
2022
+ ...(opts.launchProfile ? { profileName: opts.launchProfile } : {}),
2023
+ ...(opts.baseUrl ? { baseUrl: opts.baseUrl } : {}),
2024
+ }, accessDeps)
2025
+ : await resolveConductorBridgeApiAccess(accessDeps);
2026
+ if (!accessResult.ok) {
2027
+ deps.errorLog(`Cannot reach the Bridge API: ${accessResult.error}`);
2028
+ return 1;
2029
+ }
2030
+ access = accessResult.access;
2031
+ say(`Epic: ${opts.epicKey}`);
2032
+ say(`Repo: ${access.repoName}`);
2033
+ say(`Plan: v${plan.plan_version}, ${plan.nodes.length} node(s), ${plan.edges.length} edge(s)`);
2034
+ say(`Local hash: ${localHash}`);
2035
+ for (const w of warnings)
2036
+ say(` [warn] ${w}`);
2037
+ selectedProfilePolicyPath = "profile" in accessResult
2038
+ ? accessResult.profile?.policyRef?.path
2039
+ : undefined;
2040
+ // --- Step 0: pre-check ---------------------------------------------------
2041
+ // Never create on an ambiguous read. A wrong answer here mints a duplicate run.
2042
+ try {
2043
+ const state = await fetchEpicRunState(access, opts.epicKey, deps.fetch);
2044
+ existingRunId = state.epic_run?.epic_run_id ?? null;
2045
+ existingStatus = state.epic_run?.status ?? null;
2046
+ const existingPolicy = state.epic_run?.policy_json;
2047
+ existingPolicyJson = existingPolicy ?? null;
2048
+ const existingBase = existingPolicy && typeof existingPolicy === "object"
2049
+ ? existingPolicy.base_branch
2050
+ : undefined;
2051
+ existingBaseBranch = typeof existingBase === "string" && existingBase.trim() !== ""
2052
+ ? existingBase
2053
+ : null;
2054
+ // BAPI-679: read the existing run's review policy for the conflict guard
2055
+ // below. Absent/malformed reads as null — "the resolver's default applies" —
2056
+ // which is deliberately NOT the same as an explicitly selected source.
2057
+ const existingReview = existingPolicy && typeof existingPolicy === "object"
2058
+ ? existingPolicy.review_policy
2059
+ : undefined;
2060
+ const existingReviewSource = existingReview && typeof existingReview === "object"
2061
+ ? existingReview.source
2062
+ : undefined;
2063
+ existingReviewPolicy =
2064
+ typeof existingReviewSource === "string" && existingReviewSource.trim() !== ""
2065
+ ? existingReviewSource
2066
+ : null;
2067
+ }
2068
+ catch (err) {
2069
+ if (err instanceof ConductorBridgeApiError && err.status === 404) {
2070
+ existingRunId = null; // No live run — the normal first-run path.
2071
+ }
2072
+ else if (err instanceof ConductorBridgeApiError && err.status === 409) {
2073
+ deps.errorLog(`Epic ${opts.epicKey} has MULTIPLE active runs — it is wedged, and every ` +
2074
+ `plan call will keep failing. Abandon the duplicate before retrying:\n` +
2075
+ ` epic-implementer abandon-run --epic-run-id <duplicate_epic_run_id>\n` +
2076
+ `Detail: ${errorDetail(err)}`);
2077
+ return 1;
2078
+ }
2079
+ else {
2080
+ deps.errorLog(`Could not read the epic run state, so creating one would risk a duplicate ` +
2081
+ `(which wedges the epic permanently). Refusing to continue.\n` +
2082
+ `Detail: ${errorDetail(err)}`);
2083
+ return 1;
2084
+ }
2085
+ }
2086
+ }
1925
2087
  // --- Read the policy file (still no network) ------------------------------
1926
2088
  // BAPI-754: parsed here so a malformed file, or a contradiction between the
1927
2089
  // file and the CLI flags, fails before credentials are resolved or any request
1928
2090
  // is dispatched.
1929
2091
  let filePolicy;
1930
- if (opts.policyFile !== undefined) {
1931
- const policyRead = await readSetupEpicPolicyFile(opts.policyFile, deps.readFile);
1932
- if (!policyRead.ok) {
1933
- deps.errorLog(policyRead.error);
1934
- return 1;
2092
+ const selectedPolicyPath = opts.policyFile ?? (existingRunId === null ? selectedProfilePolicyPath : undefined);
2093
+ const selectedPolicySource = existingRunId !== null
2094
+ ? "stored" : opts.policyFile !== undefined ? "explicit" : selectedProfilePolicyPath !== undefined ? "profile" : "default";
2095
+ if (selectedPolicyPath !== undefined) {
2096
+ const pinned = handoff ? getPinnedEpicLaunchPolicy(handoff.selection) : undefined;
2097
+ if (pinned !== undefined) {
2098
+ filePolicy = pinned;
2099
+ }
2100
+ else {
2101
+ const policyRead = await readSetupEpicPolicyFile(selectedPolicyPath, deps.readFile);
2102
+ if (!policyRead.ok) {
2103
+ deps.errorLog(policyRead.error);
2104
+ return 1;
2105
+ }
2106
+ filePolicy = policyRead.policy;
1935
2107
  }
1936
- filePolicy = policyRead.policy;
1937
2108
  }
1938
2109
  const policyDeclaresBranch = filePolicy !== undefined &&
1939
2110
  ["base_branch", "baseBranch"].some((key) => key in filePolicy && filePolicy[key] !== undefined && filePolicy[key] !== null);
@@ -1963,7 +2134,7 @@ async function executeSetupEpic(argv, overrides, runIdSink) {
1963
2134
  const policyGate = preflightSetupEpicPolicy({
1964
2135
  intoBase: opts.intoBase,
1965
2136
  attended: opts.attended,
1966
- policyFile: opts.policyFile,
2137
+ policyFile: selectedPolicyPath,
1967
2138
  filePolicy,
1968
2139
  policyDeclaresBranch,
1969
2140
  featureBranch,
@@ -2001,42 +2172,14 @@ async function executeSetupEpic(argv, overrides, runIdSink) {
2001
2172
  plan_version: plan.plan_version,
2002
2173
  local_plan_hash: localHash,
2003
2174
  server_validated: false,
2175
+ feature_branch: opts.intoBase ? null : resolveEffectiveFeatureBranch(featureBranch, policyGate.policy) ?? null,
2004
2176
  policy_applied: "absent",
2177
+ effective_policy: effectivePolicyFacts(policyGate.policy, selectedPolicySource),
2005
2178
  warnings,
2006
2179
  }, null, 2));
2007
2180
  }
2008
2181
  return 0;
2009
2182
  }
2010
- // --- Resolve access ------------------------------------------------------
2011
- // BAPI-1260: a caller that selected the epic-launch contract (drive-epic
2012
- // forwards --launch-profile / --base-url) gets the target-first resolver and
2013
- // the bound credential; the legacy resolver is kept byte-for-byte otherwise.
2014
- const accessDeps = {
2015
- env: deps.env,
2016
- cwd: deps.cwd,
2017
- homedir: deps.homedir,
2018
- platform: deps.platform,
2019
- readFile: deps.readFile,
2020
- stat: deps.stat,
2021
- repoName: opts.repo,
2022
- };
2023
- const accessResult = opts.launchProfile !== undefined || opts.baseUrl !== undefined
2024
- ? await resolveEpicLaunchBridgeApiAccess({
2025
- ...(opts.launchProfile ? { profileName: opts.launchProfile } : {}),
2026
- ...(opts.baseUrl ? { baseUrl: opts.baseUrl } : {}),
2027
- }, accessDeps)
2028
- : await resolveConductorBridgeApiAccess(accessDeps);
2029
- if (!accessResult.ok) {
2030
- deps.errorLog(`Cannot reach the Bridge API: ${accessResult.error}`);
2031
- return 1;
2032
- }
2033
- const access = accessResult.access;
2034
- say(`Epic: ${opts.epicKey}`);
2035
- say(`Repo: ${access.repoName}`);
2036
- say(`Plan: v${plan.plan_version}, ${plan.nodes.length} node(s), ${plan.edges.length} edge(s)`);
2037
- say(`Local hash: ${localHash}`);
2038
- for (const w of warnings)
2039
- say(` [warn] ${w}`);
2040
2183
  // --- Branch strategy (BAPI-1009) ----------------------------------------
2041
2184
  // Already resolved above, before the network was touched. Reported HERE, ahead
2042
2185
  // of any secondary policy detail, because it is the one decision that changes
@@ -2093,6 +2236,11 @@ async function executeSetupEpic(argv, overrides, runIdSink) {
2093
2236
  reviewPolicy: opts.reviewPolicy,
2094
2237
  mergeExecutionMode: declaredMergeExecutionMode,
2095
2238
  });
2239
+ const selectedEffectivePolicyFacts = effectivePolicyFacts(existingRunId !== null ? existingPolicyJson : composedForCreate, selectedPolicySource);
2240
+ say(`Policy: effective ${selectedEffectivePolicyFacts.source}; serial execution; ` +
2241
+ `caps ${selectedEffectivePolicyFacts.caps ? JSON.stringify(selectedEffectivePolicyFacts.caps) : "unavailable"}; ` +
2242
+ `timeouts ${selectedEffectivePolicyFacts.job_timeouts ? JSON.stringify(selectedEffectivePolicyFacts.job_timeouts) : "unavailable"}; ` +
2243
+ `channel ${selectedEffectivePolicyFacts.escalation_channel}`);
2096
2244
  // BAPI-1133 (A2). Reported only when THIS command stamped the key — i.e. it
2097
2245
  // is present in the composed policy and was absent from whatever the
2098
2246
  // operator supplied — as low-emphasis policy metadata, mirroring the
@@ -2128,59 +2276,6 @@ async function executeSetupEpic(argv, overrides, runIdSink) {
2128
2276
  say(`Feature: ${effectiveFeatureBranch} (from the policy file; cut on origin with local ` +
2129
2277
  `git at the canonical indexed commit; validated server-side at approval)`);
2130
2278
  }
2131
- // --- Step 0: pre-check ---------------------------------------------------
2132
- // Never create on an ambiguous read. A wrong answer here mints a duplicate run.
2133
- let existingRunId = null;
2134
- let existingStatus = null;
2135
- let existingBaseBranch = null;
2136
- let existingReviewPolicy = null;
2137
- // BAPI-754: the live run's SERVER-REDACTED stored policy, kept for the reuse
2138
- // comparison. Redacted is the only form a client can ever see.
2139
- let existingPolicyJson = null;
2140
- try {
2141
- const state = await fetchEpicRunState(access, opts.epicKey, deps.fetch);
2142
- existingRunId = state.epic_run?.epic_run_id ?? null;
2143
- existingStatus = state.epic_run?.status ?? null;
2144
- const existingPolicy = state.epic_run?.policy_json;
2145
- existingPolicyJson = existingPolicy ?? null;
2146
- const existingBase = existingPolicy && typeof existingPolicy === "object"
2147
- ? existingPolicy.base_branch
2148
- : undefined;
2149
- existingBaseBranch = typeof existingBase === "string" && existingBase.trim() !== ""
2150
- ? existingBase
2151
- : null;
2152
- // BAPI-679: read the existing run's review policy for the conflict guard
2153
- // below. Absent/malformed reads as null — "the resolver's default applies" —
2154
- // which is deliberately NOT the same as an explicitly selected source.
2155
- const existingReview = existingPolicy && typeof existingPolicy === "object"
2156
- ? existingPolicy.review_policy
2157
- : undefined;
2158
- const existingReviewSource = existingReview && typeof existingReview === "object"
2159
- ? existingReview.source
2160
- : undefined;
2161
- existingReviewPolicy =
2162
- typeof existingReviewSource === "string" && existingReviewSource.trim() !== ""
2163
- ? existingReviewSource
2164
- : null;
2165
- }
2166
- catch (err) {
2167
- if (err instanceof ConductorBridgeApiError && err.status === 404) {
2168
- existingRunId = null; // No live run — the normal first-run path.
2169
- }
2170
- else if (err instanceof ConductorBridgeApiError && err.status === 409) {
2171
- deps.errorLog(`Epic ${opts.epicKey} has MULTIPLE active runs — it is wedged, and every ` +
2172
- `plan call will keep failing. Abandon the duplicate before retrying:\n` +
2173
- ` epic-implementer abandon-run --epic-run-id <duplicate_epic_run_id>\n` +
2174
- `Detail: ${errorDetail(err)}`);
2175
- return 1;
2176
- }
2177
- else {
2178
- deps.errorLog(`Could not read the epic run state, so creating one would risk a duplicate ` +
2179
- `(which wedges the epic permanently). Refusing to continue.\n` +
2180
- `Detail: ${errorDetail(err)}`);
2181
- return 1;
2182
- }
2183
- }
2184
2279
  // --- Feature-branch conflict guard against an existing live run ----------
2185
2280
  // A selected branch that disagrees with the existing run's stored base branch
2186
2281
  // must fail closed rather than patch or silently change that run. A matching
@@ -2508,6 +2603,7 @@ async function executeSetupEpic(argv, overrides, runIdSink) {
2508
2603
  // "absent"; server_validated says whether the check was authoritative.
2509
2604
  server_validated: validated !== null,
2510
2605
  policy_applied: "absent",
2606
+ effective_policy: selectedEffectivePolicyFacts,
2511
2607
  ...(validated
2512
2608
  ? {
2513
2609
  server_plan_hash: validated.planHash,
@@ -2579,6 +2675,7 @@ async function executeSetupEpic(argv, overrides, runIdSink) {
2579
2675
  plan_approved: false,
2580
2676
  server_validated: validated !== null,
2581
2677
  policy_applied: "absent",
2678
+ effective_policy: selectedEffectivePolicyFacts,
2582
2679
  warnings,
2583
2680
  };
2584
2681
  // BAPI-850: the EFFECTIVE feature branch (flag or policy file), so a
@@ -3128,14 +3225,14 @@ async function executeSetupEpic(argv, overrides, runIdSink) {
3128
3225
  else {
3129
3226
  say("The server-side reconciler will pick it up within ~30s.");
3130
3227
  }
3131
- // BAPI-1102 — the executor command is printed WITH a claim scope. Since
3132
- // BAPI-1026 an executor started with no scope REFUSES TO START, so the
3133
- // unscoped form printed here was guidance that could not work: an operator
3134
- // who copied it got a startup error, and the natural reading of that error is
3135
- // that the run is broken rather than that the instruction was.
3136
- say("To execute this run's claimed jobs on this machine, run:");
3137
- say(` npx -y ${MCP_PACKAGE_NAME} executor --repo ${access.repoName} ` +
3138
- `--epic-run-id ${result.epic_run_id}`);
3228
+ if (handoff) {
3229
+ say("The launcher starts or reuses the executor appropriate for this run.");
3230
+ }
3231
+ else {
3232
+ say("To execute this run's claimed jobs on this machine, run:");
3233
+ say(` npx -y ${MCP_PACKAGE_NAME} executor --repo ${access.repoName} ` +
3234
+ `--epic-run-id ${result.epic_run_id}`);
3235
+ }
3139
3236
  }
3140
3237
  return exitCode;
3141
3238
  }
@@ -9,6 +9,15 @@
9
9
  export const SFCC_VERSIONS = ["sfra", "pwakit", "sitegenesis", "storefrontnext", "hybrid"];
10
10
  /** Pinned OCAPI Data API version used for all requests (configurable at call-time). */
11
11
  export const DEFAULT_OCAPI_VERSION = "v25_6";
12
+ /**
13
+ * The `_v` value of the OCAPI Settings JSON an operator pastes in Business
14
+ * Manager. This is the settings-STRUCTURE version, not the OCAPI API version
15
+ * above; Business Manager rejects an API version here.
16
+ *
17
+ * UNVERIFIED: "23.2" is provisional until owner check OR-1 saves the emitted
18
+ * grant in Business Manager. If OR-1 shows a different value, change it here.
19
+ */
20
+ export const OCAPI_SETTINGS_STRUCTURE_VERSION = "23.2";
12
21
  /** Account Manager OAuth2 token endpoint host. */
13
22
  export const AM_HOST = "account.demandware.com";
14
23
  /** AM OAuth2 token endpoint URL. */