@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
@@ -18,10 +18,31 @@ import { normalizePrNumber, normalizeSha } from "./git-ci-types.js";
18
18
  import { ConductorValidationError } from "./errors.js";
19
19
  import { parseContinuationStatusResult, parseContinueParkedChildResult, } from "./epic-child-continuation.js";
20
20
  import { resolveDeclaredRunBaseBranch } from "./run-branch.js";
21
+ import { safeEpicLaunchTargetUrl } from "../epic-launch-diagnostic.js";
22
+ import { validateHttpBaseUrl } from "../base-url.js";
23
+ import { RERUN_CI_CHECK_STATUSES, RERUN_CI_NOT_RUNNABLE_REASONS, RERUN_CI_OUTCOMES, isCiResumeRefusalCode, } from "./repair-contract.js";
21
24
  /** Default Bridge API base URL when `BAPI_BASE_URL` is unset. */
22
25
  export const CONDUCTOR_DEFAULT_BASE_URL = "https://bridgegpt-api.com";
23
26
  /** Default per-request timeout for conductor Bridge API calls. */
24
27
  export const CONDUCTOR_FETCH_TIMEOUT_MS = 30_000;
28
+ /** Keep an explicit readiness target from borrowing a key bound to another ambient endpoint. */
29
+ export function readinessEndpointEnvironments(env, baseUrl) {
30
+ if (baseUrl === undefined)
31
+ return { reportEnv: env, resolverEnv: env };
32
+ const ambientBase = env.BAPI_BASE_URL ? validateHttpBaseUrl(env.BAPI_BASE_URL) : null;
33
+ const ambientMatchesOverride = ambientBase?.ok && ambientBase.baseUrl === baseUrl;
34
+ return {
35
+ reportEnv: { ...env, BAPI_BASE_URL: baseUrl, BAPI_EPIC_LAUNCH_PROFILE: undefined,
36
+ BAPI_API_KEY: ambientMatchesOverride ? env.BAPI_API_KEY : undefined },
37
+ resolverEnv: {
38
+ ...env,
39
+ BAPI_EPIC_LAUNCH_PROFILE: undefined,
40
+ // Never manufacture an ambient key's endpoint pairing. A stored key
41
+ // still passes its own binding check against the explicit target.
42
+ BAPI_BASE_URL: ambientMatchesOverride ? baseUrl : undefined,
43
+ },
44
+ };
45
+ }
25
46
  /**
26
47
  * Resolve `{ repoName, apiKey, baseUrl }` from the environment, the shared repo
27
48
  * resolver, and the shared credential store. Never throws and never embeds the
@@ -91,9 +112,11 @@ export async function resolveEpicLaunchBridgeApiAccess(selection, deps = {}) {
91
112
  const platform = deps.platform ?? process.platform;
92
113
  const readFileImpl = deps.readFile ?? ((p) => readFile(p, "utf-8"));
93
114
  const statImpl = deps.stat ?? ((p) => stat(p));
115
+ const environmentProfile = env.BAPI_EPIC_LAUNCH_PROFILE?.trim();
116
+ const profileName = selection.profileName ?? (environmentProfile || undefined);
94
117
  let profile = null;
95
- if (selection.profileName !== undefined) {
96
- const read = await readEpicLaunchProfile(selection.profileName, {
118
+ if (profileName !== undefined) {
119
+ const read = await readEpicLaunchProfile(profileName, {
97
120
  env,
98
121
  homedir,
99
122
  readFile: readFileImpl,
@@ -107,11 +130,13 @@ export async function resolveEpicLaunchBridgeApiAccess(selection, deps = {}) {
107
130
  flagBaseUrl: selection.baseUrl,
108
131
  flagRepo: deps.repoName,
109
132
  profile,
133
+ profileSource: selection.profileName !== undefined ? "profile" : profile ? "profile-env" : undefined,
110
134
  env,
111
135
  defaultBaseUrl: CONDUCTOR_DEFAULT_BASE_URL,
112
136
  });
113
137
  if (!target.ok)
114
138
  return { ok: false, kind: target.kind, error: target.message };
139
+ const reportableTarget = safeEpicLaunchTargetUrl(target.baseUrl) ? { baseUrl: target.baseUrl } : {};
115
140
  const repoName = target.repoName ||
116
141
  deps.repoName?.trim() ||
117
142
  (await resolveStartTicketsRepoName({ env, cwd, readFile: readFileImpl }));
@@ -120,6 +145,7 @@ export async function resolveEpicLaunchBridgeApiAccess(selection, deps = {}) {
120
145
  ok: false,
121
146
  kind: "repo-missing",
122
147
  error: "could not resolve repo name (set BAPI_REPO_NAME or add a valid .bridge/config)",
148
+ ...reportableTarget,
123
149
  };
124
150
  }
125
151
  let cred;
@@ -127,15 +153,15 @@ export async function resolveEpicLaunchBridgeApiAccess(selection, deps = {}) {
127
153
  cred = await resolveEpicLaunchCredential(repoName, target.baseUrl, { env, homedir, platform, readFile: readFileImpl, stat: statImpl }, { productionBaseUrl: CONDUCTOR_DEFAULT_BASE_URL });
128
154
  }
129
155
  catch {
130
- return { ok: false, kind: "credentials-unavailable", error: "failed to resolve Bridge API credentials" };
156
+ return { ok: false, kind: "credentials-unavailable", error: "failed to resolve Bridge API credentials", ...reportableTarget };
131
157
  }
132
158
  if (!cred.ok) {
133
159
  const bindingFailure = cred.kind === "binding-unbound" || cred.kind === "binding-mismatch" || cred.kind === "ambient-key-unbound";
134
160
  // The binding messages are fixed sentences naming the target KEY and the
135
161
  // store path only; the generic store failures stay generic as before.
136
162
  return bindingFailure
137
- ? { ok: false, kind: "credential-unbound", error: cred.error }
138
- : { ok: false, kind: "credentials-unavailable", error: "Bridge API credentials unavailable" };
163
+ ? { ok: false, kind: "credential-unbound", error: cred.error, ...reportableTarget }
164
+ : { ok: false, kind: "credentials-unavailable", error: "Bridge API credentials unavailable", ...reportableTarget };
139
165
  }
140
166
  return {
141
167
  ok: true,
@@ -1478,6 +1504,9 @@ async function postRepairRequest(access, apiPath, body, fetchImpl) {
1478
1504
  // The server's refusal explanation is the operator-facing message here.
1479
1505
  { readErrorField: true });
1480
1506
  }
1507
+ function withGuard(body, guard) {
1508
+ return guard === undefined ? body : { ...body, guard };
1509
+ }
1481
1510
  /**
1482
1511
  * POST `/jira/epic-runs/runs/{epic_run_id}/scope-reparse` — re-drive the run's
1483
1512
  * OWN index scope through the retry-reparse lifecycle. Body: `{repo_name,
@@ -1487,7 +1516,7 @@ async function postRepairRequest(access, apiPath, body, fetchImpl) {
1487
1516
  export async function scopeReparse(access, request, fetchImpl = globalThis.fetch) {
1488
1517
  requireNonEmptyString(request.epicRunId);
1489
1518
  requireNonEmptyString(request.scopeId);
1490
- const parsed = await postRepairRequest(access, `${epicRunApiPath(request.epicRunId)}/scope-reparse`, { scope_id: request.scopeId }, fetchImpl);
1519
+ const parsed = await postRepairRequest(access, `${epicRunApiPath(request.epicRunId)}/scope-reparse`, withGuard({ scope_id: request.scopeId }, request.guard), fetchImpl);
1491
1520
  return parseScopeReparseResult(parsed);
1492
1521
  }
1493
1522
  /**
@@ -1500,9 +1529,120 @@ export async function dispatchRemediation(access, request, fetchImpl = globalThi
1500
1529
  requireNonEmptyString(request.epicRunId);
1501
1530
  requireNonEmptyString(request.ticketKey);
1502
1531
  requireNonEmptyString(request.context);
1503
- const parsed = await postRepairRequest(access, `${epicRunApiPath(request.epicRunId)}/tickets/${encodeURIComponent(request.ticketKey)}/dispatch-remediation`, { context: request.context }, fetchImpl);
1532
+ const parsed = await postRepairRequest(access, `${epicRunApiPath(request.epicRunId)}/tickets/${encodeURIComponent(request.ticketKey)}/dispatch-remediation`, withGuard({ context: request.context }, request.guard), fetchImpl);
1504
1533
  return parseDispatchRemediationResult(parsed);
1505
1534
  }
1535
+ // ---------------------------------------------------------------------------
1536
+ // BAPI-1388 — `rerun-ci`
1537
+ // ---------------------------------------------------------------------------
1538
+ /** The synthetic code an old server's missing route is reported under. */
1539
+ export const RERUN_CI_ROUTE_UNAVAILABLE_CODE = "RERUN_CI_ROUTE_UNAVAILABLE";
1540
+ /** The fixed operator text for a server that has no `rerun-ci` route. */
1541
+ export const RERUN_CI_ROUTE_UNAVAILABLE_MESSAGE = "rerun-ci is not available on this server; upgrade the Bridge API server before using it";
1542
+ const RERUN_CI_HEAD_RE = /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/;
1543
+ function parseRerunCiCheck(value) {
1544
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
1545
+ throw new ConductorBridgeApiError("server");
1546
+ }
1547
+ const c = value;
1548
+ const status = c["status"];
1549
+ if (typeof status !== "string" ||
1550
+ !RERUN_CI_CHECK_STATUSES.includes(status)) {
1551
+ throw new ConductorBridgeApiError("server");
1552
+ }
1553
+ const reason = c["reason"];
1554
+ if (reason !== undefined &&
1555
+ reason !== null &&
1556
+ (typeof reason !== "string" ||
1557
+ !RERUN_CI_NOT_RUNNABLE_REASONS.includes(reason))) {
1558
+ throw new ConductorBridgeApiError("server");
1559
+ }
1560
+ const optionalPositive = (field) => {
1561
+ const v = c[field];
1562
+ if (v === undefined || v === null)
1563
+ return null;
1564
+ if (typeof v !== "number" || !Number.isSafeInteger(v) || v <= 0) {
1565
+ throw new ConductorBridgeApiError("server");
1566
+ }
1567
+ return v;
1568
+ };
1569
+ const optionalText = (field) => {
1570
+ const v = c[field];
1571
+ if (v === undefined || v === null)
1572
+ return null;
1573
+ if (typeof v !== "string")
1574
+ throw new ConductorBridgeApiError("server");
1575
+ return v;
1576
+ };
1577
+ return {
1578
+ name: optionalText("name"),
1579
+ workflow_identity: optionalText("workflow_identity"),
1580
+ run_id: optionalPositive("run_id"),
1581
+ status: status,
1582
+ reason: (reason ?? null),
1583
+ job_id: optionalPositive("job_id"),
1584
+ };
1585
+ }
1586
+ /**
1587
+ * Strictly parse a `rerun-ci` success body. Anything short of the contract — an
1588
+ * unknown outcome or per-check status, a malformed head, a non-list `checks` —
1589
+ * throws, so a malformed answer is never read as a success.
1590
+ */
1591
+ export function parseRerunCiResult(parsed) {
1592
+ const p = parseRepairBase(parsed);
1593
+ const outcome = p["outcome"];
1594
+ const checks = p["checks"];
1595
+ const head = p["head_sha"];
1596
+ if (typeof p["ticket_key"] !== "string" ||
1597
+ typeof outcome !== "string" ||
1598
+ !RERUN_CI_OUTCOMES.includes(outcome) ||
1599
+ !Array.isArray(checks) ||
1600
+ (head !== undefined && head !== null && (typeof head !== "string" || !RERUN_CI_HEAD_RE.test(head)))) {
1601
+ throw new ConductorBridgeApiError("server");
1602
+ }
1603
+ return {
1604
+ epic_run_id: p["epic_run_id"],
1605
+ ticket_key: p["ticket_key"],
1606
+ before: parseRepairNullableString(p["before"]),
1607
+ after: parseRepairNullableString(p["after"]),
1608
+ committed: p["committed"],
1609
+ event_recorded: p["event_recorded"],
1610
+ outcome: outcome,
1611
+ head_sha: typeof head === "string" ? head : null,
1612
+ checks: checks.map(parseRerunCiCheck),
1613
+ };
1614
+ }
1615
+ /**
1616
+ * POST `/jira/epic-runs/runs/{epic_run_id}/tickets/{ticket_key}/rerun-ci` — body
1617
+ * `{repo_name, guard}` and nothing else: no context, check list, or run id, because
1618
+ * the SERVER decides what is re-runnable from its own fresh, head-bound CI read.
1619
+ *
1620
+ * A generic `{"detail":"Not Found"}` 404 or a 405 means the server predates the
1621
+ * route; it is reported under {@link RERUN_CI_ROUTE_UNAVAILABLE_CODE} so no caller
1622
+ * can read it as a refusal of THIS ticket or as a success (following
1623
+ * {@link classifySupervisorBootstrapFailure}). A structured 404 — notably
1624
+ * `TICKET_NOT_IN_RUN` — keeps its own code and stays a refusal.
1625
+ */
1626
+ export async function rerunCi(access, request, fetchImpl = globalThis.fetch) {
1627
+ requireNonEmptyString(request.epicRunId);
1628
+ requireNonEmptyString(request.ticketKey);
1629
+ let parsed;
1630
+ try {
1631
+ parsed = await postRepairRequest(access, `${epicRunApiPath(request.epicRunId)}/tickets/${encodeURIComponent(request.ticketKey)}/rerun-ci`, { guard: request.guard }, fetchImpl);
1632
+ }
1633
+ catch (err) {
1634
+ if (err instanceof ConductorBridgeApiError &&
1635
+ !err.errorCode &&
1636
+ (err.status === 404 || err.status === 405)) {
1637
+ throw new ConductorBridgeApiError("http", err.status, {
1638
+ errorCode: RERUN_CI_ROUTE_UNAVAILABLE_CODE,
1639
+ bodyPreview: RERUN_CI_ROUTE_UNAVAILABLE_MESSAGE,
1640
+ });
1641
+ }
1642
+ throw err;
1643
+ }
1644
+ return parseRerunCiResult(parsed);
1645
+ }
1506
1646
  /**
1507
1647
  * POST `/jira/epic-runs/runs/{epic_run_id}/merge-child` — body EXACTLY
1508
1648
  * `{repo_name, pr_number}`. The server composes the head, gate, and action key.
@@ -1662,7 +1802,7 @@ function parseResumeRunResult(parsed) {
1662
1802
  export async function scopeCatchUp(access, request, fetchImpl = globalThis.fetch) {
1663
1803
  requireNonEmptyString(request.epicRunId);
1664
1804
  requireNonEmptyString(request.scopeId);
1665
- const parsed = await postRepairRequest(access, `${epicRunApiPath(request.epicRunId)}/scope-catch-up`, { scope_id: request.scopeId }, fetchImpl);
1805
+ const parsed = await postRepairRequest(access, `${epicRunApiPath(request.epicRunId)}/scope-catch-up`, withGuard({ scope_id: request.scopeId }, request.guard), fetchImpl);
1666
1806
  return parseScopeCatchUpResult(parsed);
1667
1807
  }
1668
1808
  /**
@@ -1682,7 +1822,7 @@ export async function patchPolicy(access, request, fetchImpl = globalThis.fetch)
1682
1822
  Object.keys(patch).length === 0) {
1683
1823
  throw new ConductorBridgeApiError("invalid-input");
1684
1824
  }
1685
- const parsed = await postRepairRequest(access, `${epicRunApiPath(request.epicRunId)}/patch-policy`, { merge_patch: patch }, fetchImpl);
1825
+ const parsed = await postRepairRequest(access, `${epicRunApiPath(request.epicRunId)}/patch-policy`, withGuard({ merge_patch: patch }, request.guard), fetchImpl);
1686
1826
  return parsePatchPolicyResult(parsed);
1687
1827
  }
1688
1828
  /**
@@ -1695,7 +1835,7 @@ export async function resumeRun(access, request, fetchImpl = globalThis.fetch) {
1695
1835
  if (typeof request.clearQuarantine !== "boolean") {
1696
1836
  throw new ConductorBridgeApiError("invalid-input");
1697
1837
  }
1698
- const parsed = await postRepairRequest(access, `${epicRunApiPath(request.epicRunId)}/resume-run`, { clear_quarantine: request.clearQuarantine }, fetchImpl);
1838
+ const parsed = await postRepairRequest(access, `${epicRunApiPath(request.epicRunId)}/resume-run`, withGuard({ clear_quarantine: request.clearQuarantine }, request.guard), fetchImpl);
1699
1839
  return parseResumeRunResult(parsed);
1700
1840
  }
1701
1841
  /**
@@ -2440,6 +2580,20 @@ function requireBool(o, key) {
2440
2580
  throw readinessShapeError();
2441
2581
  return v;
2442
2582
  }
2583
+ /**
2584
+ * An optional boolean field as a spreadable fragment: `{}` when the key is absent,
2585
+ * `{ [key]: value }` when it is a boolean, and a shape error for anything else —
2586
+ * including an explicit `null`. Spreading keeps an absent field ABSENT rather than
2587
+ * present-as-`undefined`.
2588
+ */
2589
+ function optionalBoolField(o, key) {
2590
+ if (!(key in o) || o[key] === undefined)
2591
+ return {};
2592
+ const v = o[key];
2593
+ if (typeof v !== "boolean")
2594
+ throw readinessShapeError();
2595
+ return { [key]: v };
2596
+ }
2443
2597
  function requireInt(o, key) {
2444
2598
  const v = o[key];
2445
2599
  if (typeof v !== "number" || !Number.isInteger(v))
@@ -2588,6 +2742,15 @@ function parseRequiredChecksVerification(sup, requiredChecksCount) {
2588
2742
  required_checks_evidence_fresh: fresh,
2589
2743
  };
2590
2744
  }
2745
+ /** Optional boolean: absent/null from an older server, a non-boolean fails closed. */
2746
+ function parseOptionalBool(o, key) {
2747
+ const v = o[key];
2748
+ if (v === undefined || v === null)
2749
+ return null;
2750
+ if (typeof v !== "boolean")
2751
+ throw readinessShapeError();
2752
+ return v;
2753
+ }
2591
2754
  /** Optional positive-integer threshold: absent/null from an older server. */
2592
2755
  function parseOptionalPositiveInt(o, key) {
2593
2756
  const v = o[key];
@@ -2637,6 +2800,9 @@ export function parseConductorReadinessResponse(body) {
2637
2800
  // Absent on a pre-BAPI-694 server, where no repository default can exist.
2638
2801
  review_policy_present: sup.review_policy_present === true,
2639
2802
  ...parseRequiredChecksVerification(sup, requiredChecksCount),
2803
+ required_checks_retired_gate_configured: parseOptionalBool(sup, "required_checks_retired_gate_configured"),
2804
+ required_checks_ci_workflow_absent: parseOptionalBool(sup, "required_checks_ci_workflow_absent"),
2805
+ required_checks_review_workflow_absent: parseOptionalBool(sup, "required_checks_review_workflow_absent"),
2640
2806
  },
2641
2807
  github: {
2642
2808
  credentials_readable: requireBool(gh, "credentials_readable"),
@@ -2678,8 +2844,29 @@ export function parseConductorReadinessResponse(body) {
2678
2844
  const evidence = parseAdmissionEvidence(root);
2679
2845
  return evidence === undefined ? {} : { admission_evidence: evidence };
2680
2846
  })(),
2847
+ ...(() => {
2848
+ const diagnostics = parseReadinessDiagnostics(root);
2849
+ return diagnostics === undefined ? {} : { diagnostics };
2850
+ })(),
2681
2851
  };
2682
2852
  }
2853
+ function parseReadinessDiagnostics(body) {
2854
+ if (body.diagnostics === undefined || body.diagnostics === null)
2855
+ return undefined;
2856
+ if (!Array.isArray(body.diagnostics) || body.diagnostics.length > 64)
2857
+ throw readinessShapeError();
2858
+ return body.diagnostics.map((entry) => {
2859
+ const item = requireObject(entry);
2860
+ if (typeof item.id !== "string" || !/^[a-z][a-z0-9_.-]{0,79}$/.test(item.id))
2861
+ throw readinessShapeError();
2862
+ const severity = item.severity === "mandatory" || item.severity === "advisory" ? item.severity : null;
2863
+ const applicability = item.applicability === "applicable" || item.applicability === "inapplicable"
2864
+ ? item.applicability : null;
2865
+ const verdict = item.verdict === "passed" || item.verdict === "failed" || item.verdict === "unknown"
2866
+ ? item.verdict : "unknown";
2867
+ return { id: item.id, severity, applicability, verdict };
2868
+ });
2869
+ }
2683
2870
  const ADMISSION_EVIDENCE_OWNERS = new Set(["operator", "repository_admin", "bridge_operations"]);
2684
2871
  const BOUNDED_TOKEN = /^[a-z0-9][a-z0-9_.:-]{0,79}$/;
2685
2872
  /**
@@ -2841,6 +3028,11 @@ function parseConductorCiWorkflow(body) {
2841
3028
  probe_succeeded: requireBool(o, "probe_succeeded"),
2842
3029
  workflow_present: requireBool(o, "workflow_present"),
2843
3030
  migration_guard_present: requireBool(o, "migration_guard_present"),
3031
+ // BAPI-1359: OPTIONAL so a newer CLI still reads an older server. An absent
3032
+ // key stays `undefined` — never materialized as a default — and a present key
3033
+ // must be a real boolean (`null`, strings, numbers, and objects all reject).
3034
+ ...optionalBoolField(o, "migration_guard_required"),
3035
+ ...optionalBoolField(o, "migration_chain_probe_succeeded"),
2844
3036
  };
2845
3037
  }
2846
3038
  /**
@@ -3086,3 +3278,173 @@ export function classifySupervisorBootstrapFailure(err) {
3086
3278
  return "server";
3087
3279
  return "rejected";
3088
3280
  }
3281
+ const CI_STOP_REASONS = new Set(["wait_expired", "verdict_unavailable", "repair_exhausted"]);
3282
+ const RESUME_CI_AUDIT_WARNING_MAX = 200;
3283
+ function isPlainRecord(value) {
3284
+ return value !== null && typeof value === "object" && !Array.isArray(value);
3285
+ }
3286
+ function isNonNegativeInt(value) {
3287
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
3288
+ }
3289
+ function nullableNonNegativeInt(value) {
3290
+ return value === null || value === undefined || isNonNegativeInt(value);
3291
+ }
3292
+ function nullableString(value) {
3293
+ return value === null || value === undefined || typeof value === "string";
3294
+ }
3295
+ function parseCiResumeOperation(value) {
3296
+ if (!isPlainRecord(value))
3297
+ throw new ConductorBridgeApiError("server");
3298
+ const v = value;
3299
+ if (typeof v.resumed_at !== "string" ||
3300
+ !CI_STOP_REASONS.has(v.stop_reason) ||
3301
+ !isNonNegativeInt(v.park_generation) ||
3302
+ !isNonNegativeInt(v.resulting_park_generation) ||
3303
+ v.resulting_park_generation !== v.park_generation + 1 ||
3304
+ v.resumed_gate !== "ci" ||
3305
+ v.ci_gate_status !== "in_progress" ||
3306
+ v.allowance_effect !== "repair_allowances_preserved" ||
3307
+ !isNonNegativeInt(v.preserved_attempts) ||
3308
+ !nullableNonNegativeInt(v.preserved_ci_infra_rerun_attempts_used) ||
3309
+ !nullableNonNegativeInt(v.preserved_entry_attempts) ||
3310
+ !nullableString(v.deadline_at) ||
3311
+ !(v.deadline_source === null || v.deadline_source === undefined ||
3312
+ v.deadline_source === "original_gate" || v.deadline_source === "post_repair") ||
3313
+ // The three fixed consequences are part of the contract, not decoration: a
3314
+ // response claiming an extended deadline, a created job, or a CI pass is not
3315
+ // a resume-ci success and is refused as malformed.
3316
+ v.deadline_extended !== false ||
3317
+ v.jobs_created !== 0 ||
3318
+ v.ci_passed_by_action !== false) {
3319
+ throw new ConductorBridgeApiError("server");
3320
+ }
3321
+ return {
3322
+ resumed_at: v.resumed_at,
3323
+ stop_reason: v.stop_reason,
3324
+ park_generation: v.park_generation,
3325
+ resulting_park_generation: v.resulting_park_generation,
3326
+ resumed_gate: "ci",
3327
+ ci_gate_status: "in_progress",
3328
+ allowance_effect: "repair_allowances_preserved",
3329
+ preserved_attempts: v.preserved_attempts,
3330
+ preserved_ci_infra_rerun_attempts_used: (v.preserved_ci_infra_rerun_attempts_used ?? null),
3331
+ preserved_entry_attempts: (v.preserved_entry_attempts ?? null),
3332
+ deadline_at: (v.deadline_at ?? null),
3333
+ deadline_source: (v.deadline_source ?? null),
3334
+ deadline_extended: false,
3335
+ jobs_created: 0,
3336
+ ci_passed_by_action: false,
3337
+ };
3338
+ }
3339
+ function parseCiResumeCurrentState(value) {
3340
+ if (!isPlainRecord(value))
3341
+ throw new ConductorBridgeApiError("server");
3342
+ const v = value;
3343
+ if (!nullableString(v.ticket_status) ||
3344
+ !nullableString(v.current_gate) ||
3345
+ !nullableString(v.ci_gate_status) ||
3346
+ typeof v.held_by_run_control !== "boolean") {
3347
+ throw new ConductorBridgeApiError("server");
3348
+ }
3349
+ return {
3350
+ ticket_status: (v.ticket_status ?? null),
3351
+ current_gate: (v.current_gate ?? null),
3352
+ ci_gate_status: (v.ci_gate_status ?? null),
3353
+ held_by_run_control: v.held_by_run_control,
3354
+ };
3355
+ }
3356
+ /**
3357
+ * Strictly parse a `resume-ci` success body. Anything short of the full
3358
+ * contract — an unknown outcome, a missing history or current-state block, a
3359
+ * non-boolean audit flag, an over-long warning — throws, and the caller treats
3360
+ * that as an uncertain outcome, never as success.
3361
+ */
3362
+ export function parseResumeCiResult(value) {
3363
+ if (!isPlainRecord(value))
3364
+ throw new ConductorBridgeApiError("server");
3365
+ const v = value;
3366
+ if (typeof v.epic_run_id !== "string" || !v.epic_run_id ||
3367
+ typeof v.ticket_key !== "string" || !v.ticket_key ||
3368
+ (v.outcome !== "committed" && v.outcome !== "replayed") ||
3369
+ typeof v.audit_recorded !== "boolean" ||
3370
+ !(v.audit_warning === null || v.audit_warning === undefined ||
3371
+ (typeof v.audit_warning === "string" && v.audit_warning.length <= RESUME_CI_AUDIT_WARNING_MAX))) {
3372
+ throw new ConductorBridgeApiError("server");
3373
+ }
3374
+ return {
3375
+ epic_run_id: v.epic_run_id,
3376
+ ticket_key: v.ticket_key,
3377
+ outcome: v.outcome,
3378
+ operation: parseCiResumeOperation(v.operation),
3379
+ current: parseCiResumeCurrentState(v.current),
3380
+ audit_recorded: v.audit_recorded,
3381
+ audit_warning: (v.audit_warning ?? null),
3382
+ };
3383
+ }
3384
+ /**
3385
+ * POST `/jira/epic-runs/runs/{epic_run_id}/tickets/{ticket_key}/resume-ci`.
3386
+ *
3387
+ * Sends the pinned operation key and stop binding only in the authenticated
3388
+ * body. Never throws for an HTTP or transport outcome: every response is
3389
+ * classified into a {@link ResumeCiPostOutcome}, so the caller — not this
3390
+ * function — decides whether an uncertain outcome is retried with the SAME
3391
+ * operation.
3392
+ */
3393
+ export async function resumeCiTicket(access, request, fetchImpl = globalThis.fetch, timeoutMs = CONDUCTOR_FETCH_TIMEOUT_MS) {
3394
+ requireNonEmptyString(request.epicRunId);
3395
+ requireNonEmptyString(request.ticketKey);
3396
+ requireNonEmptyString(request.operationKey);
3397
+ requireNonNegativeSafeInteger(request.expectedRowVersion);
3398
+ const url = buildConductorJiraUrl(access.baseUrl, `${continuationChildPath(request.epicRunId, request.ticketKey)}/resume-ci`);
3399
+ const body = JSON.stringify({
3400
+ repo_name: access.repoName,
3401
+ expected_row_version: request.expectedRowVersion,
3402
+ operation_key: request.operationKey,
3403
+ stop_binding: request.stopBinding,
3404
+ });
3405
+ const controller = new AbortController();
3406
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
3407
+ try {
3408
+ let resp;
3409
+ try {
3410
+ resp = await fetchImpl(url, {
3411
+ method: "POST",
3412
+ headers: conductorPostHeaders(access),
3413
+ body,
3414
+ signal: controller.signal,
3415
+ });
3416
+ }
3417
+ catch {
3418
+ return { kind: "uncertain", reason: controller.signal.aborted ? "timeout" : "network" };
3419
+ }
3420
+ let parsed;
3421
+ let parsedOk = true;
3422
+ try {
3423
+ parsed = await resp.json();
3424
+ }
3425
+ catch {
3426
+ parsedOk = false;
3427
+ }
3428
+ if (resp.status >= 200 && resp.status < 300) {
3429
+ if (!parsedOk)
3430
+ return { kind: "uncertain", reason: "malformed", status: resp.status };
3431
+ try {
3432
+ return { kind: "result", result: parseResumeCiResult(parsed) };
3433
+ }
3434
+ catch {
3435
+ return { kind: "uncertain", reason: "malformed", status: resp.status };
3436
+ }
3437
+ }
3438
+ const errorCode = parsedOk ? safeDetailErrorCode(parsed) : null;
3439
+ if (errorCode !== null && isCiResumeRefusalCode(errorCode)) {
3440
+ return { kind: "refused", status: resp.status, errorCode };
3441
+ }
3442
+ if (resp.status >= 500) {
3443
+ return { kind: "uncertain", reason: "server", status: resp.status };
3444
+ }
3445
+ return { kind: "rejected", status: resp.status, errorCode };
3446
+ }
3447
+ finally {
3448
+ clearTimeout(timer);
3449
+ }
3450
+ }
@@ -1,5 +1,5 @@
1
1
  /** The pinned operator runbook every remedy below points into. */
2
- export const BRING_UP_RUNBOOK = "docs/claude/epic-implementer-v2-operator-runbook.md";
2
+ export const BRING_UP_RUNBOOK = "docs/epic-implementer/operator-runbook.md";
3
3
  /**
4
4
  * The client-derived refusal vocabulary, IN REFUSAL ORDER.
5
5
  *
@@ -822,9 +822,9 @@ export function formatConductorDoctorReport(report) {
822
822
  lines.push(` - ${w}`);
823
823
  }
824
824
  lines.push("");
825
- lines.push("Epic Conductor v2 reconciles epics server-side — nothing to schedule");
826
- lines.push("locally. To execute claimed jobs on this machine, run:");
827
- lines.push(` npx -y ${MCP_PACKAGE_NAME} executor --repo <name>`);
825
+ lines.push("The Epic Implementer reconciles epics server-side — nothing to schedule");
826
+ lines.push("locally. To execute a run's claimed jobs on this machine, run:");
827
+ lines.push(` npx -y ${MCP_PACKAGE_NAME} executor --repo <name> --epic-run-id <run-id>`);
828
828
  lines.push("");
829
829
  lines.push("MCP Profile (optional, local)");
830
830
  lines.push("─────────────────────────────");
@@ -222,21 +222,74 @@ export function parseContinuationStatusResult(value) {
222
222
  receipt,
223
223
  };
224
224
  }
225
- /** Directory holding recovery references: beside the user credential store. */
226
- export function getContinuationReferenceDirectory(deps = {}) {
225
+ /** The namespace (subdirectory) of the parked-child continuation references. */
226
+ export const CONTINUATION_REFERENCE_NAMESPACE = "epic-child-continuations";
227
+ /**
228
+ * Directory holding one namespace of recovery references, beside the user
229
+ * credential store. BAPI-1326's `resume-ci` keeps its pinned operations in its
230
+ * own namespace through this same store, so the two recoveries share one file
231
+ * format, one permission model, and one atomic-write path.
232
+ */
233
+ export function getRecoveryReferenceDirectory(namespace, deps = {}) {
227
234
  const credentials = getPrimaryCredentialStorePath({
228
235
  env: deps.env ?? process.env,
229
236
  homedir: deps.homedir ?? os.homedir,
230
237
  });
231
- return path.join(path.dirname(credentials), "epic-child-continuations");
238
+ return path.join(path.dirname(credentials), namespace);
239
+ }
240
+ /** Directory holding continuation recovery references. */
241
+ export function getContinuationReferenceDirectory(deps = {}) {
242
+ return getRecoveryReferenceDirectory(CONTINUATION_REFERENCE_NAMESPACE, deps);
232
243
  }
233
244
  function referenceFileName(epicRunId, ticketKey) {
234
245
  // Hashed so an arbitrary run id or ticket key can never form a path segment.
235
246
  const digest = createHash("sha256").update(`${epicRunId}\u0000${ticketKey}`, "utf8").digest("hex");
236
247
  return `${digest.slice(0, 32)}.json`;
237
248
  }
249
+ /** The file one (run, ticket) reference occupies within a namespace. */
250
+ export function getRecoveryReferencePath(namespace, epicRunId, ticketKey, deps = {}) {
251
+ return path.join(getRecoveryReferenceDirectory(namespace, deps), referenceFileName(epicRunId, ticketKey));
252
+ }
238
253
  export function getContinuationReferencePath(epicRunId, ticketKey, deps = {}) {
239
- return path.join(getContinuationReferenceDirectory(deps), referenceFileName(epicRunId, ticketKey));
254
+ return getRecoveryReferencePath(CONTINUATION_REFERENCE_NAMESPACE, epicRunId, ticketKey, deps);
255
+ }
256
+ /** Read one namespaced reference as parsed JSON, or `null` when absent or unreadable. */
257
+ export async function readRecoveryReferenceFile(namespace, key, deps = {}) {
258
+ const read = deps.readFile ?? fsReadFile;
259
+ let text;
260
+ try {
261
+ text = (await read(getRecoveryReferencePath(namespace, key.epicRunId, key.ticketKey, deps), "utf8"));
262
+ }
263
+ catch {
264
+ return null;
265
+ }
266
+ try {
267
+ return JSON.parse(text);
268
+ }
269
+ catch {
270
+ return null;
271
+ }
272
+ }
273
+ /** Durably write one namespaced reference (0700 directory, 0600 file, write-then-rename). */
274
+ export async function writeRecoveryReferenceFile(namespace, key, content, deps = {}) {
275
+ const mkdir = deps.mkdir ?? fsMkdir;
276
+ const writeFile = deps.writeFile ?? fsWriteFile;
277
+ const rename = deps.rename ?? fsRename;
278
+ await mkdir(getRecoveryReferenceDirectory(namespace, deps), { recursive: true, mode: 0o700 });
279
+ const target = getRecoveryReferencePath(namespace, key.epicRunId, key.ticketKey, deps);
280
+ const temp = `${target}.${process.pid}.tmp`;
281
+ await writeFile(temp, `${JSON.stringify(content, null, 2)}\n`, { mode: 0o600 });
282
+ await rename(temp, target);
283
+ }
284
+ /** Remove one namespaced reference. Never throws. */
285
+ export async function clearRecoveryReferenceFile(namespace, key, deps = {}) {
286
+ const unlink = deps.unlink ?? fsUnlink;
287
+ try {
288
+ await unlink(getRecoveryReferencePath(namespace, key.epicRunId, key.ticketKey, deps));
289
+ }
290
+ catch {
291
+ // Already gone — nothing to clear.
292
+ }
240
293
  }
241
294
  function parseReference(raw) {
242
295
  if (!isRecord(raw) || raw.version !== 1 || !isRecord(raw.bindings))
@@ -276,44 +329,18 @@ function parseReference(raw) {
276
329
  }
277
330
  /** Read the in-flight reference for a child, or `null` when none (or unusable). */
278
331
  export async function readContinuationReference(epicRunId, ticketKey, deps = {}) {
279
- const read = deps.readFile ?? fsReadFile;
280
- let text;
281
- try {
282
- text = (await read(getContinuationReferencePath(epicRunId, ticketKey, deps), "utf8"));
283
- }
284
- catch {
285
- return null;
286
- }
287
- try {
288
- return parseReference(JSON.parse(text));
289
- }
290
- catch {
291
- return null;
292
- }
332
+ const raw = await readRecoveryReferenceFile(CONTINUATION_REFERENCE_NAMESPACE, { epicRunId, ticketKey }, deps);
333
+ return raw === null ? null : parseReference(raw);
293
334
  }
294
335
  /** Durably write a reference (0700 directory, 0600 file, write-then-rename). */
295
336
  export async function writeContinuationReference(reference, deps = {}, key = reference.bindings) {
296
- const mkdir = deps.mkdir ?? fsMkdir;
297
- const writeFile = deps.writeFile ?? fsWriteFile;
298
- const rename = deps.rename ?? fsRename;
299
- const dir = getContinuationReferenceDirectory(deps);
300
- await mkdir(dir, { recursive: true, mode: 0o700 });
301
337
  // ``key`` only names the file (the caller may address the run by its epic key);
302
338
  // the stored bindings are always the exact, fingerprinted ones.
303
- const target = getContinuationReferencePath(key.epicRunId, key.ticketKey, deps);
304
- const temp = `${target}.${process.pid}.tmp`;
305
- await writeFile(temp, `${JSON.stringify(reference, null, 2)}\n`, { mode: 0o600 });
306
- await rename(temp, target);
339
+ await writeRecoveryReferenceFile(CONTINUATION_REFERENCE_NAMESPACE, key, reference, deps);
307
340
  }
308
341
  /** Remove a child's reference once its outcome is authoritative. Never throws. */
309
342
  export async function clearContinuationReference(epicRunId, ticketKey, deps = {}) {
310
- const unlink = deps.unlink ?? fsUnlink;
311
- try {
312
- await unlink(getContinuationReferencePath(epicRunId, ticketKey, deps));
313
- }
314
- catch {
315
- // Already gone — nothing to clear.
316
- }
343
+ await clearRecoveryReferenceFile(CONTINUATION_REFERENCE_NAMESPACE, { epicRunId, ticketKey }, deps);
317
344
  }
318
345
  /** A fresh request id: a UUID, which satisfies the server's request-id bound. */
319
346
  export function newContinuationRequestId() {