@bridge_gpt/mcp-server 0.2.66 → 0.2.68

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (171) hide show
  1. package/README.md +421 -83
  2. package/build/agent-capabilities/probes.js +15 -5
  3. package/build/agent-launchers/claude-executor-adapter.js +70 -16
  4. package/build/agent-launchers/executor-adapter.js +7 -4
  5. package/build/agents.generated.js +22 -2
  6. package/build/bitbucket-workspace-remote.js +136 -0
  7. package/build/bootstrap-project-identity.js +24 -0
  8. package/build/bounded-wait.js +67 -10
  9. package/build/bridge-permissions.js +829 -0
  10. package/build/claude-settings.js +18 -0
  11. package/build/cli-help.js +21 -0
  12. package/build/command-assets-doctor.js +24 -70
  13. package/build/command-provisioning.js +29 -5
  14. package/build/commands.generated.js +12 -12
  15. package/build/conductor/bridge-api-client.js +857 -1
  16. package/build/conductor/bring-up-facts.js +1 -1
  17. package/build/conductor/command-drift-production.js +92 -0
  18. package/build/conductor/command-drift.js +89 -0
  19. package/build/conductor/doctor.js +72 -29
  20. package/build/conductor/epic-branch-ci-inspection.js +1375 -0
  21. package/build/conductor/epic-implementer-cli.js +98 -28
  22. package/build/conductor/errors.js +4 -3
  23. package/build/conductor/gate-discovery.js +890 -0
  24. package/build/conductor/guided-repair.js +6 -2
  25. package/build/conductor/hook-runtime-qualification.js +1 -1
  26. package/build/conductor/install-doctor.js +15 -3
  27. package/build/conductor/json-output.js +63 -0
  28. package/build/conductor/merge-execution-contract.js +27 -8
  29. package/build/conductor/merge-execution-eligibility.js +3 -2
  30. package/build/conductor/pr-ci-producer.js +1 -1
  31. package/build/conductor/readiness-cli.js +113 -12
  32. package/build/conductor/readiness-sections.js +410 -9
  33. package/build/conductor/readiness.js +388 -33
  34. package/build/conductor/recovery-cli.js +310 -12
  35. package/build/conductor/recovery-operations.js +167 -3
  36. package/build/conductor/repair-contract.js +54 -3
  37. package/build/conductor/repository-workflow-remedy.js +125 -0
  38. package/build/conductor/tools.js +2 -2
  39. package/build/conductor-bundle-artifacts.js +8 -0
  40. package/build/connect-bitbucket-api.js +41 -6
  41. package/build/connect-bitbucket.js +106 -33
  42. package/build/credential-store.js +207 -47
  43. package/build/docs.generated.js +62 -3
  44. package/build/doctor.js +92 -1
  45. package/build/drive-epic-key.js +6 -5
  46. package/build/drive-epic.js +129 -61
  47. package/build/epic-coherence-input.js +2 -1
  48. package/build/epic-coherence-input.test-fixtures.js +4 -3
  49. package/build/epic-implementer-bin.js +1379 -28
  50. package/build/epic-implementer-bundle-cli.js +2 -2
  51. package/build/epic-workarounds-path.js +39 -0
  52. package/build/executor/cli.js +33 -2
  53. package/build/executor/completion-contract.js +66 -10
  54. package/build/executor/completion-supervision.js +4 -0
  55. package/build/executor/deps.js +4 -1
  56. package/build/executor/env.js +19 -0
  57. package/build/executor/gate-qualification-job.js +499 -0
  58. package/build/executor/guarded-fast-forward-push.js +129 -0
  59. package/build/executor/heartbeat.js +1 -1
  60. package/build/executor/http-client.js +20 -0
  61. package/build/executor/integration-job.js +122 -8
  62. package/build/executor/job-errors.js +33 -1
  63. package/build/executor/job-runner.js +813 -87
  64. package/build/executor/likely-tests.js +290 -0
  65. package/build/executor/live-worker-registry.js +1 -1
  66. package/build/executor/observation.js +37 -3
  67. package/build/executor/packaged-recipe.js +106 -0
  68. package/build/executor/permissions.js +115 -28
  69. package/build/executor/platform.js +1 -1
  70. package/build/executor/preflight.js +112 -26
  71. package/build/executor/process-containment.js +36 -12
  72. package/build/executor/process.js +15 -7
  73. package/build/executor/prompt-spec.js +33 -1
  74. package/build/executor/recently-failed-tests-file.js +71 -0
  75. package/build/executor/reconcile-overlap-job.js +41 -23
  76. package/build/executor/required-verification.js +1 -1
  77. package/build/executor/required-work-ledger.js +111 -18
  78. package/build/executor/resume-pre-spawn.js +2 -0
  79. package/build/executor/retirement.js +69 -0
  80. package/build/executor/runner.js +122 -13
  81. package/build/executor/service-unit.js +7 -1
  82. package/build/executor/test-clock.js +15 -0
  83. package/build/executor/types.js +32 -0
  84. package/build/executor/viewer-tabs.js +58 -8
  85. package/build/executor/worker-command.js +23 -7
  86. package/build/executor/worker-diagnostics.js +37 -0
  87. package/build/executor/worker-finalization.js +113 -34
  88. package/build/executor/worker-guard-hook.js +316 -58
  89. package/build/executor/worker-log.js +4 -3
  90. package/build/executor/worker-verification-guidance.js +37 -0
  91. package/build/executor/worktree.js +166 -8
  92. package/build/first-build-advisory.js +97 -0
  93. package/build/generated-assets-advisory.js +14 -0
  94. package/build/generated-assets-catalog.js +66 -0
  95. package/build/generated-assets-context.js +46 -0
  96. package/build/generated-assets-dependencies.js +26 -0
  97. package/build/generated-assets-doctor.js +50 -0
  98. package/build/generated-assets-health.js +65 -0
  99. package/build/generated-assets-host.js +17 -0
  100. package/build/generated-assets-inventory.js +17 -0
  101. package/build/generated-assets-paths.js +60 -0
  102. package/build/generated-assets-publisher.js +133 -0
  103. package/build/generated-assets-remedy.js +25 -0
  104. package/build/generated-assets-repair.js +72 -0
  105. package/build/generated-assets-session.js +71 -0
  106. package/build/generated-assets-test-support.js +35 -0
  107. package/build/index.js +1224 -1054
  108. package/build/init.js +54 -177
  109. package/build/install-bridge-conductor-gate.js +1372 -0
  110. package/build/install-bridge-conductor.js +296 -41
  111. package/build/install-bridge.js +547 -40
  112. package/build/mcp-install-state.js +50 -4
  113. package/build/merge-workflow-install.js +3 -4
  114. package/build/phase-context-git-probe.js +68 -0
  115. package/build/phase-result-artifacts.js +173 -5
  116. package/build/pipeline-utils.js +6 -2
  117. package/build/pipelines.generated.js +34 -160
  118. package/build/plan-execution-ledger.js +2 -32
  119. package/build/plane/alembic-head.js +12 -3
  120. package/build/plane/base-worktree-guard.js +147 -0
  121. package/build/plane/cli.js +217 -17
  122. package/build/plane/code-snapshot.js +468 -0
  123. package/build/plane/defaults.js +48 -3
  124. package/build/plane/endpoint.js +41 -0
  125. package/build/plane/manifest.js +136 -2
  126. package/build/plane/member-roster.js +94 -19
  127. package/build/plane/preflight.js +83 -2
  128. package/build/plane/process-cwd.js +67 -0
  129. package/build/plane/restart.js +542 -0
  130. package/build/plane/shutdown.js +31 -0
  131. package/build/plane/status.js +143 -8
  132. package/build/plane/supervisor.js +586 -179
  133. package/build/plane/types.js +54 -2
  134. package/build/printed-remedy-commands.js +52 -0
  135. package/build/readiness-check.js +1 -1
  136. package/build/readme.generated.js +1 -1
  137. package/build/repair-assets.js +93 -0
  138. package/build/review-generation.js +22 -0
  139. package/build/run-unit-tests-launcher.js +130 -49
  140. package/build/secret-redaction.js +33 -0
  141. package/build/setup-epic.js +152 -22
  142. package/build/sfcc/config.js +9 -0
  143. package/build/sfcc/credentials.js +100 -38
  144. package/build/sfcc/hostname.js +67 -0
  145. package/build/sfcc/permissions.js +17 -13
  146. package/build/sfcc/register.js +1 -1
  147. package/build/sfcc/tool-wrapper.js +39 -43
  148. package/build/sfcc/write-grants.js +17 -11
  149. package/build/sfcc/write-result.js +4 -3
  150. package/build/sfcc/writes-site-preference.js +20 -8
  151. package/build/start-tickets-prereqs.js +70 -16
  152. package/build/start-tickets.js +208 -36
  153. package/build/tool-error-envelope.js +61 -0
  154. package/build/tool-surface-gating.js +36 -0
  155. package/build/upgrade-cli.js +55 -1
  156. package/build/version.generated.js +3 -3
  157. package/build/visual-diff.js +22 -1
  158. package/build/worker-guard-hook-bin.js +4 -6
  159. package/build/worker-guard-hook-main.js +47 -0
  160. package/build/worktree-core.js +145 -6
  161. package/docs/EPIC-IMPLEMENTER-INSTALL.md +867 -0
  162. package/docs/EPIC-IMPLEMENTER.md +728 -0
  163. package/docs/install/mcp-tool-integrations.md +24 -15
  164. package/docs/install/sfcc-integration.md +12 -4
  165. package/package.json +3 -3
  166. package/pipelines/greenfield-setup.json +4 -128
  167. package/pipelines/implement-ticket.json +3 -2
  168. package/pipelines/plan-epic.json +0 -5
  169. package/pipelines/review-ticket.json +1 -1
  170. package/docs/CONDUCTOR.md +0 -605
  171. package/pipelines/idea-to-pr.json +0 -49
@@ -20,7 +20,7 @@ import { parseContinuationStatusResult, parseContinueParkedChildResult, } from "
20
20
  import { resolveDeclaredRunBaseBranch } from "./run-branch.js";
21
21
  import { safeEpicLaunchTargetUrl } from "../epic-launch-diagnostic.js";
22
22
  import { validateHttpBaseUrl } from "../base-url.js";
23
- import { isCiResumeRefusalCode } from "./repair-contract.js";
23
+ import { RERUN_CI_CHECK_STATUSES, RERUN_CI_NOT_RUNNABLE_REASONS, RERUN_CI_OUTCOMES, isCiResumeRefusalCode, } from "./repair-contract.js";
24
24
  /** Default Bridge API base URL when `BAPI_BASE_URL` is unset. */
25
25
  export const CONDUCTOR_DEFAULT_BASE_URL = "https://bridgegpt-api.com";
26
26
  /** Default per-request timeout for conductor Bridge API calls. */
@@ -1285,6 +1285,7 @@ export async function unparkEpicTicket(access, request, fetchImpl = globalThis.f
1285
1285
  expected_row_version: request.expectedRowVersion,
1286
1286
  ...(request.idempotencyKey !== undefined ? { idempotency_key: request.idempotencyKey } : {}),
1287
1287
  ...(request.reason !== undefined ? { reason: request.reason } : {}),
1288
+ ...(request.cleanupResolution !== undefined ? { cleanup_resolution: request.cleanupResolution } : {}),
1288
1289
  });
1289
1290
  return postUnparkLikeRequest(url, conductorPostHeaders(access), body, fetchImpl);
1290
1291
  }
@@ -1325,6 +1326,7 @@ export async function postContinueParkedChild(access, request, fetchImpl = globa
1325
1326
  source_park_generation: b.sourceParkGeneration,
1326
1327
  request_fingerprint: request.requestFingerprint,
1327
1328
  ...(request.reason ? { reason: request.reason } : {}),
1329
+ ...(request.workerExitProof ? { worker_exit_proof: request.workerExitProof } : {}),
1328
1330
  });
1329
1331
  const controller = new AbortController();
1330
1332
  const timer = setTimeout(() => controller.abort(), timeoutMs);
@@ -1374,6 +1376,56 @@ export async function postContinueParkedChild(access, request, fetchImpl = globa
1374
1376
  clearTimeout(timer);
1375
1377
  }
1376
1378
  }
1379
+ /** One submission; a lost response is never interpreted as a refusal. */
1380
+ export async function postGrantContinuation(access, request, fetchImpl = globalThis.fetch) {
1381
+ const url = buildConductorJiraUrl(access.baseUrl, `${continuationChildPath(request.epicRunId, request.ticketKey)}/grant-continuation`);
1382
+ const controller = new AbortController();
1383
+ const timer = setTimeout(() => controller.abort(), CONDUCTOR_FETCH_TIMEOUT_MS);
1384
+ let response;
1385
+ try {
1386
+ response = await fetchImpl(url, {
1387
+ method: "POST", headers: conductorPostHeaders(access),
1388
+ signal: controller.signal,
1389
+ body: JSON.stringify({
1390
+ repo_name: access.repoName,
1391
+ request_id: request.requestId,
1392
+ parked_gate: request.parkedGate,
1393
+ expected_row_version: request.expectedRowVersion,
1394
+ source_park_generation: request.sourceParkGeneration,
1395
+ extra_continuations: request.extraContinuations,
1396
+ extra_seconds: request.extraSeconds,
1397
+ reason: request.reason,
1398
+ }),
1399
+ });
1400
+ }
1401
+ catch {
1402
+ clearTimeout(timer);
1403
+ return { kind: "unconfirmed", requestId: request.requestId };
1404
+ }
1405
+ clearTimeout(timer);
1406
+ let body;
1407
+ try {
1408
+ body = await response.json();
1409
+ }
1410
+ catch {
1411
+ body = null;
1412
+ }
1413
+ const grant = body && typeof body === "object"
1414
+ ? body.grant : null;
1415
+ if (response.ok && body && typeof body === "object" &&
1416
+ typeof body.replayed === "boolean" &&
1417
+ grant && typeof grant === "object" &&
1418
+ grant.request_id === request.requestId) {
1419
+ return {
1420
+ kind: "result", requestId: request.requestId,
1421
+ replayed: body.replayed,
1422
+ };
1423
+ }
1424
+ if (response.status >= 500 || response.ok) {
1425
+ return { kind: "unconfirmed", requestId: request.requestId };
1426
+ }
1427
+ return { kind: "refused", status: response.status, errorCode: safeDetailErrorCode(body) };
1428
+ }
1377
1429
  /**
1378
1430
  * GET `.../tickets/{ticket_key}/continuation` — the child's current episode and,
1379
1431
  * with `requestId`, that request's receipt. Read-only; bounded by the standard
@@ -1532,6 +1584,134 @@ export async function dispatchRemediation(access, request, fetchImpl = globalThi
1532
1584
  const parsed = await postRepairRequest(access, `${epicRunApiPath(request.epicRunId)}/tickets/${encodeURIComponent(request.ticketKey)}/dispatch-remediation`, withGuard({ context: request.context }, request.guard), fetchImpl);
1533
1585
  return parseDispatchRemediationResult(parsed);
1534
1586
  }
1587
+ export async function escalateTicket(access, request, fetchImpl = globalThis.fetch) {
1588
+ requireNonEmptyString(request.epicRunId);
1589
+ requireNonEmptyString(request.ticketKey);
1590
+ requireNonEmptyString(request.cause);
1591
+ requireNonEmptyString(request.actionNeeded);
1592
+ if (request.actionNeeded.length > 300)
1593
+ 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);
1595
+ const value = result;
1596
+ if (!value || !["created", "deduplicated", "suppressed"].includes(value.status) ||
1597
+ !Number.isSafeInteger(value.notification_id) || value.notification_id <= 0 ||
1598
+ typeof value.episode !== "string" || !value.blocks || !Array.isArray(value.blocks.tickets) ||
1599
+ typeof value.channel !== "string" || typeof value.delivery_status !== "string" ||
1600
+ !["server_park", "driver_enforced"].includes(value.blocker_enforcement))
1601
+ throw new ConductorBridgeApiError("server");
1602
+ return value;
1603
+ }
1604
+ // ---------------------------------------------------------------------------
1605
+ // BAPI-1388 — `rerun-ci`
1606
+ // ---------------------------------------------------------------------------
1607
+ /** The synthetic code an old server's missing route is reported under. */
1608
+ export const RERUN_CI_ROUTE_UNAVAILABLE_CODE = "RERUN_CI_ROUTE_UNAVAILABLE";
1609
+ /** The fixed operator text for a server that has no `rerun-ci` route. */
1610
+ export const RERUN_CI_ROUTE_UNAVAILABLE_MESSAGE = "rerun-ci is not available on this server; upgrade the Bridge API server before using it";
1611
+ const RERUN_CI_HEAD_RE = /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/;
1612
+ function parseRerunCiCheck(value) {
1613
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
1614
+ throw new ConductorBridgeApiError("server");
1615
+ }
1616
+ const c = value;
1617
+ const status = c["status"];
1618
+ if (typeof status !== "string" ||
1619
+ !RERUN_CI_CHECK_STATUSES.includes(status)) {
1620
+ throw new ConductorBridgeApiError("server");
1621
+ }
1622
+ const reason = c["reason"];
1623
+ if (reason !== undefined &&
1624
+ reason !== null &&
1625
+ (typeof reason !== "string" ||
1626
+ !RERUN_CI_NOT_RUNNABLE_REASONS.includes(reason))) {
1627
+ throw new ConductorBridgeApiError("server");
1628
+ }
1629
+ const optionalPositive = (field) => {
1630
+ const v = c[field];
1631
+ if (v === undefined || v === null)
1632
+ return null;
1633
+ if (typeof v !== "number" || !Number.isSafeInteger(v) || v <= 0) {
1634
+ throw new ConductorBridgeApiError("server");
1635
+ }
1636
+ return v;
1637
+ };
1638
+ const optionalText = (field) => {
1639
+ const v = c[field];
1640
+ if (v === undefined || v === null)
1641
+ return null;
1642
+ if (typeof v !== "string")
1643
+ throw new ConductorBridgeApiError("server");
1644
+ return v;
1645
+ };
1646
+ return {
1647
+ name: optionalText("name"),
1648
+ workflow_identity: optionalText("workflow_identity"),
1649
+ run_id: optionalPositive("run_id"),
1650
+ status: status,
1651
+ reason: (reason ?? null),
1652
+ job_id: optionalPositive("job_id"),
1653
+ };
1654
+ }
1655
+ /**
1656
+ * Strictly parse a `rerun-ci` success body. Anything short of the contract — an
1657
+ * unknown outcome or per-check status, a malformed head, a non-list `checks` —
1658
+ * throws, so a malformed answer is never read as a success.
1659
+ */
1660
+ export function parseRerunCiResult(parsed) {
1661
+ const p = parseRepairBase(parsed);
1662
+ const outcome = p["outcome"];
1663
+ const checks = p["checks"];
1664
+ const head = p["head_sha"];
1665
+ if (typeof p["ticket_key"] !== "string" ||
1666
+ typeof outcome !== "string" ||
1667
+ !RERUN_CI_OUTCOMES.includes(outcome) ||
1668
+ !Array.isArray(checks) ||
1669
+ (head !== undefined && head !== null && (typeof head !== "string" || !RERUN_CI_HEAD_RE.test(head)))) {
1670
+ throw new ConductorBridgeApiError("server");
1671
+ }
1672
+ return {
1673
+ epic_run_id: p["epic_run_id"],
1674
+ ticket_key: p["ticket_key"],
1675
+ before: parseRepairNullableString(p["before"]),
1676
+ after: parseRepairNullableString(p["after"]),
1677
+ committed: p["committed"],
1678
+ event_recorded: p["event_recorded"],
1679
+ outcome: outcome,
1680
+ head_sha: typeof head === "string" ? head : null,
1681
+ checks: checks.map(parseRerunCiCheck),
1682
+ };
1683
+ }
1684
+ /**
1685
+ * POST `/jira/epic-runs/runs/{epic_run_id}/tickets/{ticket_key}/rerun-ci` — body
1686
+ * `{repo_name, guard}` and nothing else: no context, check list, or run id, because
1687
+ * the SERVER decides what is re-runnable from its own fresh, head-bound CI read.
1688
+ *
1689
+ * A generic `{"detail":"Not Found"}` 404 or a 405 means the server predates the
1690
+ * route; it is reported under {@link RERUN_CI_ROUTE_UNAVAILABLE_CODE} so no caller
1691
+ * can read it as a refusal of THIS ticket or as a success (following
1692
+ * {@link classifySupervisorBootstrapFailure}). A structured 404 — notably
1693
+ * `TICKET_NOT_IN_RUN` — keeps its own code and stays a refusal.
1694
+ */
1695
+ export async function rerunCi(access, request, fetchImpl = globalThis.fetch) {
1696
+ requireNonEmptyString(request.epicRunId);
1697
+ requireNonEmptyString(request.ticketKey);
1698
+ let parsed;
1699
+ try {
1700
+ parsed = await postRepairRequest(access, `${epicRunApiPath(request.epicRunId)}/tickets/${encodeURIComponent(request.ticketKey)}/rerun-ci`, { guard: request.guard }, fetchImpl);
1701
+ }
1702
+ catch (err) {
1703
+ if (err instanceof ConductorBridgeApiError &&
1704
+ !err.errorCode &&
1705
+ (err.status === 404 || err.status === 405)) {
1706
+ throw new ConductorBridgeApiError("http", err.status, {
1707
+ errorCode: RERUN_CI_ROUTE_UNAVAILABLE_CODE,
1708
+ bodyPreview: RERUN_CI_ROUTE_UNAVAILABLE_MESSAGE,
1709
+ });
1710
+ }
1711
+ throw err;
1712
+ }
1713
+ return parseRerunCiResult(parsed);
1714
+ }
1535
1715
  /**
1536
1716
  * POST `/jira/epic-runs/runs/{epic_run_id}/merge-child` — body EXACTLY
1537
1717
  * `{repo_name, pr_number}`. The server composes the head, gate, and action key.
@@ -2442,6 +2622,47 @@ export const ADMISSION_EVIDENCE_VERDICTS = [
2442
2622
  "unable_to_verify",
2443
2623
  "not_applicable",
2444
2624
  ];
2625
+ /**
2626
+ * BAPI-1390 — the four `required_checks` reasons the stored gate profile adds.
2627
+ * Named for the remedies; an unrecognized reason still renders generically.
2628
+ */
2629
+ export const GATE_PROFILE_ADMISSION_REASONS = [
2630
+ "provider_check_unseen",
2631
+ "provider_evidence_unavailable",
2632
+ "local_command_unqualified",
2633
+ "migration_guard_unset",
2634
+ ];
2635
+ /** The two known warn-only codes. Any other code is kept and rendered generically. */
2636
+ export const GATE_PROFILE_WARNING_CODES = [
2637
+ "provider_check_not_seen_on_epic_branches",
2638
+ "epic_branch_search_unavailable",
2639
+ ];
2640
+ /**
2641
+ * True when the server's rules waive the CI lane file
2642
+ * (`.github/workflows/epic-implementer-ci.yml`): a repository with a STORED gate
2643
+ * profile names its own checks, so the lane file is not required. Absent profile
2644
+ * (older server) keeps the lane-file requirement exactly as before.
2645
+ */
2646
+ export function gateProfileWaivesCiWorkflow(readiness) {
2647
+ return readiness.gate_profile?.source === "stored";
2648
+ }
2649
+ /** BAPI-1405 — the review mode the server will actually apply to this repository. */
2650
+ export const EFFECTIVE_REVIEW_MODES = [
2651
+ "repository_policy",
2652
+ "verdict_protocol",
2653
+ "native_review_decision",
2654
+ "none",
2655
+ ];
2656
+ /**
2657
+ * BAPI-1405 — why the server warns that epic-branch PRs may run no CI. Fixed
2658
+ * order on the server. A reason this client does not know is kept (when it is a
2659
+ * bounded token) and rendered generically.
2660
+ */
2661
+ export const EPIC_BRANCH_CI_NO_CI_REASONS = [
2662
+ "gate_profile_none",
2663
+ "gate_profile_local_only",
2664
+ "triggers_do_not_cover_epic",
2665
+ ];
2445
2666
  /** The evidence schema this client renders. A newer one is shown as unknown, never as held. */
2446
2667
  export const ADMISSION_EVIDENCE_SCHEMA_VERSION = 1;
2447
2668
  /** Upper bound of the server's capped `fresh_reconciler_count` ("two or more"). */
@@ -2469,6 +2690,20 @@ function requireBool(o, key) {
2469
2690
  throw readinessShapeError();
2470
2691
  return v;
2471
2692
  }
2693
+ /**
2694
+ * An optional boolean field as a spreadable fragment: `{}` when the key is absent,
2695
+ * `{ [key]: value }` when it is a boolean, and a shape error for anything else —
2696
+ * including an explicit `null`. Spreading keeps an absent field ABSENT rather than
2697
+ * present-as-`undefined`.
2698
+ */
2699
+ function optionalBoolField(o, key) {
2700
+ if (!(key in o) || o[key] === undefined)
2701
+ return {};
2702
+ const v = o[key];
2703
+ if (typeof v !== "boolean")
2704
+ throw readinessShapeError();
2705
+ return { [key]: v };
2706
+ }
2472
2707
  function requireInt(o, key) {
2473
2708
  const v = o[key];
2474
2709
  if (typeof v !== "number" || !Number.isInteger(v))
@@ -2617,6 +2852,15 @@ function parseRequiredChecksVerification(sup, requiredChecksCount) {
2617
2852
  required_checks_evidence_fresh: fresh,
2618
2853
  };
2619
2854
  }
2855
+ /** Optional boolean: absent/null from an older server, a non-boolean fails closed. */
2856
+ function parseOptionalBool(o, key) {
2857
+ const v = o[key];
2858
+ if (v === undefined || v === null)
2859
+ return null;
2860
+ if (typeof v !== "boolean")
2861
+ throw readinessShapeError();
2862
+ return v;
2863
+ }
2620
2864
  /** Optional positive-integer threshold: absent/null from an older server. */
2621
2865
  function parseOptionalPositiveInt(o, key) {
2622
2866
  const v = o[key];
@@ -2666,6 +2910,9 @@ export function parseConductorReadinessResponse(body) {
2666
2910
  // Absent on a pre-BAPI-694 server, where no repository default can exist.
2667
2911
  review_policy_present: sup.review_policy_present === true,
2668
2912
  ...parseRequiredChecksVerification(sup, requiredChecksCount),
2913
+ required_checks_retired_gate_configured: parseOptionalBool(sup, "required_checks_retired_gate_configured"),
2914
+ required_checks_ci_workflow_absent: parseOptionalBool(sup, "required_checks_ci_workflow_absent"),
2915
+ required_checks_review_workflow_absent: parseOptionalBool(sup, "required_checks_review_workflow_absent"),
2669
2916
  },
2670
2917
  github: {
2671
2918
  credentials_readable: requireBool(gh, "credentials_readable"),
@@ -2707,6 +2954,14 @@ export function parseConductorReadinessResponse(body) {
2707
2954
  const evidence = parseAdmissionEvidence(root);
2708
2955
  return evidence === undefined ? {} : { admission_evidence: evidence };
2709
2956
  })(),
2957
+ ...(() => {
2958
+ const profile = parseGateProfile(root);
2959
+ return profile === undefined ? {} : { gate_profile: profile };
2960
+ })(),
2961
+ ...(() => {
2962
+ const epicBranchCi = parseEpicBranchCiReadiness(root);
2963
+ return epicBranchCi === undefined ? {} : { epic_branch_ci: epicBranchCi };
2964
+ })(),
2710
2965
  ...(() => {
2711
2966
  const diagnostics = parseReadinessDiagnostics(root);
2712
2967
  return diagnostics === undefined ? {} : { diagnostics };
@@ -2793,10 +3048,210 @@ function parseAdmissionEvidence(body) {
2793
3048
  max_age_seconds: maxAge,
2794
3049
  reason: r.reason === undefined ? null : token(r.reason, true),
2795
3050
  next_action_owner: (owner ?? null),
3051
+ // BAPI-1390 — additive and lenient: omitted when the server did not send
3052
+ // the key, so an older server's records keep their exact shape.
3053
+ ...(r.subject === undefined ? {} : { subject: safeSubject(r.subject) }),
2796
3054
  };
2797
3055
  });
2798
3056
  return { schema_version: schemaVersion, observed_at: observedAt, records };
2799
3057
  }
3058
+ /** The longest check or command name this client renders. */
3059
+ const GATE_PROFILE_SUBJECT_MAX_LENGTH = 200;
3060
+ const GATE_PROFILE_MEMBER_LIMIT = 64;
3061
+ /**
3062
+ * A server-validated check or command name, or `null`. Lenient by design: a
3063
+ * value this client will not render degrades to `null` and never fails the read.
3064
+ */
3065
+ function safeSubject(value) {
3066
+ if (typeof value !== "string" || value.length === 0)
3067
+ return null;
3068
+ if (value.length > GATE_PROFILE_SUBJECT_MAX_LENGTH)
3069
+ return null;
3070
+ // eslint-disable-next-line no-control-regex -- refusing control characters is the point
3071
+ if (/[\u0000-\u001f\u007f]/.test(value))
3072
+ return null;
3073
+ return value;
3074
+ }
3075
+ function enumOrNull(value, allowed) {
3076
+ return typeof value === "string" && allowed.includes(value) ? value : null;
3077
+ }
3078
+ function boolOrNull(value) {
3079
+ return typeof value === "boolean" ? value : null;
3080
+ }
3081
+ function tokenOrNull(value) {
3082
+ return typeof value === "string" && BOUNDED_TOKEN.test(value) ? value : null;
3083
+ }
3084
+ /**
3085
+ * Parse the optional BAPI-1390 `gate_profile` block.
3086
+ *
3087
+ * Absent → `undefined` (the key is then omitted). `null` → `null`. A PRESENT
3088
+ * block follows the admission-evidence rule: its STRUCTURE is validated strictly
3089
+ * (an object, booleans, a non-negative integer revision, bounded arrays of
3090
+ * objects) and a malformed structure throws the field-free shape error, because a
3091
+ * block that cannot be read is untrusted input, not an older server.
3092
+ *
3093
+ * Its VOCABULARY is lenient, so a newer server never turns into "no readiness
3094
+ * at all": an unknown `kind`, outcome, qualification state, or verdict degrades
3095
+ * to `null` (unknown, never held); an unknown `source` degrades to `unconfirmed`
3096
+ * and clears `confirmed`; an unknown `reason` or warning `code` is kept when it
3097
+ * is a bounded token and rendered generically. Names are kept only when they are
3098
+ * safe to render ({@link safeSubject}).
3099
+ */
3100
+ function parseGateProfile(body) {
3101
+ const raw = body.gate_profile;
3102
+ if (raw === undefined)
3103
+ return undefined;
3104
+ if (raw === null)
3105
+ return null;
3106
+ const o = requireObject(raw);
3107
+ const revision = requireInt(o, "revision");
3108
+ if (revision < 0)
3109
+ throw readinessShapeError();
3110
+ const sourceKnown = enumOrNull(o.source, ["stored", "legacy", "unconfirmed"]);
3111
+ const list = (value) => {
3112
+ if (value === undefined || value === null)
3113
+ return [];
3114
+ if (!Array.isArray(value) || value.length > GATE_PROFILE_MEMBER_LIMIT)
3115
+ throw readinessShapeError();
3116
+ return value.map((entry) => requireObject(entry));
3117
+ };
3118
+ const requiredName = (value) => {
3119
+ const name = safeSubject(value);
3120
+ if (name === null)
3121
+ throw readinessShapeError();
3122
+ return name;
3123
+ };
3124
+ const guardRaw = o.migration_guard;
3125
+ let migrationGuard = null;
3126
+ if (guardRaw !== undefined && guardRaw !== null) {
3127
+ const g = requireObject(guardRaw);
3128
+ const guardKind = enumOrNull(g.kind, ["provider", "local"]);
3129
+ migrationGuard = guardKind === null ? null : { kind: guardKind, name: safeSubject(g.name) };
3130
+ }
3131
+ return {
3132
+ source: sourceKnown ?? "unconfirmed",
3133
+ confirmed: sourceKnown === null ? false : requireBool(o, "confirmed"),
3134
+ stored_malformed: requireBool(o, "stored_malformed"),
3135
+ read_failed: requireBool(o, "read_failed"),
3136
+ kind: enumOrNull(o.kind, ["provider", "local", "mixed", "none"]),
3137
+ revision,
3138
+ provider_checks: list(o.provider_checks).map((c) => ({
3139
+ name: requiredName(c.name),
3140
+ outcome: enumOrNull(c.outcome, ["reported", "unseen", "evidence_unavailable"]),
3141
+ epic_branch_seen: boolOrNull(c.epic_branch_seen),
3142
+ })),
3143
+ local_commands: list(o.local_commands).map((c) => ({
3144
+ name: requiredName(c.name),
3145
+ id: requiredName(c.id),
3146
+ })),
3147
+ migration_guard: migrationGuard,
3148
+ migration_guard_required: boolOrNull(o.migration_guard_required),
3149
+ qualification_state: enumOrNull(o.qualification_state, [
3150
+ "qualified",
3151
+ "unqualified",
3152
+ "stale",
3153
+ "not_required",
3154
+ ]),
3155
+ verdict: enumOrNull(o.verdict, ["confirmed", "denied", "unable_to_verify"]),
3156
+ reason: tokenOrNull(o.reason),
3157
+ subject: safeSubject(o.subject),
3158
+ provider_evidence_complete: boolOrNull(o.provider_evidence_complete),
3159
+ provider_evidence_unavailable_reason: tokenOrNull(o.provider_evidence_unavailable_reason),
3160
+ epic_branch_search_available: boolOrNull(o.epic_branch_search_available),
3161
+ warnings: list(o.warnings).map((w) => ({
3162
+ code: tokenOrNull(w.code) ?? "unknown",
3163
+ name: safeSubject(w.name),
3164
+ })),
3165
+ };
3166
+ }
3167
+ const EPIC_BRANCH_CI_DECISIONS = new Set(["include", "decline"]);
3168
+ const EPIC_BRANCH_CI_OBSERVATION_QUALITIES = new Set(["exact", "inferred", "could_not_tell"]);
3169
+ const EPIC_BRANCH_CI_EXEMPTION_OUTCOMES = new Set(["exempt", "not_exempt", "unknown"]);
3170
+ const EPIC_BRANCH_CI_EVIDENCE_KEYS = new Set([
3171
+ "workflow_triggers_cover_epic",
3172
+ "required_approvals_apply_to_epic",
3173
+ "observation_quality",
3174
+ "exemption_outcome",
3175
+ ]);
3176
+ const EPIC_BRANCH_CI_REASON_LIMIT = 16;
3177
+ /**
3178
+ * BAPI-1405 — validate an {@link EpicBranchCiEvidence} object, fail-closed. All
3179
+ * four keys are required, every value must be one of its closed literals, and
3180
+ * any extra key is a shape error (the server model forbids extras, so an extra
3181
+ * key means the body is not what this client understands).
3182
+ */
3183
+ function parseEpicBranchCiEvidence(value) {
3184
+ const o = requireObject(value);
3185
+ for (const key of Object.keys(o)) {
3186
+ if (!EPIC_BRANCH_CI_EVIDENCE_KEYS.has(key))
3187
+ throw readinessShapeError();
3188
+ }
3189
+ const nullableBool = (key) => {
3190
+ if (!(key in o))
3191
+ throw readinessShapeError();
3192
+ const v = o[key];
3193
+ if (v === null)
3194
+ return null;
3195
+ if (typeof v !== "boolean")
3196
+ throw readinessShapeError();
3197
+ return v;
3198
+ };
3199
+ return {
3200
+ workflow_triggers_cover_epic: nullableBool("workflow_triggers_cover_epic"),
3201
+ required_approvals_apply_to_epic: nullableBool("required_approvals_apply_to_epic"),
3202
+ observation_quality: requireEnum(o, "observation_quality", EPIC_BRANCH_CI_OBSERVATION_QUALITIES),
3203
+ exemption_outcome: requireEnum(o, "exemption_outcome", EPIC_BRANCH_CI_EXEMPTION_OUTCOMES),
3204
+ };
3205
+ }
3206
+ /**
3207
+ * Parse the optional BAPI-1405 `epic_branch_ci` readiness block.
3208
+ *
3209
+ * Absent → `undefined` (the key is then omitted, so older fixtures keep their
3210
+ * shape). `null` → `null`. Both mean "unreported" to every renderer — never
3211
+ * "no warning".
3212
+ *
3213
+ * A PRESENT block follows the {@link parseGateProfile} rule. Its STRUCTURE is
3214
+ * strict: an object, a boolean `no_ci_warning`, a bounded `no_ci_reasons` list,
3215
+ * a `decision` that is `include`, `decline`, or `null`, and child-1 evidence
3216
+ * validated by {@link parseEpicBranchCiEvidence} (or `null`). A malformed
3217
+ * structure throws the field-free shape error. Only the open-ended VOCABULARY is
3218
+ * lenient so a newer server never becomes "no readiness at all": an unknown
3219
+ * `effective_review_mode` degrades to `null` (rendered as unknown), and an
3220
+ * unknown reason is kept when it is a bounded token and rendered generically.
3221
+ */
3222
+ export function parseEpicBranchCiReadiness(body) {
3223
+ const raw = body.epic_branch_ci;
3224
+ if (raw === undefined)
3225
+ return undefined;
3226
+ if (raw === null)
3227
+ return null;
3228
+ const o = requireObject(raw);
3229
+ const decisionRaw = o.decision;
3230
+ let decision = null;
3231
+ if (decisionRaw !== null && decisionRaw !== undefined) {
3232
+ if (typeof decisionRaw !== "string" || !EPIC_BRANCH_CI_DECISIONS.has(decisionRaw)) {
3233
+ throw readinessShapeError();
3234
+ }
3235
+ decision = decisionRaw;
3236
+ }
3237
+ const evidence = o.evidence === null || o.evidence === undefined ? null : parseEpicBranchCiEvidence(o.evidence);
3238
+ const reasonsRaw = o.no_ci_reasons;
3239
+ if (!Array.isArray(reasonsRaw) || reasonsRaw.length > EPIC_BRANCH_CI_REASON_LIMIT) {
3240
+ throw readinessShapeError();
3241
+ }
3242
+ const reasons = reasonsRaw.map((r) => {
3243
+ if (typeof r !== "string" || !BOUNDED_TOKEN.test(r))
3244
+ throw readinessShapeError();
3245
+ return r;
3246
+ });
3247
+ return {
3248
+ effective_review_mode: enumOrNull(o.effective_review_mode, EFFECTIVE_REVIEW_MODES),
3249
+ decision,
3250
+ evidence,
3251
+ no_ci_warning: requireBool(o, "no_ci_warning"),
3252
+ no_ci_reasons: reasons,
3253
+ };
3254
+ }
2800
3255
  /**
2801
3256
  * Parse the optional `merge_execution` block (BAPI-1135).
2802
3257
  *
@@ -2891,6 +3346,11 @@ function parseConductorCiWorkflow(body) {
2891
3346
  probe_succeeded: requireBool(o, "probe_succeeded"),
2892
3347
  workflow_present: requireBool(o, "workflow_present"),
2893
3348
  migration_guard_present: requireBool(o, "migration_guard_present"),
3349
+ // BAPI-1359: OPTIONAL so a newer CLI still reads an older server. An absent
3350
+ // key stays `undefined` — never materialized as a default — and a present key
3351
+ // must be a real boolean (`null`, strings, numbers, and objects all reject).
3352
+ ...optionalBoolField(o, "migration_guard_required"),
3353
+ ...optionalBoolField(o, "migration_chain_probe_succeeded"),
2894
3354
  };
2895
3355
  }
2896
3356
  /**
@@ -3306,3 +3766,399 @@ export async function resumeCiTicket(access, request, fetchImpl = globalThis.fet
3306
3766
  clearTimeout(timer);
3307
3767
  }
3308
3768
  }
3769
+ // ---------------------------------------------------------------------------
3770
+ // BAPI-1391 — gate profile, provider history, capability probe, qualification
3771
+ // ---------------------------------------------------------------------------
3772
+ //
3773
+ // The installer's typed access to BAPI-1390's gate-profile routes and to
3774
+ // BAPI-1391's qualification routes. Every parser is FAIL-CLOSED in the
3775
+ // bootstrap-preview style: a body this client cannot validate is never read as
3776
+ // a confirmed profile, an available history, an advertised capability, or an
3777
+ // accepted enqueue. Errors are the sanitized {@link ConductorBridgeApiError};
3778
+ // nothing here echoes a response body, the API key, or command output.
3779
+ /** The capability token the gate-profile server answers (BAPI-1390). */
3780
+ export const GATE_PROFILE_CAPABILITY_TOKEN = "gate_profile.v1";
3781
+ const GATE_PROFILE_KINDS = new Set(["provider", "local", "mixed", "none"]);
3782
+ const GATE_PROFILE_SOURCES = new Set(["stored", "legacy", "unconfirmed"]);
3783
+ const GATE_QUALIFICATION_STATES = new Set(["qualified", "unqualified", "stale", "not_required"]);
3784
+ const GATE_PROFILE_WRITE_OUTCOMES = new Set(["confirmed", "cleared", "preserved"]);
3785
+ const GATE_CHECK_OUTCOMES = new Set(["reported", "unseen", "evidence_unavailable"]);
3786
+ const EXECUTOR_JOB_STATUSES = new Set([
3787
+ "queued",
3788
+ "running",
3789
+ "succeeded",
3790
+ "failed",
3791
+ "cancelled",
3792
+ "dead_letter",
3793
+ ]);
3794
+ const GATE_DIGEST_RE = /^[0-9a-f]{64}$/;
3795
+ const GATE_COMMAND_ID_RE = /^cmd-[0-9]{2,}-[0-9a-f]{16}$/;
3796
+ const GATE_MEMBER_NAME_MAX = 200;
3797
+ const GATE_COMMAND_NAME_MAX = 120;
3798
+ const GATE_ARGV_ENTRY_MAX = 1024;
3799
+ const GATE_MAX_PROVIDER_CHECKS = 32;
3800
+ const GATE_MAX_LOCAL_COMMANDS = 16;
3801
+ const GATE_MAX_CUSTOMIZED = 64;
3802
+ /** The provider-history walk makes bounded provider calls; give it longer. */
3803
+ const GATE_CHECK_HISTORY_TIMEOUT_MS = 90_000;
3804
+ function gateName(value, max) {
3805
+ if (typeof value !== "string")
3806
+ throw readinessShapeError();
3807
+ const trimmed = value.trim();
3808
+ if (!trimmed || trimmed !== value || value.length > max || /[\u0000-\u001F\u007F]/.test(value)) {
3809
+ throw readinessShapeError();
3810
+ }
3811
+ return value;
3812
+ }
3813
+ function parseGateProfileBody(raw) {
3814
+ const o = requireObject(raw);
3815
+ const kind = requireEnum(o, "kind", GATE_PROFILE_KINDS);
3816
+ const checksRaw = o.provider_checks;
3817
+ if (!Array.isArray(checksRaw) || checksRaw.length > GATE_MAX_PROVIDER_CHECKS)
3818
+ throw readinessShapeError();
3819
+ const providerChecks = checksRaw.map((c) => gateName(requireObject(c).name, GATE_MEMBER_NAME_MAX));
3820
+ const commandsRaw = o.local_commands;
3821
+ if (!Array.isArray(commandsRaw) || commandsRaw.length > GATE_MAX_LOCAL_COMMANDS)
3822
+ throw readinessShapeError();
3823
+ const localCommands = commandsRaw.map((raw) => {
3824
+ const c = requireObject(raw);
3825
+ const id = c.id;
3826
+ const digest = c.digest;
3827
+ const argv = c.argv;
3828
+ if (typeof id !== "string" || !GATE_COMMAND_ID_RE.test(id))
3829
+ throw readinessShapeError();
3830
+ if (typeof digest !== "string" || !GATE_DIGEST_RE.test(digest))
3831
+ throw readinessShapeError();
3832
+ if (!Array.isArray(argv) ||
3833
+ argv.length === 0 ||
3834
+ argv.some((a) => typeof a !== "string" || a.length === 0 || a.length > GATE_ARGV_ENTRY_MAX)) {
3835
+ throw readinessShapeError();
3836
+ }
3837
+ const timeout = requireInt(c, "timeout_seconds");
3838
+ if (timeout < 1 || timeout > 1800)
3839
+ throw readinessShapeError();
3840
+ return {
3841
+ id,
3842
+ name: gateName(c.name, GATE_COMMAND_NAME_MAX),
3843
+ argv: argv,
3844
+ timeout_seconds: timeout,
3845
+ digest,
3846
+ };
3847
+ });
3848
+ let guard = null;
3849
+ if (o.migration_guard !== null && o.migration_guard !== undefined) {
3850
+ const g = requireObject(o.migration_guard);
3851
+ guard = {
3852
+ kind: requireEnum(g, "kind", new Set(["provider", "local"])),
3853
+ id: gateName(g.id, GATE_MEMBER_NAME_MAX),
3854
+ };
3855
+ }
3856
+ const customized = o.customized_commands ?? [];
3857
+ if (!Array.isArray(customized) || customized.length > GATE_MAX_CUSTOMIZED)
3858
+ throw readinessShapeError();
3859
+ const revision = requireInt(o, "revision");
3860
+ if (revision < 0)
3861
+ throw readinessShapeError();
3862
+ // The kind must agree with its members, exactly as the server validates it;
3863
+ // a profile that does not cannot be shown as a confirmed gate.
3864
+ const expectedKind = providerChecks.length > 0 && localCommands.length > 0
3865
+ ? "mixed"
3866
+ : providerChecks.length > 0
3867
+ ? "provider"
3868
+ : localCommands.length > 0
3869
+ ? "local"
3870
+ : "none";
3871
+ if (expectedKind !== kind)
3872
+ throw readinessShapeError();
3873
+ return {
3874
+ kind,
3875
+ provider_checks: providerChecks,
3876
+ local_commands: localCommands,
3877
+ migration_guard: guard,
3878
+ customized_commands: customized.map((n) => gateName(n, GATE_MEMBER_NAME_MAX)),
3879
+ confirmed_by: requireNullableString(o, "confirmed_by"),
3880
+ confirmed_at: requireNullableString(o, "confirmed_at"),
3881
+ revision,
3882
+ };
3883
+ }
3884
+ /** Validate a gate-profile body, fail-closed. */
3885
+ export function parseConductorGateProfileResponse(body, expectedRepoName) {
3886
+ const o = requireObject(body);
3887
+ if (o.repo_name !== expectedRepoName)
3888
+ throw readinessShapeError();
3889
+ const source = requireEnum(o, "source", GATE_PROFILE_SOURCES);
3890
+ const confirmed = requireBool(o, "confirmed");
3891
+ const revision = requireInt(o, "revision");
3892
+ if (revision < 0)
3893
+ throw readinessShapeError();
3894
+ const profile = o.profile === null || o.profile === undefined ? null : parseGateProfileBody(o.profile);
3895
+ // "Confirmed" is believed only when it is internally consistent: a confirmed
3896
+ // read needs a readable profile from a stored or legacy source whose revision
3897
+ // matches. Anything else is a shape error, never a quietly confirmed gate.
3898
+ if (confirmed && (profile === null || source === "unconfirmed" || profile.revision !== revision)) {
3899
+ throw readinessShapeError();
3900
+ }
3901
+ if (!confirmed && profile !== null)
3902
+ throw readinessShapeError();
3903
+ const outcome = o.outcome;
3904
+ if (outcome !== undefined && outcome !== null && (typeof outcome !== "string" || !GATE_PROFILE_WRITE_OUTCOMES.has(outcome))) {
3905
+ throw readinessShapeError();
3906
+ }
3907
+ return {
3908
+ repo_name: expectedRepoName,
3909
+ source,
3910
+ confirmed,
3911
+ stored_malformed: requireBool(o, "stored_malformed"),
3912
+ revision,
3913
+ profile,
3914
+ qualification_state: requireEnum(o, "qualification_state", GATE_QUALIFICATION_STATES),
3915
+ outcome: (outcome ?? null),
3916
+ };
3917
+ }
3918
+ function gateProfileUrl(access, suffix = "") {
3919
+ return buildConductorJiraUrl(access.baseUrl, `${EPIC_RUNS_API_PREFIX}/supervisor-setup/gate-profile/${suffix}`, { repo_name: access.repoName });
3920
+ }
3921
+ /** GET the repository's gate profile. Throws a sanitized error on any failure. */
3922
+ export async function fetchConductorGateProfile(access, fetchImpl) {
3923
+ const body = await fetchConductorJsonWithTimeout(gateProfileUrl(access), conductorGetHeaders(access), CONDUCTOR_FETCH_TIMEOUT_MS, fetchImpl);
3924
+ return parseConductorGateProfileResponse(body, access.repoName);
3925
+ }
3926
+ function classifyGateProfileWriteFailure(err) {
3927
+ if (!(err instanceof ConductorBridgeApiError))
3928
+ return "network";
3929
+ if (err.errorCode === "STALE_REVISION")
3930
+ return "stale-revision";
3931
+ if (err.errorCode === "UNKNOWN_STORED_STATE")
3932
+ return "unknown-stored-state";
3933
+ if (err.errorCode === "VALIDATION")
3934
+ return "validation";
3935
+ if (err.errorCode === "STATE_UNAVAILABLE")
3936
+ return "state-unavailable";
3937
+ if (err.kind === "invalid-input")
3938
+ return "unsupported-protocol";
3939
+ if (err.kind === "unauthorized")
3940
+ return "unauthorized";
3941
+ if (err.kind === "timeout")
3942
+ return "timeout";
3943
+ if (err.kind === "network")
3944
+ return "network";
3945
+ if (err.status === 404 || err.status === 405 || err.status === 422)
3946
+ return "unsupported-protocol";
3947
+ if (err.kind === "server")
3948
+ return "server";
3949
+ return "rejected";
3950
+ }
3951
+ /**
3952
+ * PUT a confirm or clear. Never throws: a refusal (403 admin gate, 409 stale
3953
+ * revision, 400 validation, an unreadable answer) is a typed outcome, and the
3954
+ * caller keeps showing the PRIOR confirmed profile, because nothing was written.
3955
+ */
3956
+ export async function putConductorGateProfile(access, request, fetchImpl) {
3957
+ try {
3958
+ const body = await fetchConductorJsonWithMethodAndTimeout("PUT", gateProfileUrl(access), conductorPostHeaders(access), JSON.stringify(request), CONDUCTOR_FETCH_TIMEOUT_MS, fetchImpl);
3959
+ return { ok: true, profile: parseConductorGateProfileResponse(body, access.repoName) };
3960
+ }
3961
+ catch (err) {
3962
+ return { ok: false, refusal: classifyGateProfileWriteFailure(err) };
3963
+ }
3964
+ }
3965
+ const EPIC_BRANCH_CI_AUDITED_FIELD_LIMIT = 8;
3966
+ /**
3967
+ * BAPI-1405 — validate a decision-write response, fail-closed. Every closed
3968
+ * value (the decision and all four evidence keys) must be a known literal; an
3969
+ * unknown literal, a missing key, or an extra evidence key throws the
3970
+ * field-free shape error rather than being coerced.
3971
+ */
3972
+ export function parseEpicBranchCiDecisionResponse(body) {
3973
+ const o = requireObject(body);
3974
+ const repoName = o.repo_name;
3975
+ if (typeof repoName !== "string" || repoName.length === 0)
3976
+ throw readinessShapeError();
3977
+ const decision = requireEnum(o, "decision", EPIC_BRANCH_CI_DECISIONS);
3978
+ const evidence = parseEpicBranchCiEvidence(o.evidence);
3979
+ const names = o.audited_field_names;
3980
+ if (!Array.isArray(names) || names.length > EPIC_BRANCH_CI_AUDITED_FIELD_LIMIT)
3981
+ throw readinessShapeError();
3982
+ const auditedFieldNames = names.map((n) => {
3983
+ if (typeof n !== "string" || !BOUNDED_TOKEN.test(n))
3984
+ throw readinessShapeError();
3985
+ return n;
3986
+ });
3987
+ return { repo_name: repoName, decision, evidence, audited_field_names: auditedFieldNames };
3988
+ }
3989
+ /**
3990
+ * BAPI-1405 — record the repository's explicit epic-branch CI decision and the
3991
+ * evidence recorded at install.
3992
+ *
3993
+ * Uses the same PUT transport as {@link putConductorGateProfile} (sanitized
3994
+ * {@link ConductorBridgeApiError}, timeout, `X-API-Key` header only), but
3995
+ * THROWS instead of returning a refusal so the install flow can tell a recorded
3996
+ * decision from one that was not written. Invalid local input (a decision other
3997
+ * than `include`/`decline`, or evidence outside child 1's closed shape) is
3998
+ * rejected with `invalid-input` BEFORE any request is sent. A 2xx answer that
3999
+ * does not validate, or that names a different repository, also throws.
4000
+ * Never call this with a `decline` synthesized from an unreadable inspection.
4001
+ */
4002
+ export async function putEpicBranchCiDecision(access, repoName, body, fetchImpl = globalThis.fetch) {
4003
+ if (typeof repoName !== "string" || repoName.length === 0) {
4004
+ throw new ConductorBridgeApiError("invalid-input");
4005
+ }
4006
+ if (typeof body?.decision !== "string" || !EPIC_BRANCH_CI_DECISIONS.has(body.decision)) {
4007
+ throw new ConductorBridgeApiError("invalid-input");
4008
+ }
4009
+ let evidence;
4010
+ try {
4011
+ evidence = parseEpicBranchCiEvidence(body.evidence);
4012
+ }
4013
+ catch {
4014
+ throw new ConductorBridgeApiError("invalid-input");
4015
+ }
4016
+ const url = buildConductorJiraUrl(access.baseUrl, `${EPIC_RUNS_API_PREFIX}/supervisor-setup/epic-branch-ci-decision/`, { repo_name: repoName });
4017
+ const response = await fetchConductorJsonWithMethodAndTimeout("PUT", url, conductorPostHeaders(access), JSON.stringify({ decision: body.decision, evidence }), CONDUCTOR_FETCH_TIMEOUT_MS, fetchImpl);
4018
+ const parsed = parseEpicBranchCiDecisionResponse(response);
4019
+ if (parsed.repo_name !== repoName)
4020
+ throw readinessShapeError();
4021
+ return parsed;
4022
+ }
4023
+ /** Validate a provider-history body; only names the caller asked about are kept. */
4024
+ export function parseConductorGateCheckHistory(body, expectedRepoName, askedNames) {
4025
+ const o = requireObject(body);
4026
+ if (o.repo_name !== expectedRepoName)
4027
+ throw readinessShapeError();
4028
+ const checksRaw = o.checks;
4029
+ if (!Array.isArray(checksRaw) || checksRaw.length > GATE_MAX_PROVIDER_CHECKS)
4030
+ throw readinessShapeError();
4031
+ const asked = new Set(askedNames);
4032
+ const checks = [];
4033
+ for (const raw of checksRaw) {
4034
+ const c = requireObject(raw);
4035
+ const name = c.name;
4036
+ if (typeof name !== "string")
4037
+ throw readinessShapeError();
4038
+ const outcome = requireEnum(c, "outcome", GATE_CHECK_OUTCOMES);
4039
+ if (asked.has(name))
4040
+ checks.push({ name, outcome });
4041
+ }
4042
+ const reason = requireNullableString(o, "unavailable_reason");
4043
+ if (reason !== null && !BOUNDED_TOKEN.test(reason))
4044
+ throw readinessShapeError();
4045
+ return { complete: requireBool(o, "complete"), unavailable_reason: reason, checks };
4046
+ }
4047
+ /**
4048
+ * GET bounded provider history for exactly `names` (at most 32). Read-only on
4049
+ * the server. Throws a sanitized error on any failure, so a caller can tell
4050
+ * "history unavailable" from a successful empty answer.
4051
+ */
4052
+ export async function fetchConductorGateCheckHistory(access, names, fetchImpl) {
4053
+ const url = new URL(gateProfileUrl(access, "check-history/"));
4054
+ for (const name of names.slice(0, GATE_MAX_PROVIDER_CHECKS))
4055
+ url.searchParams.append("check", name);
4056
+ const body = await fetchConductorJsonWithTimeout(url.toString(), conductorGetHeaders(access), GATE_CHECK_HISTORY_TIMEOUT_MS, fetchImpl);
4057
+ return parseConductorGateCheckHistory(body, access.repoName, names);
4058
+ }
4059
+ /**
4060
+ * GET the BAPI-1390 capability probe. NEVER throws and fails CLOSED: a 404 (a
4061
+ * server older than BAPI-1390), a malformed body, or a body without the token
4062
+ * all read as unsupported, so the installer writes nothing.
4063
+ */
4064
+ export async function probeConductorGateProfileCapability(access, fetchImpl) {
4065
+ let body;
4066
+ try {
4067
+ body = await fetchConductorJsonWithTimeout(gateProfileUrl(access, "capability/"), conductorGetHeaders(access), CONDUCTOR_FETCH_TIMEOUT_MS, fetchImpl);
4068
+ }
4069
+ catch (err) {
4070
+ if (err instanceof ConductorBridgeApiError) {
4071
+ if (err.kind === "unauthorized")
4072
+ return { supported: false, reason: "unauthorized" };
4073
+ if (err.kind === "network" || err.kind === "timeout")
4074
+ return { supported: false, reason: "unreachable" };
4075
+ if (err.kind === "invalid-input")
4076
+ return { supported: false, reason: "unreadable" };
4077
+ }
4078
+ return { supported: false, reason: "absent" };
4079
+ }
4080
+ if (typeof body !== "object" || body === null || Array.isArray(body)) {
4081
+ return { supported: false, reason: "unreadable" };
4082
+ }
4083
+ const o = body;
4084
+ const caps = o.capabilities;
4085
+ if (o.repo_name !== access.repoName || !Array.isArray(caps) || caps.some((c) => typeof c !== "string")) {
4086
+ return { supported: false, reason: "unreadable" };
4087
+ }
4088
+ return caps.includes(GATE_PROFILE_CAPABILITY_TOKEN)
4089
+ ? { supported: true }
4090
+ : { supported: false, reason: "absent" };
4091
+ }
4092
+ const QUALIFICATION_REFUSAL_CODES = Object.freeze({
4093
+ PROFILE_NOT_CONFIRMED: "profile-not-confirmed",
4094
+ NO_LOCAL_COMMANDS: "no-local-commands",
4095
+ STALE_REVISION: "stale-revision",
4096
+ STATE_UNAVAILABLE: "state-unavailable",
4097
+ });
4098
+ /** POST the qualification enqueue for the confirmed revision. Never throws. */
4099
+ export async function enqueueConductorGateQualification(access, request, fetchImpl) {
4100
+ let body;
4101
+ try {
4102
+ body = await fetchConductorJsonWithMethodAndTimeout("POST", gateProfileUrl(access, "qualification/"), conductorPostHeaders(access), JSON.stringify(request.expectedRevision === undefined ? {} : { expected_revision: request.expectedRevision }), CONDUCTOR_FETCH_TIMEOUT_MS, fetchImpl);
4103
+ }
4104
+ catch (err) {
4105
+ if (!(err instanceof ConductorBridgeApiError))
4106
+ return { kind: "uncertain" };
4107
+ const mapped = err.errorCode ? QUALIFICATION_REFUSAL_CODES[err.errorCode] : undefined;
4108
+ if (mapped)
4109
+ return { kind: "refused", refusal: mapped };
4110
+ if (err.kind === "unauthorized")
4111
+ return { kind: "refused", refusal: "unauthorized" };
4112
+ if (err.kind === "timeout" || err.kind === "network" || err.kind === "server") {
4113
+ return { kind: "uncertain" };
4114
+ }
4115
+ if (err.status === 404 || err.status === 405 || err.status === 422) {
4116
+ return { kind: "refused", refusal: "unsupported-protocol" };
4117
+ }
4118
+ return { kind: "refused", refusal: "rejected" };
4119
+ }
4120
+ if (typeof body !== "object" || body === null || Array.isArray(body))
4121
+ return { kind: "uncertain" };
4122
+ const o = body;
4123
+ const jobId = o.job_id;
4124
+ const revision = o.revision;
4125
+ const jobStatus = o.job_status;
4126
+ if (o.repo_name !== access.repoName ||
4127
+ typeof jobId !== "number" ||
4128
+ !Number.isSafeInteger(jobId) ||
4129
+ jobId <= 0 ||
4130
+ typeof revision !== "number" ||
4131
+ !Number.isSafeInteger(revision) ||
4132
+ revision <= 0 ||
4133
+ typeof jobStatus !== "string" ||
4134
+ !EXECUTOR_JOB_STATUSES.has(jobStatus)) {
4135
+ return { kind: "uncertain" };
4136
+ }
4137
+ return { kind: "accepted", jobId, jobStatus, revision };
4138
+ }
4139
+ export function parseConductorGateQualificationJob(body, expectedRepoName, expectedJobId) {
4140
+ const o = requireObject(body);
4141
+ if (o.repo_name !== expectedRepoName || o.job_id !== expectedJobId)
4142
+ throw readinessShapeError();
4143
+ const state = o.qualification_state;
4144
+ if (state !== null && state !== undefined && !["qualified", "unqualified", "stale"].includes(state)) {
4145
+ throw readinessShapeError();
4146
+ }
4147
+ const failure = o.failure;
4148
+ if (failure !== null && failure !== undefined && failure !== "stale" && failure !== "could_not_run") {
4149
+ throw readinessShapeError();
4150
+ }
4151
+ return {
4152
+ job_id: expectedJobId,
4153
+ job_status: requireEnum(o, "job_status", EXECUTOR_JOB_STATUSES),
4154
+ revision: requireInt(o, "revision"),
4155
+ qualification_state: (state ?? null),
4156
+ current_revision: requireNullableInt(o, "current_revision"),
4157
+ failure: (failure ?? null),
4158
+ };
4159
+ }
4160
+ /** GET one qualification job's verified state. Throws a sanitized error on failure. */
4161
+ export async function fetchConductorGateQualificationJob(access, jobId, fetchImpl) {
4162
+ const body = await fetchConductorJsonWithTimeout(gateProfileUrl(access, `qualification/${encodeURIComponent(String(jobId))}/`), conductorGetHeaders(access), CONDUCTOR_FETCH_TIMEOUT_MS, fetchImpl);
4163
+ return parseConductorGateQualificationJob(body, access.repoName, jobId);
4164
+ }