@bridge_gpt/mcp-server 0.2.62 → 0.2.66

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 (113) hide show
  1. package/README.md +66 -15
  2. package/build/agent-launchers/claude-executor-adapter.js +145 -20
  3. package/build/agent-launchers/executor-adapter.js +81 -2
  4. package/build/agents.generated.js +10 -1
  5. package/build/bounded-wait.js +32 -1
  6. package/build/bridge-config.js +104 -0
  7. package/build/codex-bridge-profile.js +10 -1
  8. package/build/commands.generated.js +7 -6
  9. package/build/conductor/bridge-api-client.js +565 -32
  10. package/build/conductor/bring-up-facts.js +3 -1
  11. package/build/conductor/doctor.js +23 -14
  12. package/build/conductor/epic-child-continuation.js +351 -0
  13. package/build/conductor/epic-implementer-cli.js +154 -17
  14. package/build/conductor/git-hooks.js +529 -118
  15. package/build/conductor/guided-repair.js +323 -0
  16. package/build/conductor/hook-runtime-qualification.js +340 -0
  17. package/build/conductor/readiness-cli.js +30 -9
  18. package/build/conductor/readiness-sections.js +5 -4
  19. package/build/conductor/readiness.js +45 -12
  20. package/build/conductor/recovery-cli.js +859 -27
  21. package/build/conductor/recovery-operations.js +574 -5
  22. package/build/conductor/repair-contract.js +69 -3
  23. package/build/conductor-bundle-artifacts.js +1 -0
  24. package/build/credential-store.js +52 -13
  25. package/build/diagnostic-text.js +36 -0
  26. package/build/docs.generated.js +3 -2
  27. package/build/doctor.js +147 -21
  28. package/build/drive-epic-key.js +12 -0
  29. package/build/drive-epic.js +1537 -125
  30. package/build/epic-coherence-input-cli.js +137 -0
  31. package/build/epic-coherence-input.js +920 -0
  32. package/build/epic-coherence-input.test-fixtures.js +175 -0
  33. package/build/epic-implementer-bin.js +41 -33
  34. package/build/epic-implementer-bundle-cli.js +14 -1
  35. package/build/epic-launch-diagnostic.js +31 -0
  36. package/build/epic-launch-profile.js +58 -19
  37. package/build/epic-run-status.js +5 -0
  38. package/build/epic-shared/bridge-client.js +46 -4
  39. package/build/executor/cli.js +39 -0
  40. package/build/executor/completion-contract.js +423 -0
  41. package/build/executor/completion-supervision.js +291 -0
  42. package/build/executor/correction-dispatch.js +60 -0
  43. package/build/executor/credentials.js +3 -1
  44. package/build/executor/deps.js +65 -1
  45. package/build/executor/env.js +12 -0
  46. package/build/executor/heartbeat.js +13 -3
  47. package/build/executor/http-client.js +91 -4
  48. package/build/executor/integration-job.js +6 -1
  49. package/build/executor/job-errors.js +61 -4
  50. package/build/executor/job-log-registry.js +340 -73
  51. package/build/executor/job-runner.js +1550 -55
  52. package/build/executor/job-types.js +9 -6
  53. package/build/executor/preflight.js +189 -1
  54. package/build/executor/process-containment.js +756 -0
  55. package/build/executor/process.js +235 -5
  56. package/build/executor/prompt-spec.js +366 -1
  57. package/build/executor/provider-checks.js +69 -0
  58. package/build/executor/rebase-finalization.js +351 -0
  59. package/build/executor/reconcile-overlap-job.js +77 -84
  60. package/build/executor/required-verification.js +313 -0
  61. package/build/executor/required-work-ledger.js +1121 -0
  62. package/build/executor/results.js +142 -0
  63. package/build/executor/resume-pre-spawn.js +87 -4
  64. package/build/executor/runner.js +168 -0
  65. package/build/executor/stale-artifacts.js +5 -4
  66. package/build/executor/test-declaration.js +20 -0
  67. package/build/executor/types.js +13 -0
  68. package/build/executor/watch-cli.js +35 -8
  69. package/build/executor/worker-command.js +70 -34
  70. package/build/executor/worker-finalization.js +168 -26
  71. package/build/executor/worker-guard-hook.js +75 -14
  72. package/build/executor/worker-log.js +137 -70
  73. package/build/executor/worktree-gc.js +30 -6
  74. package/build/executor/worktree.js +13 -0
  75. package/build/executor-launch.js +658 -0
  76. package/build/index.js +594 -1152
  77. package/build/init.js +107 -4
  78. package/build/install-bridge-conductor.js +231 -41
  79. package/build/install-bridge.js +284 -20
  80. package/build/install-doctor.js +172 -30
  81. package/build/install-reexec.js +69 -7
  82. package/build/macos-terminal-permission-guidance.js +16 -0
  83. package/build/merge-pull-request.js +1 -1
  84. package/build/model-thresholds-tool.js +185 -0
  85. package/build/pipelines.generated.js +9 -8
  86. package/build/plan-discovery.js +137 -0
  87. package/build/plan-epic-implementer-eligibility.js +2 -2
  88. package/build/plane/cli.js +37 -3
  89. package/build/plane/member-logs.js +3 -0
  90. package/build/plane/member-roster.js +7 -1
  91. package/build/plane/shutdown.js +59 -1
  92. package/build/plane/status.js +4 -0
  93. package/build/plane/supervisor.js +146 -0
  94. package/build/plane/worker-exit-code.js +2 -0
  95. package/build/project-root.js +99 -0
  96. package/build/readme.generated.js +1 -1
  97. package/build/review-tickets.js +156 -13
  98. package/build/scripts/executor-protocol-contract-driver.js +13 -3
  99. package/build/setup-epic.js +208 -125
  100. package/build/sfcc/setup-status.js +2 -0
  101. package/build/sfcc/tool-wrapper.js +2 -0
  102. package/build/sfcc/tools-configured.js +96 -0
  103. package/build/start-tickets.js +163 -58
  104. package/build/upgrade-cli.js +109 -24
  105. package/build/version.generated.js +3 -3
  106. package/build/windows-npm-entry.js +103 -0
  107. package/build/worker-guard-hook-bin.js +5 -4
  108. package/build/worktree-core.js +6 -5
  109. package/docs/CONDUCTOR.md +116 -2
  110. package/docs/install/mcp-tool-integrations.md +1 -0
  111. package/docs/install/sfcc-integration.md +12 -7
  112. package/package.json +2 -1
  113. package/pipelines/implement-ticket.json +1 -1
@@ -78,7 +78,8 @@
78
78
  import { readFile as fsReadFile, stat as fsStat } from "node:fs/promises";
79
79
  import os from "node:os";
80
80
  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";
81
+ import { approveEpicPlan, buildConductorJiraUrl, conductorGetHeaders, createEpicRunWithDisposition, decodeCanonicalProvenance, fetchConductorReadiness, fetchEpicRunState, replaceEpicRunPolicy, resolveConductorBridgeApiAccess, resolveEpicLaunchBridgeApiAccess, safeDiagnosticMessage, storeEpicPlan, validateEpicPlan, ConductorBridgeApiError, } from "./conductor/bridge-api-client.js";
82
+ import { formatEpicLaunchChannelIdentity, getPinnedEpicLaunchPolicy, readEpicLaunchProfile, } from "./epic-launch-profile.js";
82
83
  // BAPI-1133 (A2) — the single legacy-to-typed merge-execution-mode mapping and
83
84
  // its total legacy resolver live in `merge-pull-request.ts` (no import cycle:
84
85
  // that module never imports from `setup-epic.ts`). Reused here rather than
@@ -209,10 +210,11 @@ export function getSetupEpicUsage() {
209
210
  "that selects no epic branch (--into-base, or a single-node plan) is unchanged",
210
211
  "and composes no posture at all.",
211
212
  "",
212
- "Unattended runs have TWO one-time repository prerequisites, refused by name",
213
- "before any run is created: repository consent (unattended_epic_implementer_allowed) and",
214
- "a verified notify webhook default. `drive-epic` readiness lists both with their",
215
- "current state.",
213
+ "Unattended runs have TWO repository prerequisites, refused by name before any",
214
+ "run is created: repository consent (ON by default; the repository owner can",
215
+ "switch 'Allow unattended Epic Implementer runs' off and on in the Bridge admin",
216
+ "panel's Setup page) and a verified notify webhook default. `drive-epic`",
217
+ "readiness lists both with their current state.",
216
218
  "",
217
219
  "Required:",
218
220
  " --epic-key <KEY> Jira epic key (e.g. BAPI-405)",
@@ -261,7 +263,8 @@ export function getSetupEpicUsage() {
261
263
  " --json Emit a single JSON result object on stdout",
262
264
  " --launch-profile <name> (BAPI-1260) Resolve the target, repository, and bound",
263
265
  " credential from this host-local launch profile. Passed",
264
- " by drive-epic; there is no production default with it.",
266
+ " by drive-epic. The profile names its own target; it",
267
+ " never falls back to production by itself.",
265
268
  " --base-url <url> (BAPI-1260) Explicit target; must agree with the profile.",
266
269
  " -h, --help Show this help",
267
270
  "",
@@ -1115,6 +1118,24 @@ function findCycle(keys, adjacency) {
1115
1118
  }
1116
1119
  return null;
1117
1120
  }
1121
+ export function effectivePolicyFacts(policy, source) {
1122
+ const object = policy && typeof policy === "object" && !Array.isArray(policy)
1123
+ ? policy : {};
1124
+ const numbers = (value) => {
1125
+ if (!value || typeof value !== "object" || Array.isArray(value))
1126
+ return null;
1127
+ const entries = Object.entries(value).filter(([key, item]) => /^[a-z][a-z0-9_]{0,63}$/.test(key) && typeof item === "number" && Number.isFinite(item));
1128
+ return entries.length ? Object.fromEntries(entries) : null;
1129
+ };
1130
+ return {
1131
+ source,
1132
+ serial_execution: "serial",
1133
+ posture: object.posture === "unattended" ? "unattended" : "attended",
1134
+ caps: numbers(object.caps),
1135
+ job_timeouts: numbers(object.job_timeouts),
1136
+ escalation_channel: formatEpicLaunchChannelIdentity(source === "stored" ? "unavailable" : policyDeclaresNotifyWebhook(object) ? "configured webhook" : "account email"),
1137
+ };
1138
+ }
1118
1139
  /** Propose a conductor epic feature branch name for an epic key (BAPI-655). */
1119
1140
  export function proposeFeatureBranchName(epicKey) {
1120
1141
  return `epic/${epicKey}`;
@@ -1235,10 +1256,10 @@ export function evaluateSetupEpicUnattendedPrerequisites(readiness, requestedPol
1235
1256
  if (!unattended.conductor_allowed) {
1236
1257
  return {
1237
1258
  reason: "target_not_allowed",
1238
- message: "this repository has not consented to unattended conductor runs. Set " +
1239
- "`unattended_epic_implementer_allowed` for it (see " +
1240
- "docs/claude/account-settings-operator-runbook.md), or pass --attended to " +
1241
- "create an attended run. No run was created.",
1259
+ message: "unattended runs are off for this repository. Turn on 'Allow unattended " +
1260
+ "Epic Implementer runs' on the repository's Setup page in the Bridge admin " +
1261
+ "panel, or pass --attended to create an attended run. If it is already on, " +
1262
+ "contact Bridge support. No run was created.",
1242
1263
  };
1243
1264
  }
1244
1265
  // Two independent sources satisfy the escalation channel, and the check has to
@@ -1253,13 +1274,21 @@ export function evaluateSetupEpicUnattendedPrerequisites(readiness, requestedPol
1253
1274
  // Presence, not verification: the server probes the URL at activation and is
1254
1275
  // the authority on whether it is reachable. This gate exists to spare the
1255
1276
  // operator a created-and-refused run, not to duplicate the probe.
1277
+ //
1278
+ // BAPI-1274 — a third source: when no webhook is declared anywhere, the
1279
+ // account's email address is the escalation channel, and the server reports
1280
+ // its presence (never the address). A declared webhook stays authoritative;
1281
+ // email only fills the gap when none is declared. `notify.local_sink` still
1282
+ // satisfies nothing.
1256
1283
  if (!unattended.notify_webhook_default_declared &&
1257
- !policyDeclaresNotifyWebhook(requestedPolicy)) {
1284
+ !policyDeclaresNotifyWebhook(requestedPolicy) &&
1285
+ unattended.escalation_email_available !== true) {
1258
1286
  return {
1259
1287
  reason: "webhook_unverified",
1260
- message: "an unattended run must have a verified escalation channel, and neither this " +
1261
- "repository nor this run declares one. Supply a --policy-file declaring " +
1262
- "notify.webhook_url (notify.local_sink does not satisfy this), or pass " +
1288
+ message: "an unattended run must have an escalation channel, and this repository, this " +
1289
+ "run, and the account provide none. Supply a --policy-file declaring " +
1290
+ "notify.webhook_url (notify.local_sink does not satisfy this), or add an email " +
1291
+ "address to the account so escalation alerts can be emailed, or pass " +
1263
1292
  "--attended to create an attended run. No run was created.",
1264
1293
  };
1265
1294
  }
@@ -1551,12 +1580,7 @@ composesDefaultPolicy = true) {
1551
1580
  `(pass --feature-branch to force ${proposeFeatureBranchName(opts.epicKey)} anyway)`);
1552
1581
  }
1553
1582
  function errorDetail(err) {
1554
- if (err instanceof ConductorBridgeApiError) {
1555
- const status = err.status !== undefined ? ` (HTTP ${err.status})` : "";
1556
- const preview = err.bodyPreview ? `: ${err.bodyPreview}` : "";
1557
- return `${err.message}${status}${preview}`;
1558
- }
1559
- return err instanceof Error ? err.message : String(err);
1583
+ return safeDiagnosticMessage(err, "operation failed");
1560
1584
  }
1561
1585
  /**
1562
1586
  * Reconcile a policy file against a REUSED run's stored policy, applying it only
@@ -1578,11 +1602,13 @@ async function applyReusedRunPolicy(args) {
1578
1602
  });
1579
1603
  if (outcome.kind === "noop") {
1580
1604
  args.result.policy_applied = "noop";
1605
+ args.result.effective_policy = effectivePolicyFacts(args.storedPolicy, "stored");
1581
1606
  args.say("Policy: policy unchanged");
1582
1607
  return true;
1583
1608
  }
1584
1609
  if (outcome.kind === "refused_divergent") {
1585
1610
  args.result.policy_applied = "refused_divergent";
1611
+ args.result.effective_policy = effectivePolicyFacts(args.storedPolicy, "stored");
1586
1612
  args.deps.errorLog(`Epic run ${args.epicRunId} is live and its stored policy differs from the ` +
1587
1613
  `policy file. setup-epic will not silently change a running epic's policy.\n` +
1588
1614
  `${outcome.diffLines.join("\n")}\n` +
@@ -1595,10 +1621,12 @@ async function applyReusedRunPolicy(args) {
1595
1621
  }
1596
1622
  catch (err) {
1597
1623
  args.result.policy_applied = "refused_divergent";
1624
+ args.result.effective_policy = effectivePolicyFacts(args.storedPolicy, "stored");
1598
1625
  args.deps.errorLog(`Failed to replace the run policy: ${errorDetail(err)}`);
1599
1626
  return false;
1600
1627
  }
1601
1628
  args.result.policy_applied = "replaced";
1629
+ args.result.effective_policy = effectivePolicyFacts(args.requestedPolicy, "explicit");
1602
1630
  args.say("Policy: policy applied: replaced");
1603
1631
  for (const line of outcome.diffLines)
1604
1632
  args.say(line);
@@ -1850,12 +1878,13 @@ function describeScopeTransition(state, status) {
1850
1878
  * {@link runSetupEpicCli} in every observable way — same output, same refusals,
1851
1879
  * same exit codes — and differs only in what it hands back.
1852
1880
  */
1853
- export async function runSetupEpicWorkflow(argv, overrides = {}) {
1881
+ export async function runSetupEpicWorkflow(argv, overrides = {}, handoff) {
1854
1882
  const sink = {};
1855
- const exitCode = await executeSetupEpic(argv, overrides, sink);
1883
+ const exitCode = await executeSetupEpic(argv, overrides, sink, handoff);
1856
1884
  return {
1857
1885
  exitCode,
1858
1886
  ...(sink.epicRunId !== undefined ? { epicRunId: sink.epicRunId } : {}),
1887
+ ...(sink.epicRunId !== undefined ? { runExists: true } : {}),
1859
1888
  };
1860
1889
  }
1861
1890
  /**
@@ -1865,7 +1894,7 @@ export async function runSetupEpicWorkflow(argv, overrides = {}) {
1865
1894
  export async function runSetupEpicCli(argv, overrides = {}) {
1866
1895
  return (await runSetupEpicWorkflow(argv, overrides)).exitCode;
1867
1896
  }
1868
- async function executeSetupEpic(argv, overrides, runIdSink) {
1897
+ async function executeSetupEpic(argv, overrides, runIdSink, handoff) {
1869
1898
  const deps = { ...createDefaultSetupEpicDeps(), ...overrides };
1870
1899
  const parsed = parseSetupEpicArgs(argv);
1871
1900
  if (parsed.status === "help") {
@@ -1881,6 +1910,14 @@ async function executeSetupEpic(argv, overrides, runIdSink) {
1881
1910
  const opts = parsed.options;
1882
1911
  // With --json, stdout carries the single result object and nothing else.
1883
1912
  const say = opts.json ? deps.errorLog : deps.log;
1913
+ if (handoff && (handoff.access.baseUrl !== handoff.selection.baseUrl ||
1914
+ handoff.access.repoName !== handoff.selection.repoName ||
1915
+ (opts.baseUrl !== undefined && opts.baseUrl !== handoff.selection.baseUrl) ||
1916
+ (opts.repo !== undefined && opts.repo !== handoff.selection.repoName) ||
1917
+ (opts.launchProfile !== undefined && opts.launchProfile !== handoff.selection.profileName))) {
1918
+ deps.errorLog("setup-epic REFUSED (selection-conflict): delegated target or repository disagrees with the pinned launch selection.");
1919
+ return 1;
1920
+ }
1884
1921
  // --- Read + validate the plan sidecar (no network yet) -------------------
1885
1922
  let raw;
1886
1923
  try {
@@ -1912,18 +1949,137 @@ async function executeSetupEpic(argv, overrides, runIdSink) {
1912
1949
  return 1;
1913
1950
  }
1914
1951
  const localHash = hashPlan(plan);
1952
+ // Resolve the selected target and live-run identity before choosing a policy.
1953
+ // The offline local-only path keeps its no-network contract.
1954
+ let access;
1955
+ let selectedProfilePolicyPath = handoff?.profile?.policyRef?.path;
1956
+ if (opts.localOnly && !handoff) {
1957
+ const profileName = opts.launchProfile ?? deps.env.BAPI_EPIC_LAUNCH_PROFILE?.trim();
1958
+ if (profileName) {
1959
+ const profileRead = await readEpicLaunchProfile(profileName, {
1960
+ env: deps.env,
1961
+ homedir: deps.homedir,
1962
+ readFile: deps.readFile,
1963
+ host: os.hostname(),
1964
+ });
1965
+ if (!profileRead.ok) {
1966
+ deps.errorLog(profileRead.message);
1967
+ return 1;
1968
+ }
1969
+ selectedProfilePolicyPath = profileRead.profile.policyRef?.path;
1970
+ }
1971
+ }
1972
+ let existingRunId = null;
1973
+ let existingStatus = null;
1974
+ let existingBaseBranch = null;
1975
+ let existingReviewPolicy = null;
1976
+ // BAPI-754: the live run's SERVER-REDACTED stored policy, kept for the reuse
1977
+ // comparison. Redacted is the only form a client can ever see.
1978
+ let existingPolicyJson = null;
1979
+ if (!opts.localOnly) {
1980
+ // --- Resolve access ------------------------------------------------------
1981
+ // BAPI-1260: a caller that selected the epic-launch contract (drive-epic
1982
+ // forwards --launch-profile / --base-url) gets the target-first resolver and
1983
+ // the bound credential; the legacy resolver is kept byte-for-byte otherwise.
1984
+ const accessDeps = {
1985
+ env: deps.env,
1986
+ cwd: deps.cwd,
1987
+ homedir: deps.homedir,
1988
+ platform: deps.platform,
1989
+ readFile: deps.readFile,
1990
+ stat: deps.stat,
1991
+ repoName: opts.repo,
1992
+ };
1993
+ const accessResult = handoff
1994
+ ? { ok: true, access: handoff.access, profile: handoff.profile }
1995
+ : opts.launchProfile !== undefined || opts.baseUrl !== undefined || Boolean(deps.env.BAPI_EPIC_LAUNCH_PROFILE?.trim())
1996
+ ? await resolveEpicLaunchBridgeApiAccess({
1997
+ ...(opts.launchProfile ? { profileName: opts.launchProfile } : {}),
1998
+ ...(opts.baseUrl ? { baseUrl: opts.baseUrl } : {}),
1999
+ }, accessDeps)
2000
+ : await resolveConductorBridgeApiAccess(accessDeps);
2001
+ if (!accessResult.ok) {
2002
+ deps.errorLog(`Cannot reach the Bridge API: ${accessResult.error}`);
2003
+ return 1;
2004
+ }
2005
+ access = accessResult.access;
2006
+ say(`Epic: ${opts.epicKey}`);
2007
+ say(`Repo: ${access.repoName}`);
2008
+ say(`Plan: v${plan.plan_version}, ${plan.nodes.length} node(s), ${plan.edges.length} edge(s)`);
2009
+ say(`Local hash: ${localHash}`);
2010
+ for (const w of warnings)
2011
+ say(` [warn] ${w}`);
2012
+ selectedProfilePolicyPath = "profile" in accessResult
2013
+ ? accessResult.profile?.policyRef?.path
2014
+ : undefined;
2015
+ // --- Step 0: pre-check ---------------------------------------------------
2016
+ // Never create on an ambiguous read. A wrong answer here mints a duplicate run.
2017
+ try {
2018
+ const state = await fetchEpicRunState(access, opts.epicKey, deps.fetch);
2019
+ existingRunId = state.epic_run?.epic_run_id ?? null;
2020
+ existingStatus = state.epic_run?.status ?? null;
2021
+ const existingPolicy = state.epic_run?.policy_json;
2022
+ existingPolicyJson = existingPolicy ?? null;
2023
+ const existingBase = existingPolicy && typeof existingPolicy === "object"
2024
+ ? existingPolicy.base_branch
2025
+ : undefined;
2026
+ existingBaseBranch = typeof existingBase === "string" && existingBase.trim() !== ""
2027
+ ? existingBase
2028
+ : null;
2029
+ // BAPI-679: read the existing run's review policy for the conflict guard
2030
+ // below. Absent/malformed reads as null — "the resolver's default applies" —
2031
+ // which is deliberately NOT the same as an explicitly selected source.
2032
+ const existingReview = existingPolicy && typeof existingPolicy === "object"
2033
+ ? existingPolicy.review_policy
2034
+ : undefined;
2035
+ const existingReviewSource = existingReview && typeof existingReview === "object"
2036
+ ? existingReview.source
2037
+ : undefined;
2038
+ existingReviewPolicy =
2039
+ typeof existingReviewSource === "string" && existingReviewSource.trim() !== ""
2040
+ ? existingReviewSource
2041
+ : null;
2042
+ }
2043
+ catch (err) {
2044
+ if (err instanceof ConductorBridgeApiError && err.status === 404) {
2045
+ existingRunId = null; // No live run — the normal first-run path.
2046
+ }
2047
+ else if (err instanceof ConductorBridgeApiError && err.status === 409) {
2048
+ deps.errorLog(`Epic ${opts.epicKey} has MULTIPLE active runs — it is wedged, and every ` +
2049
+ `plan call will keep failing. Abandon the duplicate before retrying:\n` +
2050
+ ` epic-implementer abandon-run --epic-run-id <duplicate_epic_run_id>\n` +
2051
+ `Detail: ${errorDetail(err)}`);
2052
+ return 1;
2053
+ }
2054
+ else {
2055
+ deps.errorLog(`Could not read the epic run state, so creating one would risk a duplicate ` +
2056
+ `(which wedges the epic permanently). Refusing to continue.\n` +
2057
+ `Detail: ${errorDetail(err)}`);
2058
+ return 1;
2059
+ }
2060
+ }
2061
+ }
1915
2062
  // --- Read the policy file (still no network) ------------------------------
1916
2063
  // BAPI-754: parsed here so a malformed file, or a contradiction between the
1917
2064
  // file and the CLI flags, fails before credentials are resolved or any request
1918
2065
  // is dispatched.
1919
2066
  let filePolicy;
1920
- if (opts.policyFile !== undefined) {
1921
- const policyRead = await readSetupEpicPolicyFile(opts.policyFile, deps.readFile);
1922
- if (!policyRead.ok) {
1923
- deps.errorLog(policyRead.error);
1924
- return 1;
2067
+ const selectedPolicyPath = opts.policyFile ?? (existingRunId === null ? selectedProfilePolicyPath : undefined);
2068
+ const selectedPolicySource = existingRunId !== null
2069
+ ? "stored" : opts.policyFile !== undefined ? "explicit" : selectedProfilePolicyPath !== undefined ? "profile" : "default";
2070
+ if (selectedPolicyPath !== undefined) {
2071
+ const pinned = handoff ? getPinnedEpicLaunchPolicy(handoff.selection) : undefined;
2072
+ if (pinned !== undefined) {
2073
+ filePolicy = pinned;
2074
+ }
2075
+ else {
2076
+ const policyRead = await readSetupEpicPolicyFile(selectedPolicyPath, deps.readFile);
2077
+ if (!policyRead.ok) {
2078
+ deps.errorLog(policyRead.error);
2079
+ return 1;
2080
+ }
2081
+ filePolicy = policyRead.policy;
1925
2082
  }
1926
- filePolicy = policyRead.policy;
1927
2083
  }
1928
2084
  const policyDeclaresBranch = filePolicy !== undefined &&
1929
2085
  ["base_branch", "baseBranch"].some((key) => key in filePolicy && filePolicy[key] !== undefined && filePolicy[key] !== null);
@@ -1953,7 +2109,7 @@ async function executeSetupEpic(argv, overrides, runIdSink) {
1953
2109
  const policyGate = preflightSetupEpicPolicy({
1954
2110
  intoBase: opts.intoBase,
1955
2111
  attended: opts.attended,
1956
- policyFile: opts.policyFile,
2112
+ policyFile: selectedPolicyPath,
1957
2113
  filePolicy,
1958
2114
  policyDeclaresBranch,
1959
2115
  featureBranch,
@@ -1991,42 +2147,14 @@ async function executeSetupEpic(argv, overrides, runIdSink) {
1991
2147
  plan_version: plan.plan_version,
1992
2148
  local_plan_hash: localHash,
1993
2149
  server_validated: false,
2150
+ feature_branch: opts.intoBase ? null : resolveEffectiveFeatureBranch(featureBranch, policyGate.policy) ?? null,
1994
2151
  policy_applied: "absent",
2152
+ effective_policy: effectivePolicyFacts(policyGate.policy, selectedPolicySource),
1995
2153
  warnings,
1996
2154
  }, null, 2));
1997
2155
  }
1998
2156
  return 0;
1999
2157
  }
2000
- // --- Resolve access ------------------------------------------------------
2001
- // BAPI-1260: a caller that selected the epic-launch contract (drive-epic
2002
- // forwards --launch-profile / --base-url) gets the target-first resolver and
2003
- // the bound credential; the legacy resolver is kept byte-for-byte otherwise.
2004
- const accessDeps = {
2005
- env: deps.env,
2006
- cwd: deps.cwd,
2007
- homedir: deps.homedir,
2008
- platform: deps.platform,
2009
- readFile: deps.readFile,
2010
- stat: deps.stat,
2011
- repoName: opts.repo,
2012
- };
2013
- const accessResult = opts.launchProfile !== undefined || opts.baseUrl !== undefined
2014
- ? await resolveEpicLaunchBridgeApiAccess({
2015
- ...(opts.launchProfile ? { profileName: opts.launchProfile } : {}),
2016
- ...(opts.baseUrl ? { baseUrl: opts.baseUrl } : {}),
2017
- }, accessDeps)
2018
- : await resolveConductorBridgeApiAccess(accessDeps);
2019
- if (!accessResult.ok) {
2020
- deps.errorLog(`Cannot reach the Bridge API: ${accessResult.error}`);
2021
- return 1;
2022
- }
2023
- const access = accessResult.access;
2024
- say(`Epic: ${opts.epicKey}`);
2025
- say(`Repo: ${access.repoName}`);
2026
- say(`Plan: v${plan.plan_version}, ${plan.nodes.length} node(s), ${plan.edges.length} edge(s)`);
2027
- say(`Local hash: ${localHash}`);
2028
- for (const w of warnings)
2029
- say(` [warn] ${w}`);
2030
2158
  // --- Branch strategy (BAPI-1009) ----------------------------------------
2031
2159
  // Already resolved above, before the network was touched. Reported HERE, ahead
2032
2160
  // of any secondary policy detail, because it is the one decision that changes
@@ -2083,6 +2211,11 @@ async function executeSetupEpic(argv, overrides, runIdSink) {
2083
2211
  reviewPolicy: opts.reviewPolicy,
2084
2212
  mergeExecutionMode: declaredMergeExecutionMode,
2085
2213
  });
2214
+ const selectedEffectivePolicyFacts = effectivePolicyFacts(existingRunId !== null ? existingPolicyJson : composedForCreate, selectedPolicySource);
2215
+ say(`Policy: effective ${selectedEffectivePolicyFacts.source}; serial execution; ` +
2216
+ `caps ${selectedEffectivePolicyFacts.caps ? JSON.stringify(selectedEffectivePolicyFacts.caps) : "unavailable"}; ` +
2217
+ `timeouts ${selectedEffectivePolicyFacts.job_timeouts ? JSON.stringify(selectedEffectivePolicyFacts.job_timeouts) : "unavailable"}; ` +
2218
+ `channel ${selectedEffectivePolicyFacts.escalation_channel}`);
2086
2219
  // BAPI-1133 (A2). Reported only when THIS command stamped the key — i.e. it
2087
2220
  // is present in the composed policy and was absent from whatever the
2088
2221
  // operator supplied — as low-emphasis policy metadata, mirroring the
@@ -2118,59 +2251,6 @@ async function executeSetupEpic(argv, overrides, runIdSink) {
2118
2251
  say(`Feature: ${effectiveFeatureBranch} (from the policy file; cut on origin with local ` +
2119
2252
  `git at the canonical indexed commit; validated server-side at approval)`);
2120
2253
  }
2121
- // --- Step 0: pre-check ---------------------------------------------------
2122
- // Never create on an ambiguous read. A wrong answer here mints a duplicate run.
2123
- let existingRunId = null;
2124
- let existingStatus = null;
2125
- let existingBaseBranch = null;
2126
- let existingReviewPolicy = null;
2127
- // BAPI-754: the live run's SERVER-REDACTED stored policy, kept for the reuse
2128
- // comparison. Redacted is the only form a client can ever see.
2129
- let existingPolicyJson = null;
2130
- try {
2131
- const state = await fetchEpicRunState(access, opts.epicKey, deps.fetch);
2132
- existingRunId = state.epic_run?.epic_run_id ?? null;
2133
- existingStatus = state.epic_run?.status ?? null;
2134
- const existingPolicy = state.epic_run?.policy_json;
2135
- existingPolicyJson = existingPolicy ?? null;
2136
- const existingBase = existingPolicy && typeof existingPolicy === "object"
2137
- ? existingPolicy.base_branch
2138
- : undefined;
2139
- existingBaseBranch = typeof existingBase === "string" && existingBase.trim() !== ""
2140
- ? existingBase
2141
- : null;
2142
- // BAPI-679: read the existing run's review policy for the conflict guard
2143
- // below. Absent/malformed reads as null — "the resolver's default applies" —
2144
- // which is deliberately NOT the same as an explicitly selected source.
2145
- const existingReview = existingPolicy && typeof existingPolicy === "object"
2146
- ? existingPolicy.review_policy
2147
- : undefined;
2148
- const existingReviewSource = existingReview && typeof existingReview === "object"
2149
- ? existingReview.source
2150
- : undefined;
2151
- existingReviewPolicy =
2152
- typeof existingReviewSource === "string" && existingReviewSource.trim() !== ""
2153
- ? existingReviewSource
2154
- : null;
2155
- }
2156
- catch (err) {
2157
- if (err instanceof ConductorBridgeApiError && err.status === 404) {
2158
- existingRunId = null; // No live run — the normal first-run path.
2159
- }
2160
- else if (err instanceof ConductorBridgeApiError && err.status === 409) {
2161
- deps.errorLog(`Epic ${opts.epicKey} has MULTIPLE active runs — it is wedged, and every ` +
2162
- `plan call will keep failing. Abandon the duplicate before retrying:\n` +
2163
- ` epic-implementer abandon-run --epic-run-id <duplicate_epic_run_id>\n` +
2164
- `Detail: ${errorDetail(err)}`);
2165
- return 1;
2166
- }
2167
- else {
2168
- deps.errorLog(`Could not read the epic run state, so creating one would risk a duplicate ` +
2169
- `(which wedges the epic permanently). Refusing to continue.\n` +
2170
- `Detail: ${errorDetail(err)}`);
2171
- return 1;
2172
- }
2173
- }
2174
2254
  // --- Feature-branch conflict guard against an existing live run ----------
2175
2255
  // A selected branch that disagrees with the existing run's stored base branch
2176
2256
  // must fail closed rather than patch or silently change that run. A matching
@@ -2498,6 +2578,7 @@ async function executeSetupEpic(argv, overrides, runIdSink) {
2498
2578
  // "absent"; server_validated says whether the check was authoritative.
2499
2579
  server_validated: validated !== null,
2500
2580
  policy_applied: "absent",
2581
+ effective_policy: selectedEffectivePolicyFacts,
2501
2582
  ...(validated
2502
2583
  ? {
2503
2584
  server_plan_hash: validated.planHash,
@@ -2569,6 +2650,7 @@ async function executeSetupEpic(argv, overrides, runIdSink) {
2569
2650
  plan_approved: false,
2570
2651
  server_validated: validated !== null,
2571
2652
  policy_applied: "absent",
2653
+ effective_policy: selectedEffectivePolicyFacts,
2572
2654
  warnings,
2573
2655
  };
2574
2656
  // BAPI-850: the EFFECTIVE feature branch (flag or policy file), so a
@@ -2946,10 +3028,11 @@ async function executeSetupEpic(argv, overrides, runIdSink) {
2946
3028
  deps.errorLog(`The plan was NOT approved: this repository is no longer authorized for ` +
2947
3029
  `unattended runs. The run WAS created and is left in 'planning' with no ` +
2948
3030
  `activation and no executor dispatch — nothing was half-applied.\n` +
2949
- `Restore \`unattended_epic_implementer_allowed\` for this repository (see ` +
2950
- `docs/claude/account-settings-operator-runbook.md) and re-run setup-epic, ` +
2951
- `or deliberately convert this run to an attended posture through the ` +
2952
- `supported operator flow.\nDetail: ${errorDetail(err)}`);
3031
+ `Turn 'Allow unattended Epic Implementer runs' back on in the repository's ` +
3032
+ `Setup page in the Bridge admin panel and re-run setup-epic, or ` +
3033
+ `deliberately convert this run to an attended posture through the ` +
3034
+ `supported operator flow. If it is already on, contact Bridge support.` +
3035
+ `\nDetail: ${errorDetail(err)}`);
2953
3036
  }
2954
3037
  else {
2955
3038
  deps.errorLog(`Failed to approve the plan: ${errorDetail(err)}`);
@@ -3117,14 +3200,14 @@ async function executeSetupEpic(argv, overrides, runIdSink) {
3117
3200
  else {
3118
3201
  say("The server-side reconciler will pick it up within ~30s.");
3119
3202
  }
3120
- // BAPI-1102 — the executor command is printed WITH a claim scope. Since
3121
- // BAPI-1026 an executor started with no scope REFUSES TO START, so the
3122
- // unscoped form printed here was guidance that could not work: an operator
3123
- // who copied it got a startup error, and the natural reading of that error is
3124
- // that the run is broken rather than that the instruction was.
3125
- say("To execute this run's claimed jobs on this machine, run:");
3126
- say(` npx -y ${MCP_PACKAGE_NAME} executor --repo ${access.repoName} ` +
3127
- `--epic-run-id ${result.epic_run_id}`);
3203
+ if (handoff) {
3204
+ say("The launcher starts or reuses the executor appropriate for this run.");
3205
+ }
3206
+ else {
3207
+ say("To execute this run's claimed jobs on this machine, run:");
3208
+ say(` npx -y ${MCP_PACKAGE_NAME} executor --repo ${access.repoName} ` +
3209
+ `--epic-run-id ${result.epic_run_id}`);
3210
+ }
3128
3211
  }
3129
3212
  return exitCode;
3130
3213
  }
@@ -6,6 +6,7 @@
6
6
  * prevent the others from running.
7
7
  */
8
8
  import { SFCC_VERSIONS, getSfccVersionConfig } from "./config.js";
9
+ import { publishSfccVersion } from "./tools-configured.js";
9
10
  import { getAmToken } from "./client.js";
10
11
  // ---------------------------------------------------------------------------
11
12
  // Implementation
@@ -38,6 +39,7 @@ export async function sfccSetupStatusTool(deps) {
38
39
  if (apiKeyOk && repoOk) {
39
40
  try {
40
41
  resolvedVersion = await getSfccVersionConfig(deps.buildGetUrl, deps.getGetHeaders, deps.repoName);
42
+ publishSfccVersion(resolvedVersion);
41
43
  if (resolvedVersion === null) {
42
44
  versionStatus = "✗ Not set (configure the 'version' field in Bridge API project settings)";
43
45
  }
@@ -62,6 +62,7 @@
62
62
  * no per-tool opt-in and no exemption flag.
63
63
  */
64
64
  import { SFCC_VERSIONS, getSfccVersionConfig } from "./config.js";
65
+ import { publishSfccVersion } from "./tools-configured.js";
65
66
  import { resolveSfccCredentials } from "./credentials.js";
66
67
  import { formatSfccFailure } from "./sfcc-result.js";
67
68
  // ---------------------------------------------------------------------------
@@ -385,6 +386,7 @@ export function withSfccGate(deps, handler) {
385
386
  }
386
387
  // (a) Read /config-field/version
387
388
  const version = await getSfccVersionConfig(deps.buildGetUrl, deps.getGetHeaders, deps.repoName);
389
+ publishSfccVersion(version);
388
390
  if (version === null) {
389
391
  return notConfigured("bridge-auth", "Could not read the SFCC version from Bridge API (/config-field/version). " +
390
392
  "Ensure your Bridge API key is set and the repo is configured. " +
@@ -0,0 +1,96 @@
1
+ /** Process-local, secret-free SFCC setup verdict for authenticated Bridge requests. */
2
+ import { access } from "fs/promises";
3
+ import { constants } from "fs";
4
+ import path from "path";
5
+ import { resolveSfccCredentials } from "./credentials.js";
6
+ import { getSfccVersionConfig, SFCC_VERSIONS } from "./config.js";
7
+ const DIAGNOSTIC = "SFCC tools header omitted: version lookup did not establish SFCC setup; check setup status.";
8
+ /** Existence and readability are enough to establish the local gate. */
9
+ export async function hasRootDwJson() {
10
+ try {
11
+ await access(path.join(process.cwd(), "dw.json"), constants.R_OK);
12
+ return true;
13
+ }
14
+ catch {
15
+ return false;
16
+ }
17
+ }
18
+ /** Isolated state is injectable for offline tests; the MCP uses one instance. */
19
+ export function createSfccToolsHeaderState(deps) {
20
+ let credentialVerdict;
21
+ let knownVersion;
22
+ let lookupStarted = false;
23
+ let diagnosticPrinted = false;
24
+ function getCredentialVerdict() {
25
+ credentialVerdict ??= Promise.resolve().then(() => deps.resolveCredentials(deps.env, {
26
+ ...deps.credentialDeps,
27
+ writeFile: async () => { },
28
+ mkdir: async () => { },
29
+ })).then((result) => result.ok ? "yes" : "no")
30
+ .catch(() => "no");
31
+ return credentialVerdict;
32
+ }
33
+ function publishVersion(version) {
34
+ if (version !== null && knownVersion === undefined)
35
+ knownVersion = version;
36
+ if (version !== null && SFCC_VERSIONS.includes(version))
37
+ knownVersion = version;
38
+ }
39
+ function reportUnresolved() {
40
+ if (diagnosticPrinted)
41
+ return;
42
+ diagnosticPrinted = true;
43
+ deps.diagnostic(DIAGNOSTIC);
44
+ }
45
+ async function withHeader(headers, options) {
46
+ if (options.skipSfccToolsHeader)
47
+ return headers;
48
+ try {
49
+ const localFile = await deps.hasDwJson();
50
+ if (knownVersion === undefined && !lookupStarted && options.repoName) {
51
+ lookupStarted = true;
52
+ void deps.getVersion(options.buildGetUrl, options.getLookupHeaders, options.repoName)
53
+ .then((version) => {
54
+ if (version !== null)
55
+ publishVersion(version);
56
+ if (knownVersion === undefined) {
57
+ knownVersion = null;
58
+ if (!localFile)
59
+ reportUnresolved();
60
+ }
61
+ })
62
+ .catch(() => {
63
+ if (knownVersion === undefined) {
64
+ knownVersion = null;
65
+ if (!localFile)
66
+ reportUnresolved();
67
+ }
68
+ });
69
+ }
70
+ const isSfcc = knownVersion !== undefined && knownVersion !== null
71
+ && SFCC_VERSIONS.includes(knownVersion);
72
+ if (!localFile && !isSfcc) {
73
+ if (!options.repoName)
74
+ reportUnresolved();
75
+ return headers;
76
+ }
77
+ headers["X-Bridge-SFCC-Tools"] = await getCredentialVerdict();
78
+ }
79
+ catch {
80
+ // Optional enrichment cannot interrupt authenticated requests.
81
+ }
82
+ return headers;
83
+ }
84
+ return { getCredentialVerdict, publishVersion, withHeader };
85
+ }
86
+ const processState = createSfccToolsHeaderState({
87
+ hasDwJson: hasRootDwJson,
88
+ resolveCredentials: (env, credentialDeps) => resolveSfccCredentials(undefined, env, credentialDeps),
89
+ credentialDeps: {},
90
+ env: process.env,
91
+ getVersion: getSfccVersionConfig,
92
+ diagnostic: (message) => console.error(message),
93
+ });
94
+ export const getSfccCredentialVerdict = processState.getCredentialVerdict;
95
+ export const publishSfccVersion = processState.publishVersion;
96
+ export const withSfccToolsHeader = processState.withHeader;