@bridge_gpt/mcp-server 0.2.69 → 0.2.70

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (151) hide show
  1. package/README.md +140 -292
  2. package/build/agent-launchers/claude-executor-adapter.js +17 -4
  3. package/build/agents.generated.js +4 -4
  4. package/build/artifact-diff.js +110 -0
  5. package/build/bin-entrypoint.js +36 -0
  6. package/build/bridge-permissions.js +23 -4
  7. package/build/claude-review-workflow-drift.js +3 -3
  8. package/build/codex-bridge-profile.js +1 -1
  9. package/build/commands.generated.js +6 -6
  10. package/build/conductor/bridge-api-client.js +598 -19
  11. package/build/conductor/bring-up-facts.js +9 -9
  12. package/build/conductor/doctor.js +466 -18
  13. package/build/conductor/epic-child-continuation.js +54 -3
  14. package/build/conductor/epic-implementer-cli.js +132 -34
  15. package/build/conductor/epic-runtime.js +2 -2
  16. package/build/conductor/epic-state.js +1 -1
  17. package/build/conductor/hook-runtime-qualification.js +1 -1
  18. package/build/conductor/install-doctor.js +4 -2
  19. package/build/conductor/launch-need-probes.js +337 -0
  20. package/build/conductor/local-merge.js +241 -56
  21. package/build/conductor/merge-credential.js +172 -0
  22. package/build/conductor/merge-execution-eligibility.js +1 -1
  23. package/build/conductor/merged-child-check.js +95 -0
  24. package/build/conductor/plan-trim.js +30 -0
  25. package/build/conductor/pr-ci-producer.js +1 -1
  26. package/build/conductor/readiness-cli.js +11 -6
  27. package/build/conductor/readiness-prerequisites.js +41 -0
  28. package/build/conductor/readiness-sections.js +345 -55
  29. package/build/conductor/readiness.js +99 -87
  30. package/build/conductor/recovery-cli.js +606 -21
  31. package/build/conductor/recovery-operations.js +306 -7
  32. package/build/conductor/repair-contract.js +47 -2
  33. package/build/conductor/repository-workflow-remedy.js +8 -6
  34. package/build/conductor/stored-merge-credential.js +76 -0
  35. package/build/conductor/stored-merge-token-probe.js +28 -0
  36. package/build/conductor/temporary-plan.js +50 -0
  37. package/build/conductor/wait-run-cli.js +759 -0
  38. package/build/conductor/worker-database-doctor-inspector.js +49 -0
  39. package/build/conductor-bin.js +2 -2
  40. package/build/conductor-claude-hook-bin.js +2 -2
  41. package/build/conductor-claude-hook-removed-stub-bin.js +2 -5
  42. package/build/conductor-removed-stub-bin.js +2 -5
  43. package/build/credential-store.js +356 -13
  44. package/build/docs.generated.js +26 -13
  45. package/build/doctor.js +94 -32
  46. package/build/drive-epic-help.js +212 -0
  47. package/build/drive-epic.js +1840 -373
  48. package/build/epic-ci-workflow-drift.js +77 -0
  49. package/build/epic-ci-workflow-install.js +183 -0
  50. package/build/epic-ci-workflow.js +157 -0
  51. package/build/epic-contract-context.js +326 -0
  52. package/build/epic-implementer-bin.js +57 -41
  53. package/build/epic-implementer-claude-hook-bin.js +2 -2
  54. package/build/escalation-reachability.js +134 -0
  55. package/build/executor/cleanup-report.js +99 -0
  56. package/build/executor/cli.js +12 -1
  57. package/build/executor/completion-contract.js +66 -11
  58. package/build/executor/completion-supervision.js +99 -0
  59. package/build/executor/delivery-outcome.js +179 -0
  60. package/build/executor/deps.js +3 -0
  61. package/build/executor/env.js +71 -0
  62. package/build/executor/failure-reason-codes.js +126 -0
  63. package/build/executor/heartbeat.js +38 -0
  64. package/build/executor/http-client.js +72 -0
  65. package/build/executor/integration-job.js +298 -5
  66. package/build/executor/job-errors.js +36 -6
  67. package/build/executor/job-runner.js +720 -36
  68. package/build/executor/likely-tests.js +36 -2
  69. package/build/executor/merge-job.js +145 -2
  70. package/build/executor/observation.js +73 -4
  71. package/build/executor/permissions.js +193 -97
  72. package/build/executor/preflight.js +14 -3
  73. package/build/executor/process-containment.js +28 -2
  74. package/build/executor/prompt-spec.js +50 -5
  75. package/build/executor/rebase-finalization.js +38 -3
  76. package/build/executor/reconcile-overlap-job.js +35 -7
  77. package/build/executor/required-work-ledger.js +236 -81
  78. package/build/executor/resume-pre-spawn.js +38 -1
  79. package/build/executor/runner.js +91 -3
  80. package/build/executor/test-clock.js +8 -0
  81. package/build/executor/types.js +37 -2
  82. package/build/executor/worker-cli-launcher.js +259 -0
  83. package/build/executor/worker-command.js +41 -4
  84. package/build/executor/worker-database-target.js +91 -0
  85. package/build/executor/worker-database.js +908 -0
  86. package/build/executor/worker-finalization.js +106 -0
  87. package/build/executor/worker-guard-hook.js +203 -0
  88. package/build/executor/worker-resources.js +47 -0
  89. package/build/executor/worker-verification-guidance.js +15 -2
  90. package/build/executor/worktree.js +303 -15
  91. package/build/executor-launch.js +11 -1
  92. package/build/generated-assets-doctor.js +9 -1
  93. package/build/generated-assets-health.js +13 -4
  94. package/build/generated-assets-legacy.js +39 -0
  95. package/build/generated-assets-paths.js +12 -1
  96. package/build/generated-assets-repair.js +4 -1
  97. package/build/generated-assets-test-support.js +12 -0
  98. package/build/github-remote.js +33 -0
  99. package/build/github-sign-in.js +62 -0
  100. package/build/index.js +1067 -536
  101. package/build/init.js +39 -1
  102. package/build/install-bridge-conductor-gate.js +78 -0
  103. package/build/install-bridge-conductor-setup.js +413 -0
  104. package/build/install-bridge-conductor.js +1226 -267
  105. package/build/install-bridge.js +376 -60
  106. package/build/mcp-install-state.js +8 -2
  107. package/build/merge-pull-request.js +6 -0
  108. package/build/merge-workflow-install.js +763 -345
  109. package/build/phase-result-artifacts.js +75 -20
  110. package/build/pipeline-orchestrator.js +247 -4
  111. package/build/pipelines.generated.js +35 -35
  112. package/build/plan-discovery.js +15 -2
  113. package/build/plan-execution-ledger.js +306 -17
  114. package/build/plan-phase-routing.js +48 -13
  115. package/build/plane/cli.js +105 -51
  116. package/build/plane/defaults.js +3 -0
  117. package/build/plane/plane-freshness.js +294 -0
  118. package/build/plane/schema-compatibility.js +137 -0
  119. package/build/plane/shutdown-deps.js +38 -0
  120. package/build/plane/shutdown.js +83 -8
  121. package/build/plane/terminal-run-cleanup.js +303 -0
  122. package/build/printed-remedy-commands.js +26 -4
  123. package/build/readiness-check.js +6 -2
  124. package/build/readiness-prerequisites.generated.js +532 -0
  125. package/build/readme.generated.js +1 -1
  126. package/build/setup-epic.js +400 -30
  127. package/build/shared-resources.generated.js +5 -0
  128. package/build/start-tickets.js +28 -15
  129. package/build/test-command-detection.js +247 -0
  130. package/build/test-command-validation.js +73 -0
  131. package/build/testing-config-client.js +150 -0
  132. package/build/ticket-delivery-artifacts.js +258 -0
  133. package/build/ticket-delivery-enrollment.js +306 -0
  134. package/build/ticket-delivery.js +205 -0
  135. package/build/ticket-review-summary.js +14 -0
  136. package/build/upgrade-cli.js +13 -0
  137. package/build/v2-readiness-requirements.js +552 -0
  138. package/build/version.generated.js +3 -3
  139. package/build/worker-guard-hook-bin.js +2 -2
  140. package/build/worktree-core.js +22 -7
  141. package/docs/BRIDGE-DIRECTORY-README.md +48 -0
  142. package/docs/EPIC-IMPLEMENTER-INSTALL.md +12 -867
  143. package/docs/EPIC-IMPLEMENTER.md +104 -63
  144. package/docs/epic-implementer/getting-started.md +131 -0
  145. package/docs/epic-implementer/self-hosting.md +133 -0
  146. package/package.json +3 -3
  147. package/pipelines/idea-to-ticket.json +1 -1
  148. package/pipelines/implement-ticket.json +8 -8
  149. package/pipelines/review-ticket.json +2 -2
  150. package/design-assets/favicon/favicon-32x32.png +0 -0
  151. package/design-assets/just-logo-rough-draft.png +0 -0
@@ -1,3 +1,5 @@
1
+ import { READINESS_PREREQUISITE_BY_ID } from "../readiness-prerequisites.generated.js";
2
+ import { READINESS_PREREQUISITE_FIXES } from "../printed-remedy-commands.js";
1
3
  /**
2
4
  * TypeScript-side Bridge API access layer for the conductor producer (BAPI-395).
3
5
  *
@@ -193,6 +195,31 @@ const CONDUCTOR_BRIDGE_API_ERROR_KINDS = [
193
195
  ];
194
196
  /** Max length of the sanitized body preview embedded in error diagnostics. */
195
197
  const CONDUCTOR_ERROR_PREVIEW_MAX = 200;
198
+ /** BAPI-1455 — the stable readiness refusal code for a proven-missing local schema object. */
199
+ export const SCHEMA_INCOMPATIBLE_ERROR_CODE = "SCHEMA_INCOMPATIBLE";
200
+ /** BAPI-1455 — the bound for that refusal's server-authored explanation. */
201
+ const SCHEMA_REFUSAL_PREVIEW_MAX = 2000;
202
+ /** Whether a readiness read failed because the local schema is proven incompatible. */
203
+ export function isSchemaIncompatibleError(err) {
204
+ return err instanceof ConductorBridgeApiError && err.errorCode === SCHEMA_INCOMPATIBLE_ERROR_CODE;
205
+ }
206
+ /** BAPI-1455 — the fixed repair route. Never `alembic stamp`, never a downgrade. */
207
+ export const SCHEMA_INCOMPATIBLE_REPAIR_ROUTE = "Write a reviewed, forward-only Alembic migration that restores the missing objects, " +
208
+ "or rebuild the local database only with the owner's explicit consent.";
209
+ /**
210
+ * The operator lines for a `SCHEMA_INCOMPATIBLE` readiness refusal. The server's
211
+ * explanation (applied stamp and missing objects, from its maintained list) is
212
+ * shown when present; the heading and the repair route are fixed text.
213
+ */
214
+ export function formatSchemaIncompatibleRefusal(subject, err) {
215
+ return [
216
+ `${subject}: REFUSED (schema-incompatible) — Local schema incompatible. ` +
217
+ "Launch-critical schema objects are missing, so no run was created.",
218
+ ...(err.bodyPreview ? [` ${err.bodyPreview}`] : []),
219
+ ` Repair: ${SCHEMA_INCOMPATIBLE_REPAIR_ROUTE}`,
220
+ " A migration stamp is not proof of schema; `alembic stamp` cannot restore missing DDL.",
221
+ ];
222
+ }
196
223
  /**
197
224
  * Redact obvious secret-shaped tokens from a diagnostic preview string so a
198
225
  * backend message that happens to echo a token/header never reaches a log line.
@@ -243,11 +270,9 @@ function stripWebhookUrlsDeep(value, depth = 0) {
243
270
  return out;
244
271
  }
245
272
  /** Collapse whitespace, redact secrets, and bound a string to the preview cap. */
246
- function boundedErrorPreview(text) {
273
+ function boundedErrorPreview(text, max = CONDUCTOR_ERROR_PREVIEW_MAX) {
247
274
  const redacted = redactErrorPreview(text).replace(/\s+/g, " ").trim();
248
- return redacted.length > CONDUCTOR_ERROR_PREVIEW_MAX
249
- ? `${redacted.slice(0, CONDUCTOR_ERROR_PREVIEW_MAX)}…`
250
- : redacted;
275
+ return redacted.length > max ? `${redacted.slice(0, max)}…` : redacted;
251
276
  }
252
277
  /** Max number of FastAPI `loc` components rendered into a diagnostic path. */
253
278
  const CONDUCTOR_VALIDATION_LOC_MAX_PARTS = 8;
@@ -369,7 +394,9 @@ async function readSanitizedErrorDiagnostics(resp, headers = {}, options = {}) {
369
394
  try {
370
395
  const parsed = stripWebhookUrlsDeep(await resp.json());
371
396
  const diagnostics = extractSanitizedErrorDiagnostics(parsed);
372
- if (options.readErrorField && !diagnostics.bodyPreview) {
397
+ const codeAllowed = options.readErrorFieldOnlyFor === undefined ||
398
+ (diagnostics.errorCode !== undefined && options.readErrorFieldOnlyFor.includes(diagnostics.errorCode));
399
+ if (options.readErrorField && codeAllowed && !diagnostics.bodyPreview) {
373
400
  // BAPI-1154: opt-in only. The route layer's `_http_error` envelope carries
374
401
  // its server-authored explanation in `detail.error`, which the shared
375
402
  // extractor deliberately does not read (pinned by its own tests). The
@@ -382,7 +409,7 @@ async function readSanitizedErrorDiagnostics(resp, headers = {}, options = {}) {
382
409
  ? detail["error"]
383
410
  : undefined;
384
411
  if (typeof error === "string" && error.trim()) {
385
- diagnostics.bodyPreview = boundedErrorPreview(error.trim());
412
+ diagnostics.bodyPreview = boundedErrorPreview(error.trim(), options.errorFieldPreviewMax);
386
413
  }
387
414
  }
388
415
  const secrets = Object.entries(headers)
@@ -488,7 +515,7 @@ export function conductorGetHeaders(access) {
488
515
  * non-2xx response — never including the response body, headers, or API key. The
489
516
  * timer is always cleared.
490
517
  */
491
- export async function fetchConductorJsonWithTimeout(url, headers, timeoutMs, fetchImpl = globalThis.fetch) {
518
+ export async function fetchConductorJsonWithTimeout(url, headers, timeoutMs, fetchImpl = globalThis.fetch, errorOptions = {}) {
492
519
  const controller = new AbortController();
493
520
  const timer = setTimeout(() => controller.abort(), timeoutMs);
494
521
  try {
@@ -501,7 +528,7 @@ export async function fetchConductorJsonWithTimeout(url, headers, timeoutMs, fet
501
528
  throw new ConductorBridgeApiError(controller.signal.aborted ? "timeout" : "network");
502
529
  }
503
530
  if (!resp.ok) {
504
- const diagnostics = await readSanitizedErrorDiagnostics(resp, headers);
531
+ const diagnostics = await readSanitizedErrorDiagnostics(resp, headers, errorOptions);
505
532
  if (resp.status === 401 || resp.status === 403) {
506
533
  throw new ConductorBridgeApiError("unauthorized", resp.status, diagnostics);
507
534
  }
@@ -1037,6 +1064,41 @@ export async function fetchActiveEpicRuns(access, fetchImpl = globalThis.fetch)
1037
1064
  }
1038
1065
  return [];
1039
1066
  }
1067
+ /** Every status after which a run can still do work. */
1068
+ const NON_TERMINAL_EPIC_RUN_STATUSES = ["planning", "pending_approval", "active", "blocked"];
1069
+ const NON_TERMINAL_EPIC_RUN_PAGE_LIMIT = 100;
1070
+ /**
1071
+ * The ids of every non-terminal epic run the server holds for the access's
1072
+ * repository (BAPI-1496 plane cleanup). `null` unless the answer is CONCLUSIVE:
1073
+ * any failed read, malformed body, or full page (which may hide more runs)
1074
+ * returns `null`, never a shorter list. Never throws.
1075
+ */
1076
+ export async function listNonTerminalEpicRunIds(access, fetchImpl = globalThis.fetch) {
1077
+ const ids = [];
1078
+ try {
1079
+ for (const status of NON_TERMINAL_EPIC_RUN_STATUSES) {
1080
+ const url = buildConductorJiraUrl(access.baseUrl, `${EPIC_RUNS_API_PREFIX}/runs`, {
1081
+ repo_name: access.repoName,
1082
+ status,
1083
+ limit: String(NON_TERMINAL_EPIC_RUN_PAGE_LIMIT),
1084
+ });
1085
+ const parsed = await fetchConductorJsonWithTimeout(url, conductorGetHeaders(access), CONDUCTOR_FETCH_TIMEOUT_MS, fetchImpl);
1086
+ const runs = parsed && typeof parsed === "object" ? parsed.runs : undefined;
1087
+ if (!Array.isArray(runs) || runs.length >= NON_TERMINAL_EPIC_RUN_PAGE_LIMIT)
1088
+ return null;
1089
+ for (const run of runs) {
1090
+ const id = run && typeof run === "object" ? run.epic_run_id : undefined;
1091
+ if (typeof id !== "string" || id.length === 0)
1092
+ return null;
1093
+ ids.push(id);
1094
+ }
1095
+ }
1096
+ }
1097
+ catch {
1098
+ return null;
1099
+ }
1100
+ return ids;
1101
+ }
1040
1102
  /**
1041
1103
  * POST `/jira/epic-runs/runs` to create the durable epic run row.
1042
1104
  *
@@ -1327,6 +1389,7 @@ export async function postContinueParkedChild(access, request, fetchImpl = globa
1327
1389
  request_fingerprint: request.requestFingerprint,
1328
1390
  ...(request.reason ? { reason: request.reason } : {}),
1329
1391
  ...(request.workerExitProof ? { worker_exit_proof: request.workerExitProof } : {}),
1392
+ ...(request.driverContext !== undefined ? { driver_context: request.driverContext } : {}),
1330
1393
  });
1331
1394
  const controller = new AbortController();
1332
1395
  const timer = setTimeout(() => controller.abort(), timeoutMs);
@@ -1416,9 +1479,15 @@ export async function postGrantContinuation(access, request, fetchImpl = globalT
1416
1479
  typeof body.replayed === "boolean" &&
1417
1480
  grant && typeof grant === "object" &&
1418
1481
  grant.request_id === request.requestId) {
1482
+ const fields = body;
1483
+ const replayed = fields.replayed;
1419
1484
  return {
1420
1485
  kind: "result", requestId: request.requestId,
1421
- replayed: body.replayed,
1486
+ replayed,
1487
+ committed: typeof fields.committed === "boolean" ? fields.committed : !replayed,
1488
+ sourceParkGeneration: typeof fields.source_park_generation === "number"
1489
+ ? fields.source_park_generation : null,
1490
+ gate: typeof fields.gate === "string" ? fields.gate : null,
1422
1491
  };
1423
1492
  }
1424
1493
  if (response.status >= 500 || response.ok) {
@@ -1584,6 +1653,75 @@ export async function dispatchRemediation(access, request, fetchImpl = globalThi
1584
1653
  const parsed = await postRepairRequest(access, `${epicRunApiPath(request.epicRunId)}/tickets/${encodeURIComponent(request.ticketKey)}/dispatch-remediation`, withGuard({ context: request.context }, request.guard), fetchImpl);
1585
1654
  return parseDispatchRemediationResult(parsed);
1586
1655
  }
1656
+ function parseHandoffResolutionRecord(value) {
1657
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
1658
+ throw new ConductorBridgeApiError("server");
1659
+ }
1660
+ const r = value;
1661
+ const steps = r["steps"];
1662
+ if (!Array.isArray(steps) ||
1663
+ !steps.every((step) => typeof step === "number" && Number.isSafeInteger(step) && step > 0) ||
1664
+ typeof r["binding_attempt_id"] !== "string" ||
1665
+ typeof r["evidence_revision"] !== "number" ||
1666
+ typeof r["epic_head_sha"] !== "string" ||
1667
+ typeof r["evidence_ref"] !== "string" ||
1668
+ typeof r["actor"] !== "string" ||
1669
+ (r["kind"] !== "verified" && r["kind"] !== "risk_accepted") ||
1670
+ typeof r["reason"] !== "string" ||
1671
+ typeof r["at"] !== "string") {
1672
+ throw new ConductorBridgeApiError("server");
1673
+ }
1674
+ return {
1675
+ steps: steps,
1676
+ binding_attempt_id: r["binding_attempt_id"],
1677
+ evidence_revision: r["evidence_revision"],
1678
+ epic_head_sha: r["epic_head_sha"],
1679
+ evidence_ref: r["evidence_ref"],
1680
+ actor: r["actor"],
1681
+ kind: r["kind"],
1682
+ reason: r["reason"],
1683
+ at: r["at"],
1684
+ };
1685
+ }
1686
+ function parseResolveHandoffResult(parsed) {
1687
+ const p = parseRepairBase(parsed);
1688
+ const remaining = p["remaining_unresolved"];
1689
+ if (typeof p["ticket_key"] !== "string" ||
1690
+ !Array.isArray(p["resolutions"]) ||
1691
+ typeof remaining !== "number" ||
1692
+ !Number.isSafeInteger(remaining) ||
1693
+ remaining < 0) {
1694
+ throw new ConductorBridgeApiError("server");
1695
+ }
1696
+ return {
1697
+ epic_run_id: p["epic_run_id"],
1698
+ ticket_key: p["ticket_key"],
1699
+ committed: p["committed"],
1700
+ event_recorded: p["event_recorded"],
1701
+ resolutions: p["resolutions"].map(parseHandoffResolutionRecord),
1702
+ remaining_unresolved: remaining,
1703
+ };
1704
+ }
1705
+ /**
1706
+ * POST `/jira/epic-runs/runs/{epic_run_id}/tickets/{ticket_key}/resolve-handoff`
1707
+ * (BAPI-1494) — body `{repo_name, steps, kind, evidence_ref, reason}` and nothing
1708
+ * else. There is no actor field: the SERVER records the verified request actor.
1709
+ */
1710
+ export async function resolveHandoff(access, request, fetchImpl = globalThis.fetch) {
1711
+ requireNonEmptyString(request.epicRunId);
1712
+ requireNonEmptyString(request.ticketKey);
1713
+ const parsed = await postRepairRequest(access, `${epicRunApiPath(request.epicRunId)}/tickets/${encodeURIComponent(request.ticketKey)}/resolve-handoff`, {
1714
+ steps: request.steps,
1715
+ kind: request.kind,
1716
+ evidence_ref: request.evidenceRef,
1717
+ reason: request.reason,
1718
+ }, fetchImpl);
1719
+ return parseResolveHandoffResult(parsed);
1720
+ }
1721
+ /** The synthetic code an old server's missing escalate route is reported under. */
1722
+ export const ESCALATE_ROUTE_UNAVAILABLE_CODE = "ESCALATE_ROUTE_UNAVAILABLE";
1723
+ /** The fixed operator text for a server that has no `escalate` route. */
1724
+ export const ESCALATE_ROUTE_UNAVAILABLE_MESSAGE = "escalate is not available on this server; the Bridge API server needs an upgrade before using it";
1587
1725
  export async function escalateTicket(access, request, fetchImpl = globalThis.fetch) {
1588
1726
  requireNonEmptyString(request.epicRunId);
1589
1727
  requireNonEmptyString(request.ticketKey);
@@ -1591,7 +1729,22 @@ export async function escalateTicket(access, request, fetchImpl = globalThis.fet
1591
1729
  requireNonEmptyString(request.actionNeeded);
1592
1730
  if (request.actionNeeded.length > 300)
1593
1731
  throw new ConductorBridgeApiError("server");
1594
- const result = await postRepairRequest(access, `${epicRunApiPath(request.epicRunId)}/tickets/${encodeURIComponent(request.ticketKey)}/escalate`, { cause: request.cause, action_needed: request.actionNeeded }, fetchImpl);
1732
+ let result;
1733
+ try {
1734
+ result = await postRepairRequest(access, `${epicRunApiPath(request.epicRunId)}/tickets/${encodeURIComponent(request.ticketKey)}/escalate`, { cause: request.cause, action_needed: request.actionNeeded }, fetchImpl);
1735
+ }
1736
+ catch (err) {
1737
+ // An untyped 404/405 means the server predates the route; a server-coded
1738
+ // refusal keeps its own code and reason.
1739
+ if (err instanceof ConductorBridgeApiError && !err.errorCode &&
1740
+ (err.status === 404 || err.status === 405)) {
1741
+ throw new ConductorBridgeApiError("http", err.status, {
1742
+ errorCode: ESCALATE_ROUTE_UNAVAILABLE_CODE,
1743
+ bodyPreview: ESCALATE_ROUTE_UNAVAILABLE_MESSAGE,
1744
+ });
1745
+ }
1746
+ throw err;
1747
+ }
1595
1748
  const value = result;
1596
1749
  if (!value || !["created", "deduplicated", "suppressed"].includes(value.status) ||
1597
1750
  !Number.isSafeInteger(value.notification_id) || value.notification_id <= 0 ||
@@ -2259,6 +2412,149 @@ function safeStringArray(value) {
2259
2412
  return [];
2260
2413
  return value.filter((v) => typeof v === "string" && v !== "");
2261
2414
  }
2415
+ // ---------------------------------------------------------------------------
2416
+ // BAPI-1510 — already-merged child detection and skipped-child audit clients
2417
+ // ---------------------------------------------------------------------------
2418
+ export const CHILD_MERGE_CLASSIFICATIONS = ["done", "probably_done", "not_done", "unknown"];
2419
+ export const CHILD_MERGE_EVIDENCE_SOURCES = ["delivery_binding", "feature_branch", "none"];
2420
+ export const CHILD_MERGE_ANCESTRY_VALUES = ["covered", "behind", "unknown", "not_checked"];
2421
+ export const CHILD_MERGE_REASONS = [
2422
+ "merged_on_cut_commit",
2423
+ "merged_without_delivery_record",
2424
+ "merged_after_index",
2425
+ "ancestry_unknown",
2426
+ "no_merged_pr_found",
2427
+ "binding_ambiguous",
2428
+ "provider_unavailable",
2429
+ "binding_read_failed",
2430
+ "merge_sha_unavailable",
2431
+ "ancestry_unavailable_for_provider",
2432
+ "deadline_exceeded",
2433
+ ];
2434
+ const FULL_COMMIT_SHA_PATTERN = /^[0-9a-f]{40}$/;
2435
+ function isOneOf(allowed, value) {
2436
+ return typeof value === "string" && allowed.includes(value);
2437
+ }
2438
+ /**
2439
+ * POST `/jira/epic-runs/child-merge-status`: read-only classification of plan
2440
+ * children against one indexed cut commit. Creates no run, event, or charge.
2441
+ *
2442
+ * The response is validated strictly: a missing, duplicated, or reordered child,
2443
+ * an out-of-vocabulary value, a malformed PR number or SHA, or an echoed cut that
2444
+ * differs from the requested one is a protocol error. A malformed body must never
2445
+ * reach plan trimming.
2446
+ */
2447
+ export async function classifyChildMergeStatus(access, request, fetchImpl = globalThis.fetch) {
2448
+ const url = buildConductorJiraUrl(access.baseUrl, `${EPIC_RUNS_API_PREFIX}/child-merge-status`);
2449
+ const parsed = await fetchConductorJsonPostWithTimeout(url, conductorPostHeaders(access), JSON.stringify({
2450
+ repo_name: access.repoName,
2451
+ epic_key: request.epicKey,
2452
+ child_keys: [...request.childKeys],
2453
+ cut_commit_sha: request.cutCommitSha,
2454
+ }), CONDUCTOR_FETCH_TIMEOUT_MS, fetchImpl);
2455
+ return parseChildMergeStatusResponse(parsed, request);
2456
+ }
2457
+ /** Strict parse of the classification body; throws a sanitized error on any deviation. */
2458
+ export function parseChildMergeStatusResponse(parsed, request) {
2459
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
2460
+ throw new ConductorBridgeApiError("server");
2461
+ }
2462
+ const p = parsed;
2463
+ const cut = p["cut_commit_sha"];
2464
+ const children = p["children"];
2465
+ if (typeof cut !== "string" || cut !== request.cutCommitSha || !Array.isArray(children)) {
2466
+ throw new ConductorBridgeApiError("server");
2467
+ }
2468
+ if (children.length !== request.childKeys.length) {
2469
+ throw new ConductorBridgeApiError("server");
2470
+ }
2471
+ const seen = new Set();
2472
+ const results = [];
2473
+ for (let i = 0; i < children.length; i += 1) {
2474
+ const raw = children[i];
2475
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
2476
+ throw new ConductorBridgeApiError("server");
2477
+ }
2478
+ const c = raw;
2479
+ const key = c["ticket_key"];
2480
+ if (typeof key !== "string" || key !== request.childKeys[i] || seen.has(key)) {
2481
+ throw new ConductorBridgeApiError("server");
2482
+ }
2483
+ seen.add(key);
2484
+ const pr = c["pr_number"];
2485
+ const sha = c["merge_commit_sha"];
2486
+ if (!isOneOf(CHILD_MERGE_CLASSIFICATIONS, c["classification"]) ||
2487
+ !isOneOf(CHILD_MERGE_EVIDENCE_SOURCES, c["evidence_source"]) ||
2488
+ !isOneOf(CHILD_MERGE_ANCESTRY_VALUES, c["ancestry"]) ||
2489
+ !isOneOf(CHILD_MERGE_REASONS, c["reason"]) ||
2490
+ !(pr === null || (typeof pr === "number" && Number.isSafeInteger(pr) && pr > 0)) ||
2491
+ !(sha === null || (typeof sha === "string" && FULL_COMMIT_SHA_PATTERN.test(sha)))) {
2492
+ throw new ConductorBridgeApiError("server");
2493
+ }
2494
+ results.push({
2495
+ ticket_key: key,
2496
+ classification: c["classification"],
2497
+ evidence_source: c["evidence_source"],
2498
+ pr_number: pr,
2499
+ merge_commit_sha: sha,
2500
+ ancestry: c["ancestry"],
2501
+ reason: c["reason"],
2502
+ });
2503
+ }
2504
+ return { cut_commit_sha: cut, children: results };
2505
+ }
2506
+ export const SKIPPED_CHILD_OUTCOMES = ["recorded", "already_recorded", "refused"];
2507
+ export const SKIPPED_CHILD_REFUSAL_REASONS = [
2508
+ "not_skippable",
2509
+ "child_present_in_plan",
2510
+ "run_context_unavailable",
2511
+ ];
2512
+ /**
2513
+ * POST `/jira/epic-runs/{epic_run_id}/skipped-children`: audit-only record of
2514
+ * children a newly created run left out. The body carries repository identity,
2515
+ * child keys, and confirmation booleans only; the server derives every fact.
2516
+ */
2517
+ export async function recordSkippedChildren(access, epicRunId, children, fetchImpl = globalThis.fetch) {
2518
+ const url = buildConductorJiraUrl(access.baseUrl, `${EPIC_RUNS_API_PREFIX}/${encodeURIComponent(epicRunId)}/skipped-children`);
2519
+ const parsed = await fetchConductorJsonPostWithTimeout(url, conductorPostHeaders(access), JSON.stringify({
2520
+ repo_name: access.repoName,
2521
+ children: children.map((c) => ({
2522
+ ticket_key: c.ticketKey,
2523
+ operator_confirmed: c.operatorConfirmed,
2524
+ })),
2525
+ }), CONDUCTOR_FETCH_TIMEOUT_MS, fetchImpl);
2526
+ return parseRecordSkippedChildrenResponse(parsed, children.map((c) => c.ticketKey));
2527
+ }
2528
+ /** Strict parse of the recording body; throws a sanitized error on any deviation. */
2529
+ export function parseRecordSkippedChildrenResponse(parsed, expectedKeys) {
2530
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
2531
+ throw new ConductorBridgeApiError("server");
2532
+ }
2533
+ const results = parsed["results"];
2534
+ if (!Array.isArray(results) || results.length !== expectedKeys.length) {
2535
+ throw new ConductorBridgeApiError("server");
2536
+ }
2537
+ const out = [];
2538
+ for (let i = 0; i < results.length; i += 1) {
2539
+ const raw = results[i];
2540
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
2541
+ throw new ConductorBridgeApiError("server");
2542
+ }
2543
+ const r = raw;
2544
+ const reason = r["reason"];
2545
+ if (r["ticket_key"] !== expectedKeys[i] ||
2546
+ !isOneOf(SKIPPED_CHILD_OUTCOMES, r["outcome"]) ||
2547
+ !(reason === null || reason === undefined || isOneOf(SKIPPED_CHILD_REFUSAL_REASONS, reason))) {
2548
+ throw new ConductorBridgeApiError("server");
2549
+ }
2550
+ out.push({
2551
+ ticket_key: expectedKeys[i],
2552
+ outcome: r["outcome"],
2553
+ reason: (reason ?? null),
2554
+ });
2555
+ }
2556
+ return { results: out };
2557
+ }
2262
2558
  /** Non-negative safe integer, or 0 for anything else (absent field included). */
2263
2559
  function safeCount(value) {
2264
2560
  return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : 0;
@@ -2615,6 +2911,40 @@ export const REQUIRED_CHECKS_VERIFICATIONS = new Set([
2615
2911
  "unknown_name",
2616
2912
  "unverifiable",
2617
2913
  ]);
2914
+ export function parseReadinessPrerequisites(raw) {
2915
+ if (!Array.isArray(raw))
2916
+ return undefined;
2917
+ const seen = new Set();
2918
+ const result = [];
2919
+ for (const item of raw) {
2920
+ if (typeof item !== 'object' || item === null || Array.isArray(item))
2921
+ return undefined;
2922
+ const r = item;
2923
+ const d = typeof r.id === 'string' ? READINESS_PREREQUISITE_BY_ID.get(r.id) : undefined;
2924
+ if (!d || seen.has(d.id) || Object.keys(r).sort().join(',') !== 'check_status,fix,id,impact,label,scope' || r.label !== d.label || r.scope !== d.scope ||
2925
+ !['met', 'not_met', 'unknown', 'not_applicable'].includes(String(r.check_status)) || !['blocking', 'warning', 'info'].includes(String(r.impact)) ||
2926
+ typeof r.fix !== 'string' || !Object.values(READINESS_PREREQUISITE_FIXES).includes(r.fix) ||
2927
+ (r.check_status === 'unknown' && r.impact === 'blocking'))
2928
+ return undefined;
2929
+ seen.add(d.id);
2930
+ result.push(r);
2931
+ }
2932
+ return result;
2933
+ }
2934
+ export const ESCALATION_CHANNELS = ["webhook", "email", "none", "unknown"];
2935
+ export const WEBHOOK_LAST_DELIVERIES = ["failed", "handed_off", "queued", "unknown"];
2936
+ /** BAPI-1452 — the three supported merge execution modes, as the server reports them. */
2937
+ export const RESOLVED_MERGE_EXECUTION_MODES = ["local", "machine_token", "github_actions"];
2938
+ /**
2939
+ * BAPI-1452 — a provider repository identity: exactly one `/`, each side
2940
+ * starting with an alphanumeric and bounded, no whitespace, control
2941
+ * characters, URL syntax, or path traversal.
2942
+ */
2943
+ const PROVIDER_REPOSITORY_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,99}\/[A-Za-z0-9][A-Za-z0-9._-]{0,99}$/;
2944
+ /** True when `value` is a well-formed `owner/name` provider identity (BAPI-1452). */
2945
+ export function isProviderRepositoryIdentity(value) {
2946
+ return typeof value === "string" && PROVIDER_REPOSITORY_PATTERN.test(value);
2947
+ }
2618
2948
  /** BAPI-1260 — one mandatory-evidence verdict. Only `confirmed` ever holds. */
2619
2949
  export const ADMISSION_EVIDENCE_VERDICTS = [
2620
2950
  "confirmed",
@@ -2892,6 +3222,9 @@ export function parseConductorReadinessResponse(body) {
2892
3222
  const thr = requireObject(root.thresholds);
2893
3223
  const requiredChecksCount = requireInt(sup, "required_checks_count");
2894
3224
  return {
3225
+ ...(typeof root.is_production === 'boolean' ? { is_production: root.is_production } : {}),
3226
+ ...(root.is_production === undefined || typeof root.is_production === 'boolean'
3227
+ ? { prerequisites: parseReadinessPrerequisites(root.prerequisites) } : {}),
2895
3228
  repo_name: repoName,
2896
3229
  supervisor: {
2897
3230
  setup_present: requireBool(sup, "setup_present"),
@@ -2966,8 +3299,51 @@ export function parseConductorReadinessResponse(body) {
2966
3299
  const diagnostics = parseReadinessDiagnostics(root);
2967
3300
  return diagnostics === undefined ? {} : { diagnostics };
2968
3301
  })(),
3302
+ ...parseLaunchMergeFacts(root),
3303
+ ...parseEscalationObservations(root),
2969
3304
  };
2970
3305
  }
3306
+ /**
3307
+ * BAPI-1495 — the closed escalation observations. Absent stays absent (an older
3308
+ * server). A present value outside the closed set is dropped rather than rejecting
3309
+ * the read: the wording seam reads a dropped field as unknown.
3310
+ */
3311
+ function parseEscalationObservations(body) {
3312
+ const out = {};
3313
+ if (ESCALATION_CHANNELS.includes(body.escalation_channel)) {
3314
+ out.escalation_channel = body.escalation_channel;
3315
+ }
3316
+ if (body.webhook_last_delivery === null) {
3317
+ out.webhook_last_delivery = null;
3318
+ }
3319
+ else if (WEBHOOK_LAST_DELIVERIES.includes(body.webhook_last_delivery)) {
3320
+ out.webhook_last_delivery = body.webhook_last_delivery;
3321
+ }
3322
+ return out;
3323
+ }
3324
+ /**
3325
+ * BAPI-1452 — parse the two additive launch facts.
3326
+ *
3327
+ * Absent keys stay absent (an older server). A PRESENT value that is not a
3328
+ * supported mode, or not a well-formed `owner/name`, becomes `null` rather
3329
+ * than rejecting the whole readiness read: these facts only feed a launch
3330
+ * warning, and losing every other readiness fact over one malformed additive
3331
+ * field would be the wrong trade. `null` is never read as a usable default.
3332
+ */
3333
+ function parseLaunchMergeFacts(body) {
3334
+ const out = {};
3335
+ if (Object.prototype.hasOwnProperty.call(body, "resolved_merge_execution_mode")) {
3336
+ const mode = body.resolved_merge_execution_mode;
3337
+ out.resolved_merge_execution_mode = RESOLVED_MERGE_EXECUTION_MODES.includes(mode)
3338
+ ? mode
3339
+ : null;
3340
+ }
3341
+ if (Object.prototype.hasOwnProperty.call(body, "provider_repository")) {
3342
+ const repository = body.provider_repository;
3343
+ out.provider_repository = isProviderRepositoryIdentity(repository) ? repository : null;
3344
+ }
3345
+ return out;
3346
+ }
2971
3347
  function parseReadinessDiagnostics(body) {
2972
3348
  if (body.diagnostics === undefined || body.diagnostics === null)
2973
3349
  return undefined;
@@ -3246,6 +3622,7 @@ export function parseEpicBranchCiReadiness(body) {
3246
3622
  });
3247
3623
  return {
3248
3624
  effective_review_mode: enumOrNull(o.effective_review_mode, EFFECTIVE_REVIEW_MODES),
3625
+ effective_review_mode_scope: o.effective_review_mode_scope === "run" ? "run" : "repository",
3249
3626
  decision,
3250
3627
  evidence,
3251
3628
  no_ci_warning: requireBool(o, "no_ci_warning"),
@@ -3380,20 +3757,96 @@ function parseUnattended(body) {
3380
3757
  escalation_email_available: o.escalation_email_available === true,
3381
3758
  };
3382
3759
  }
3383
- /**
3384
- * GET `/jira/epic-runs/conductor-readiness?repo_name=<repo>`.
3385
- *
3386
- * The API key travels ONLY in `X-API-Key`; `repo_name` is URL-encoded into the
3387
- * query string because the endpoint scopes on it. Fails closed on transport,
3388
- * auth, and shape errors alike.
3389
- */
3390
- export async function fetchConductorReadiness(access, fetchImpl = globalThis.fetch) {
3760
+ export async function fetchConductorReadiness(access, fetchImpl = globalThis.fetch, options = {}) {
3391
3761
  const url = buildConductorJiraUrl(access.baseUrl, `${EPIC_RUNS_API_PREFIX}/conductor-readiness`, {
3392
3762
  repo_name: access.repoName,
3763
+ ...(options.epicRunId ? { epic_run_id: options.epicRunId } : {}),
3764
+ });
3765
+ // BAPI-1455 — a `SCHEMA_INCOMPATIBLE` refusal explains itself in
3766
+ // `detail.error` (the applied stamp, the missing objects, the repair route),
3767
+ // so that field is read, with room for the object names.
3768
+ const body = await fetchConductorJsonWithTimeout(url, conductorGetHeaders(access), CONDUCTOR_FETCH_TIMEOUT_MS, fetchImpl, {
3769
+ readErrorField: true,
3770
+ errorFieldPreviewMax: SCHEMA_REFUSAL_PREVIEW_MAX,
3771
+ readErrorFieldOnlyFor: [SCHEMA_INCOMPATIBLE_ERROR_CODE],
3393
3772
  });
3394
- const body = await fetchConductorJsonWithTimeout(url, conductorGetHeaders(access), CONDUCTOR_FETCH_TIMEOUT_MS, fetchImpl);
3395
3773
  return parseConductorReadinessResponse(body);
3396
3774
  }
3775
+ /** The answer for every failure: an older server, a non-200, a timeout, a bad body. */
3776
+ export const REPOSITORY_USAGE_ADVISORY_UNAVAILABLE = Object.freeze({
3777
+ state: "unavailable",
3778
+ utilization_percent: null,
3779
+ status: null,
3780
+ observed_at: null,
3781
+ resets_at: null,
3782
+ stale: null,
3783
+ });
3784
+ /** Same size as the plane freshness probes' bound, so no launch read waits longer. */
3785
+ export const REPOSITORY_USAGE_ADVISORY_TIMEOUT_MS = 30_000;
3786
+ const USAGE_ADVISORY_STATES = new Set([
3787
+ "observed",
3788
+ "below_warning_threshold",
3789
+ "absent",
3790
+ "unavailable",
3791
+ ]);
3792
+ const USAGE_ADVISORY_FIELD_MAX = 64;
3793
+ function boundedUsageString(value) {
3794
+ if (value === undefined || value === null)
3795
+ return null;
3796
+ if (typeof value !== "string" || value.length > USAGE_ADVISORY_FIELD_MAX)
3797
+ return undefined;
3798
+ return value;
3799
+ }
3800
+ /** Validate a usage-advisory body. `null` when any field is out of contract. */
3801
+ export function parseRepositoryUsageAdvisory(body) {
3802
+ if (body === null || typeof body !== "object" || Array.isArray(body))
3803
+ return null;
3804
+ const o = body;
3805
+ if (typeof o.state !== "string" || !USAGE_ADVISORY_STATES.has(o.state))
3806
+ return null;
3807
+ const percent = o.utilization_percent;
3808
+ let utilization = null;
3809
+ if (percent !== undefined && percent !== null) {
3810
+ if (typeof percent !== "number" || !Number.isInteger(percent) || percent < 0 || percent > 100)
3811
+ return null;
3812
+ utilization = percent;
3813
+ }
3814
+ const status = boundedUsageString(o.status);
3815
+ const observedAt = boundedUsageString(o.observed_at);
3816
+ const resetsAt = boundedUsageString(o.resets_at);
3817
+ if (status === undefined || observedAt === undefined || resetsAt === undefined)
3818
+ return null;
3819
+ if (observedAt !== null && Number.isNaN(Date.parse(observedAt)))
3820
+ return null;
3821
+ let stale = null;
3822
+ if (o.stale !== undefined && o.stale !== null) {
3823
+ if (typeof o.stale !== "boolean")
3824
+ return null;
3825
+ stale = o.stale;
3826
+ }
3827
+ const state = o.state;
3828
+ if (state === "observed" && (utilization === null || status === null))
3829
+ return null;
3830
+ return { state, utilization_percent: utilization, status, observed_at: observedAt, resets_at: resetsAt, stale };
3831
+ }
3832
+ /**
3833
+ * GET `/jira/epic-runs/usage-advisory?repo_name=<repo>`. NEVER throws: a 404
3834
+ * from an older server, any other non-200, a transport failure, a timeout, or a
3835
+ * malformed body is {@link REPOSITORY_USAGE_ADVISORY_UNAVAILABLE}. The launch
3836
+ * preview turns that into a warning row; it can never change a launch decision.
3837
+ */
3838
+ export async function fetchRepositoryUsageAdvisory(access, fetchImpl = globalThis.fetch) {
3839
+ try {
3840
+ const url = buildConductorJiraUrl(access.baseUrl, `${EPIC_RUNS_API_PREFIX}/usage-advisory`, {
3841
+ repo_name: access.repoName,
3842
+ });
3843
+ const body = await fetchConductorJsonWithTimeout(url, conductorGetHeaders(access), REPOSITORY_USAGE_ADVISORY_TIMEOUT_MS, fetchImpl);
3844
+ return parseRepositoryUsageAdvisory(body) ?? REPOSITORY_USAGE_ADVISORY_UNAVAILABLE;
3845
+ }
3846
+ catch {
3847
+ return REPOSITORY_USAGE_ADVISORY_UNAVAILABLE;
3848
+ }
3849
+ }
3397
3850
  const ROW_OUTCOMES = new Set(["created", "changed", "preserved"]);
3398
3851
  function optionalRowOutcome(o, key) {
3399
3852
  const v = o[key];
@@ -3766,6 +4219,126 @@ export async function resumeCiTicket(access, request, fetchImpl = globalThis.fet
3766
4219
  clearTimeout(timer);
3767
4220
  }
3768
4221
  }
4222
+ /** The closed refusal table of `regenerate-plan`; `CONFLICT` is the stale row version. */
4223
+ export const REGENERATE_PLAN_REFUSAL_CODES = [
4224
+ "PLAN_REGEN_NOT_ELIGIBLE",
4225
+ "PLAN_REGEN_OPERATION_CONFLICT",
4226
+ "PLAN_REGEN_ANSWERED_CONFLICT",
4227
+ "PLAN_REGEN_STALE_PARK",
4228
+ "PLAN_REGEN_ALLOWANCE_SPENT",
4229
+ "PLAN_REGEN_ROUTE_NOT_ALLOWED",
4230
+ "PLAN_REGEN_RUN_NOT_ACTIVE",
4231
+ "PLAN_REGEN_BUSY",
4232
+ "PLAN_REGEN_SCHEMA_UNAVAILABLE",
4233
+ "SPEC_SNAPSHOT_UNAVAILABLE",
4234
+ "TICKET_NOT_IN_RUN",
4235
+ "NOT_FOUND",
4236
+ "CONFLICT",
4237
+ ];
4238
+ /** True when `code` is one of the closed `regenerate-plan` refusal codes. */
4239
+ export function isRegeneratePlanRefusalCode(code) {
4240
+ return REGENERATE_PLAN_REFUSAL_CODES.includes(code);
4241
+ }
4242
+ /** Strictly parse a `regenerate-plan` success body; anything short of the contract throws. */
4243
+ export function parseRegeneratePlanResult(value) {
4244
+ if (!isPlainRecord(value))
4245
+ throw new ConductorBridgeApiError("server");
4246
+ const v = value;
4247
+ if (typeof v.epic_run_id !== "string" || !v.epic_run_id ||
4248
+ typeof v.ticket_key !== "string" || !v.ticket_key ||
4249
+ (v.outcome !== "committed" && v.outcome !== "replayed" && v.outcome !== "answered_replay") ||
4250
+ (v.accepted_route !== "test" && v.accepted_route !== "implementation") ||
4251
+ !isNonNegativeInt(v.answered_park_generation) ||
4252
+ !isNonNegativeInt(v.row_version) ||
4253
+ !nullableString(v.ticket_status) ||
4254
+ v.plan_ready !== false) {
4255
+ throw new ConductorBridgeApiError("server");
4256
+ }
4257
+ return {
4258
+ epic_run_id: v.epic_run_id,
4259
+ ticket_key: v.ticket_key,
4260
+ outcome: v.outcome,
4261
+ accepted_route: v.accepted_route,
4262
+ answered_park_generation: v.answered_park_generation,
4263
+ row_version: v.row_version,
4264
+ ticket_status: (v.ticket_status ?? null),
4265
+ plan_ready: false,
4266
+ };
4267
+ }
4268
+ /**
4269
+ * POST `/jira/epic-runs/runs/{epic_run_id}/tickets/{ticket_key}/regenerate-plan`.
4270
+ *
4271
+ * Sends the pinned operation key and park generation only in the authenticated
4272
+ * body. Never throws for an HTTP or transport outcome: every response is
4273
+ * classified into a {@link RegeneratePlanPostOutcome}, so the caller — not this
4274
+ * function — decides whether an uncertain outcome is retried with the SAME
4275
+ * operation. It never falls back to another mutation.
4276
+ */
4277
+ export async function regeneratePlanTicket(access, request, fetchImpl = globalThis.fetch, timeoutMs = CONDUCTOR_FETCH_TIMEOUT_MS) {
4278
+ requireNonEmptyString(request.epicRunId);
4279
+ requireNonEmptyString(request.ticketKey);
4280
+ requireNonEmptyString(request.operationKey);
4281
+ requireNonEmptyString(request.reason);
4282
+ requireNonNegativeSafeInteger(request.expectedRowVersion);
4283
+ requireNonNegativeSafeInteger(request.parkGeneration);
4284
+ const url = buildConductorJiraUrl(access.baseUrl, `${continuationChildPath(request.epicRunId, request.ticketKey)}/regenerate-plan`);
4285
+ const body = JSON.stringify({
4286
+ repo_name: access.repoName,
4287
+ route: request.route,
4288
+ reason: request.reason,
4289
+ expected_row_version: request.expectedRowVersion,
4290
+ park_generation: request.parkGeneration,
4291
+ operation_key: request.operationKey,
4292
+ });
4293
+ const controller = new AbortController();
4294
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
4295
+ try {
4296
+ let resp;
4297
+ try {
4298
+ resp = await fetchImpl(url, {
4299
+ method: "POST",
4300
+ headers: conductorPostHeaders(access),
4301
+ body,
4302
+ signal: controller.signal,
4303
+ });
4304
+ }
4305
+ catch {
4306
+ return { kind: "uncertain", reason: controller.signal.aborted ? "timeout" : "network" };
4307
+ }
4308
+ let parsed;
4309
+ let parsedOk = true;
4310
+ try {
4311
+ parsed = await resp.json();
4312
+ }
4313
+ catch {
4314
+ parsedOk = false;
4315
+ }
4316
+ if (resp.status >= 200 && resp.status < 300) {
4317
+ if (!parsedOk)
4318
+ return { kind: "uncertain", reason: "malformed", status: resp.status };
4319
+ try {
4320
+ return { kind: "result", result: parseRegeneratePlanResult(parsed) };
4321
+ }
4322
+ catch {
4323
+ return { kind: "uncertain", reason: "malformed", status: resp.status };
4324
+ }
4325
+ }
4326
+ const errorCode = parsedOk ? safeDetailErrorCode(parsed) : null;
4327
+ if (errorCode !== null && isRegeneratePlanRefusalCode(errorCode)) {
4328
+ return { kind: "refused", status: resp.status, errorCode };
4329
+ }
4330
+ if (errorCode === null && (resp.status === 404 || resp.status === 405)) {
4331
+ return { kind: "unsupported_server", status: resp.status };
4332
+ }
4333
+ if (resp.status >= 500) {
4334
+ return { kind: "uncertain", reason: "server", status: resp.status };
4335
+ }
4336
+ return { kind: "rejected", status: resp.status, errorCode };
4337
+ }
4338
+ finally {
4339
+ clearTimeout(timer);
4340
+ }
4341
+ }
3769
4342
  // ---------------------------------------------------------------------------
3770
4343
  // BAPI-1391 — gate profile, provider history, capability probe, qualification
3771
4344
  // ---------------------------------------------------------------------------
@@ -4162,3 +4735,9 @@ export async function fetchConductorGateQualificationJob(access, jobId, fetchImp
4162
4735
  const body = await fetchConductorJsonWithTimeout(gateProfileUrl(access, `qualification/${encodeURIComponent(String(jobId))}/`), conductorGetHeaders(access), CONDUCTOR_FETCH_TIMEOUT_MS, fetchImpl);
4163
4736
  return parseConductorGateQualificationJob(body, access.repoName, jobId);
4164
4737
  }
4738
+ /** Authenticated read-only preview of the same effective policy sent at creation. */
4739
+ export async function previewLaunchReadiness(access, policyJson, fetchImpl = globalThis.fetch) {
4740
+ const url = buildConductorJiraUrl(access.baseUrl, `${EPIC_RUNS_API_PREFIX}/launch-readiness`);
4741
+ const body = await fetchConductorJsonPostWithTimeout(url, conductorPostHeaders(access), JSON.stringify({ repo_name: access.repoName, policy_json: policyJson }), CONDUCTOR_FETCH_TIMEOUT_MS, fetchImpl);
4742
+ return parseConductorReadinessResponse(body);
4743
+ }