@bridge_gpt/mcp-server 0.2.60 → 0.2.65

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 (89) hide show
  1. package/README.md +58 -11
  2. package/build/agent-launchers/claude-executor-adapter.js +134 -3
  3. package/build/agent-launchers/executor-adapter.js +54 -2
  4. package/build/agents.generated.js +10 -1
  5. package/build/bounded-wait.js +32 -1
  6. package/build/commands.generated.js +8 -7
  7. package/build/conductor/bridge-api-client.js +678 -14
  8. package/build/conductor/bring-up-facts.js +3 -1
  9. package/build/conductor/doctor.js +23 -14
  10. package/build/conductor/epic-child-continuation.js +324 -0
  11. package/build/conductor/epic-implementer-cli.js +142 -17
  12. package/build/conductor/git-hooks.js +529 -118
  13. package/build/conductor/hook-runtime-qualification.js +340 -0
  14. package/build/conductor/merge-execution-contract.js +30 -5
  15. package/build/conductor/merge-execution-eligibility.js +10 -9
  16. package/build/conductor/readiness-sections.js +104 -20
  17. package/build/conductor/readiness.js +129 -25
  18. package/build/conductor/recovery-cli.js +354 -13
  19. package/build/conductor/recovery-operations.js +251 -5
  20. package/build/conductor/repair-contract.js +8 -0
  21. package/build/conductor-bundle-artifacts.js +1 -0
  22. package/build/credential-store.js +194 -0
  23. package/build/docs.generated.js +2 -1
  24. package/build/doctor.js +161 -7
  25. package/build/drive-epic-key.js +12 -0
  26. package/build/drive-epic.js +1318 -111
  27. package/build/epic-coherence-input-cli.js +137 -0
  28. package/build/epic-coherence-input.js +920 -0
  29. package/build/epic-coherence-input.test-fixtures.js +175 -0
  30. package/build/epic-implementer-bin.js +35 -29
  31. package/build/epic-implementer-bundle-cli.js +14 -1
  32. package/build/epic-integration-pr.js +61 -15
  33. package/build/epic-launch-profile.js +464 -0
  34. package/build/epic-run-status.js +5 -0
  35. package/build/epic-shared/bridge-client.js +46 -4
  36. package/build/executor/cli.js +48 -0
  37. package/build/executor/completion-contract.js +423 -0
  38. package/build/executor/completion-supervision.js +286 -0
  39. package/build/executor/credentials.js +19 -1
  40. package/build/executor/deps.js +65 -1
  41. package/build/executor/env.js +6 -0
  42. package/build/executor/heartbeat.js +13 -3
  43. package/build/executor/http-client.js +117 -1
  44. package/build/executor/integration-job.js +759 -0
  45. package/build/executor/job-errors.js +48 -0
  46. package/build/executor/job-runner.js +1386 -35
  47. package/build/executor/job-types.js +10 -2
  48. package/build/executor/observation.js +114 -7
  49. package/build/executor/preflight.js +99 -0
  50. package/build/executor/process-containment.js +756 -0
  51. package/build/executor/process.js +235 -5
  52. package/build/executor/prompt-spec.js +262 -1
  53. package/build/executor/provider-checks.js +69 -0
  54. package/build/executor/rebase-finalization.js +351 -0
  55. package/build/executor/reconcile-overlap-job.js +77 -84
  56. package/build/executor/required-verification.js +313 -0
  57. package/build/executor/required-work-ledger.js +1121 -0
  58. package/build/executor/results.js +142 -0
  59. package/build/executor/resume-pre-spawn.js +87 -4
  60. package/build/executor/runner.js +246 -1
  61. package/build/executor/test-declaration.js +20 -0
  62. package/build/executor/types.js +55 -0
  63. package/build/executor/worker-command.js +49 -13
  64. package/build/executor/worker-finalization.js +88 -11
  65. package/build/executor/worker-guard-hook.js +29 -4
  66. package/build/executor/worktree.js +13 -0
  67. package/build/executor-launch.js +658 -0
  68. package/build/index.js +772 -954
  69. package/build/install-bridge-conductor.js +622 -40
  70. package/build/install-bridge.js +92 -16
  71. package/build/merge-pull-request.js +1 -1
  72. package/build/merge-workflow-config.js +54 -0
  73. package/build/merge-workflow-drift.js +35 -4
  74. package/build/merge-workflow.js +386 -2
  75. package/build/pipelines.generated.js +9 -8
  76. package/build/plan-discovery.js +137 -0
  77. package/build/plan-epic-implementer-eligibility.js +2 -2
  78. package/build/plane/cli.js +37 -3
  79. package/build/plane/member-logs.js +3 -0
  80. package/build/plane/member-roster.js +7 -1
  81. package/build/plane/shutdown.js +59 -1
  82. package/build/plane/supervisor.js +148 -0
  83. package/build/readme.generated.js +1 -1
  84. package/build/scripts/executor-protocol-contract-driver.js +13 -3
  85. package/build/setup-epic.js +130 -48
  86. package/build/version.generated.js +3 -3
  87. package/build/worker-guard-hook-bin.js +2 -2
  88. package/docs/CONDUCTOR.md +116 -2
  89. package/package.json +2 -1
@@ -11,10 +11,12 @@
11
11
  */
12
12
  import os from "node:os";
13
13
  import { readFile, stat } from "node:fs/promises";
14
- import { resolveBapiCredentials } from "../credential-store.js";
14
+ import { resolveBapiCredentials, resolveEpicLaunchCredential } from "../credential-store.js";
15
+ import { readEpicLaunchProfile, resolveEpicLaunchTarget, } from "../epic-launch-profile.js";
15
16
  import { resolveStartTicketsRepoName } from "../start-tickets-repo.js";
16
17
  import { normalizePrNumber, normalizeSha } from "./git-ci-types.js";
17
18
  import { ConductorValidationError } from "./errors.js";
19
+ import { parseContinuationStatusResult, parseContinueParkedChildResult, } from "./epic-child-continuation.js";
18
20
  import { resolveDeclaredRunBaseBranch } from "./run-branch.js";
19
21
  /** Default Bridge API base URL when `BAPI_BASE_URL` is unset. */
20
22
  export const CONDUCTOR_DEFAULT_BASE_URL = "https://bridgegpt-api.com";
@@ -63,6 +65,85 @@ export async function resolveConductorBridgeApiAccess(deps = {}) {
63
65
  : CONDUCTOR_DEFAULT_BASE_URL;
64
66
  return { ok: true, access: { repoName, apiKey: credResult.credentials.apiKey, baseUrl } };
65
67
  }
68
+ /**
69
+ * The STRONGER launch-resolution contract (BAPI-1260), used by `drive-epic` and
70
+ * by a `setup-epic` that was handed a launch selection. The order is the point:
71
+ *
72
+ * 1. the selected profile is read and validated (host-local, read-only);
73
+ * 2. the TARGET is decided from `--base-url`, the profile, and `BAPI_BASE_URL`
74
+ * — conflicts refuse. With none of them set, the target is
75
+ * {@link CONDUCTOR_DEFAULT_BASE_URL} (BAPI-1272);
76
+ * 3. the repository is decided (profile binding, then `--repo`, then the
77
+ * shared resolver);
78
+ * 4. only then is a credential looked up, coupled to that target AND that
79
+ * repository ({@link resolveEpicLaunchCredential}). A stored key with no
80
+ * binding is accepted only when the target is production; a key bound to
81
+ * another endpoint, or an ambient key without a matching `BAPI_BASE_URL`,
82
+ * is still refused.
83
+ *
84
+ * Nothing is sent anywhere by this function. {@link resolveConductorBridgeApiAccess}
85
+ * is deliberately left as it is for every other client.
86
+ */
87
+ export async function resolveEpicLaunchBridgeApiAccess(selection, deps = {}) {
88
+ const env = deps.env ?? process.env;
89
+ const cwd = deps.cwd ?? process.cwd();
90
+ const homedir = deps.homedir ?? os.homedir;
91
+ const platform = deps.platform ?? process.platform;
92
+ const readFileImpl = deps.readFile ?? ((p) => readFile(p, "utf-8"));
93
+ const statImpl = deps.stat ?? ((p) => stat(p));
94
+ let profile = null;
95
+ if (selection.profileName !== undefined) {
96
+ const read = await readEpicLaunchProfile(selection.profileName, {
97
+ env,
98
+ homedir,
99
+ readFile: readFileImpl,
100
+ host: (deps.hostname ?? os.hostname)(),
101
+ });
102
+ if (!read.ok)
103
+ return { ok: false, kind: "profile-unusable", error: read.message };
104
+ profile = read.profile;
105
+ }
106
+ const target = resolveEpicLaunchTarget({
107
+ flagBaseUrl: selection.baseUrl,
108
+ flagRepo: deps.repoName,
109
+ profile,
110
+ env,
111
+ defaultBaseUrl: CONDUCTOR_DEFAULT_BASE_URL,
112
+ });
113
+ if (!target.ok)
114
+ return { ok: false, kind: target.kind, error: target.message };
115
+ const repoName = target.repoName ||
116
+ deps.repoName?.trim() ||
117
+ (await resolveStartTicketsRepoName({ env, cwd, readFile: readFileImpl }));
118
+ if (!repoName) {
119
+ return {
120
+ ok: false,
121
+ kind: "repo-missing",
122
+ error: "could not resolve repo name (set BAPI_REPO_NAME or add a valid .bridge/config)",
123
+ };
124
+ }
125
+ let cred;
126
+ try {
127
+ cred = await resolveEpicLaunchCredential(repoName, target.baseUrl, { env, homedir, platform, readFile: readFileImpl, stat: statImpl }, { productionBaseUrl: CONDUCTOR_DEFAULT_BASE_URL });
128
+ }
129
+ catch {
130
+ return { ok: false, kind: "credentials-unavailable", error: "failed to resolve Bridge API credentials" };
131
+ }
132
+ if (!cred.ok) {
133
+ const bindingFailure = cred.kind === "binding-unbound" || cred.kind === "binding-mismatch" || cred.kind === "ambient-key-unbound";
134
+ // The binding messages are fixed sentences naming the target KEY and the
135
+ // store path only; the generic store failures stay generic as before.
136
+ return bindingFailure
137
+ ? { ok: false, kind: "credential-unbound", error: cred.error }
138
+ : { ok: false, kind: "credentials-unavailable", error: "Bridge API credentials unavailable" };
139
+ }
140
+ return {
141
+ ok: true,
142
+ access: { repoName, apiKey: cred.apiKey, baseUrl: target.baseUrl },
143
+ targetSource: target.source,
144
+ profile,
145
+ };
146
+ }
66
147
  /**
67
148
  * Build a `${baseUrl}/jira${apiPath}` URL with query params, trimming trailing
68
149
  * slashes on the base so there is never a double slash after the host. Query
@@ -668,6 +749,18 @@ export async function remediateEpicTicket(access, request, fetchImpl = globalThi
668
749
  throw err;
669
750
  }
670
751
  }
752
+ /**
753
+ * BAPI-1310 (E1) — the server's refusal codes for the unexplained review-head
754
+ * stop. Each is an HTTP 409 that leaves the ticket parked.
755
+ */
756
+ export const REVIEW_HEAD_REFUSAL_CODES = [
757
+ "REVIEW_HEAD_ADOPTION_REQUIRED",
758
+ "REVIEW_HEAD_EPISODE_CONFLICT",
759
+ "REVIEW_HEAD_STATE_INVALID",
760
+ "REVIEW_HEAD_BINDING_CHANGED",
761
+ "REVIEW_HEAD_LIVE_WORK",
762
+ "REVIEW_HEAD_UNAVAILABLE",
763
+ ];
671
764
  // ---------------------------------------------------------------------------
672
765
  // Local boundary validators (reject before any fetch call)
673
766
  // ---------------------------------------------------------------------------
@@ -1169,6 +1262,108 @@ export async function unparkEpicTicket(access, request, fetchImpl = globalThis.f
1169
1262
  });
1170
1263
  return postUnparkLikeRequest(url, conductorPostHeaders(access), body, fetchImpl);
1171
1264
  }
1265
+ const TYPED_CONTINUATION_STATUSES = new Set([200, 201, 409, 503]);
1266
+ const SAFE_ERROR_CODE_RE = /^[A-Za-z0-9_.:-]{1,64}$/;
1267
+ function continuationChildPath(epicRunId, ticketKey) {
1268
+ return `${epicRunApiPath(epicRunId)}/tickets/${encodeURIComponent(ticketKey)}`;
1269
+ }
1270
+ function safeDetailErrorCode(body) {
1271
+ if (body === null || typeof body !== "object")
1272
+ return null;
1273
+ const detail = body.detail;
1274
+ if (detail === null || typeof detail !== "object")
1275
+ return null;
1276
+ const code = detail.error_code;
1277
+ return typeof code === "string" && SAFE_ERROR_CODE_RE.test(code) ? code : null;
1278
+ }
1279
+ /**
1280
+ * POST `.../tickets/{ticket_key}/continue` exactly once and classify the answer.
1281
+ *
1282
+ * Never throws for an HTTP or transport outcome — every one of them is returned
1283
+ * as a {@link ContinuationPostOutcome}, so no caller can accidentally turn a
1284
+ * timeout into "refused" by catching an exception. Never falls back to unpark.
1285
+ * The API key travels only in the header; the body carries no credential and no
1286
+ * caller-asserted authority.
1287
+ */
1288
+ export async function postContinueParkedChild(access, request, fetchImpl = globalThis.fetch, timeoutMs = CONDUCTOR_FETCH_TIMEOUT_MS) {
1289
+ const b = request.bindings;
1290
+ requireNonEmptyString(b.epicRunId);
1291
+ requireNonEmptyString(b.ticketKey);
1292
+ const url = buildConductorJiraUrl(access.baseUrl, `${continuationChildPath(b.epicRunId, b.ticketKey)}/continue`);
1293
+ const body = JSON.stringify({
1294
+ repo_name: b.repoName,
1295
+ request_id: request.requestId,
1296
+ parked_gate: b.parkedGate,
1297
+ expected_row_version: b.expectedRowVersion,
1298
+ expected_head_sha: b.expectedHeadSha,
1299
+ source_park_generation: b.sourceParkGeneration,
1300
+ request_fingerprint: request.requestFingerprint,
1301
+ ...(request.reason ? { reason: request.reason } : {}),
1302
+ });
1303
+ const controller = new AbortController();
1304
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
1305
+ try {
1306
+ let resp;
1307
+ try {
1308
+ resp = await fetchImpl(url, {
1309
+ method: "POST",
1310
+ headers: conductorPostHeaders(access),
1311
+ body,
1312
+ signal: controller.signal,
1313
+ });
1314
+ }
1315
+ catch {
1316
+ return { kind: "uncertain", reason: controller.signal.aborted ? "timeout" : "network" };
1317
+ }
1318
+ let parsed;
1319
+ let parsedOk = true;
1320
+ try {
1321
+ parsed = await resp.json();
1322
+ }
1323
+ catch {
1324
+ parsedOk = false;
1325
+ }
1326
+ if (TYPED_CONTINUATION_STATUSES.has(resp.status) && parsedOk) {
1327
+ try {
1328
+ return { kind: "result", status: resp.status, result: parseContinueParkedChildResult(parsed) };
1329
+ }
1330
+ catch {
1331
+ // Fall through: an untyped body on these statuses is classified below.
1332
+ }
1333
+ }
1334
+ if (resp.status >= 200 && resp.status < 300) {
1335
+ return { kind: "uncertain", reason: "malformed", status: resp.status };
1336
+ }
1337
+ if (resp.status >= 500) {
1338
+ return { kind: "uncertain", reason: "server", status: resp.status };
1339
+ }
1340
+ // A 4xx without a typed body is an authoritative pre-mutation rejection.
1341
+ return {
1342
+ kind: "refused_http",
1343
+ status: resp.status,
1344
+ errorCode: parsedOk ? safeDetailErrorCode(parsed) : null,
1345
+ };
1346
+ }
1347
+ finally {
1348
+ clearTimeout(timer);
1349
+ }
1350
+ }
1351
+ /**
1352
+ * GET `.../tickets/{ticket_key}/continuation` — the child's current episode and,
1353
+ * with `requestId`, that request's receipt. Read-only; bounded by the standard
1354
+ * conductor timeout. Throws {@link ConductorBridgeApiError} on transport or HTTP
1355
+ * failure and {@link ContinuationParseError} on a malformed body.
1356
+ */
1357
+ export async function fetchChildContinuationStatus(access, query, fetchImpl = globalThis.fetch) {
1358
+ requireNonEmptyString(query.epicRunId);
1359
+ requireNonEmptyString(query.ticketKey);
1360
+ const params = { repo_name: access.repoName };
1361
+ if (query.requestId)
1362
+ params.request_id = query.requestId;
1363
+ const url = buildConductorJiraUrl(access.baseUrl, `${continuationChildPath(query.epicRunId, query.ticketKey)}/continuation`, params);
1364
+ const parsed = await fetchConductorJsonWithTimeout(url, conductorGetHeaders(access), CONDUCTOR_FETCH_TIMEOUT_MS, fetchImpl);
1365
+ return parseContinuationStatusResult(parsed);
1366
+ }
1172
1367
  /**
1173
1368
  * POST `/jira/epic-runs/runs/{epic_run_id}/tickets/{ticket_key}/adopt-current-head-and-unpark`
1174
1369
  * — the recovery lane for a human/external push that drifted the PR head off the
@@ -1359,8 +1554,13 @@ const REPAIR_TOKEN_RE = /^[a-z0-9_.:-]{1,120}$/;
1359
1554
  const POLICY_DIGEST_TOKEN_RE = /^policy:[0-9a-f]{12}$/;
1360
1555
  /** A dotted policy-path NAME. Never a value. */
1361
1556
  const POLICY_PATH_RE = /^[A-Za-z0-9_-]{1,100}(?:\.[A-Za-z0-9_-]{1,100}){0,15}$/;
1362
- /** `resume-run`'s closed run-control state vocabulary. */
1363
- const RESUME_RUN_STATES = ["paused", "quarantined", "none"];
1557
+ /**
1558
+ * `resume-run`'s closed PRIOR-state vocabulary. `blocked` is the qualifying
1559
+ * integration block the run-addressed route may lift; it is never a resulting state.
1560
+ */
1561
+ const RESUME_RUN_BEFORE_STATES = ["blocked", "paused", "quarantined", "none"];
1562
+ /** `resume-run`'s closed RESULTING-state vocabulary: a resume always leaves no control. */
1563
+ const RESUME_RUN_AFTER_STATES = ["none"];
1364
1564
  function requireOnlyRepairKeys(p, extraKeys) {
1365
1565
  for (const key of Object.keys(p)) {
1366
1566
  if (!REPAIR_BASE_KEYS.includes(key) && !extraKeys.includes(key)) {
@@ -1427,18 +1627,29 @@ function parseResumeRunResult(parsed) {
1427
1627
  requireOnlyRepairKeys(p, []);
1428
1628
  const before = p["before"];
1429
1629
  const after = p["after"];
1630
+ const committed = p["committed"];
1631
+ const eventRecorded = p["event_recorded"];
1430
1632
  if (typeof before !== "string" ||
1431
- !RESUME_RUN_STATES.includes(before) ||
1633
+ !RESUME_RUN_BEFORE_STATES.includes(before) ||
1432
1634
  typeof after !== "string" ||
1433
- !RESUME_RUN_STATES.includes(after)) {
1635
+ !RESUME_RUN_AFTER_STATES.includes(after)) {
1636
+ throw new ConductorBridgeApiError("server");
1637
+ }
1638
+ // Exactly two families: an applied resume (`blocked`/`paused`/`quarantined` →
1639
+ // `none`, committed, either audit outcome) or the already-current no-op
1640
+ // (`none` → `none`, nothing committed, no event). Any other combination is
1641
+ // unconfirmed, never coerced into a result.
1642
+ const applied = before !== "none" && committed;
1643
+ const noOp = before === "none" && !committed && !eventRecorded;
1644
+ if (!applied && !noOp) {
1434
1645
  throw new ConductorBridgeApiError("server");
1435
1646
  }
1436
1647
  return {
1437
1648
  epic_run_id: p["epic_run_id"],
1438
1649
  before,
1439
1650
  after,
1440
- committed: p["committed"],
1441
- event_recorded: p["event_recorded"],
1651
+ committed,
1652
+ event_recorded: eventRecorded,
1442
1653
  };
1443
1654
  }
1444
1655
  /**
@@ -1983,12 +2194,109 @@ export async function getEpicPlan(access, epicKey, planVersion, fetchImpl = glob
1983
2194
  const parsed = await fetchConductorJsonWithTimeout(url, conductorGetHeaders(access), CONDUCTOR_FETCH_TIMEOUT_MS, fetchImpl);
1984
2195
  return parsed;
1985
2196
  }
2197
+ /**
2198
+ * BAPI-1252: the backend's closed canonical refusal vocabulary, mirrored as
2199
+ * string literals (the Python source is `api/models/canonical_index_state.py`;
2200
+ * a test pins the two together). A refusal carries exactly one of these and
2201
+ * nothing else — no SHA, generation, owner, or server error text.
2202
+ */
2203
+ export const CANONICAL_REFUSAL_REASONS = [
2204
+ "canonical_validity_unavailable",
2205
+ "canonical_index_unverified",
2206
+ "canonical_source_missing",
2207
+ "canonical_generation_mismatch",
2208
+ "canonical_manifest_mismatch",
2209
+ "canonical_index_advanced",
2210
+ "canonical_capability_unsupported",
2211
+ ];
2212
+ /** The canonical validity states the backend can report. */
2213
+ export const CANONICAL_VALIDITY_STATES = ["unverified", "writing", "verified"];
2214
+ /** The only canonical manifest versions this client accepts as evidence. */
2215
+ export const SUPPORTED_CANONICAL_MANIFEST_VERSIONS = ["v1"];
2216
+ const CANONICAL_PROVENANCE_KEYS = [
2217
+ "canonical_validity_state",
2218
+ "canonical_validity_verified",
2219
+ "canonical_source_commit_sha",
2220
+ "canonical_generation",
2221
+ "canonical_manifest_version",
2222
+ "canonical_validity_reason",
2223
+ ];
2224
+ const FULL_COMMIT_SHA_RE = /^[0-9a-f]{40}$/;
2225
+ const CANONICAL_GENERATION_RE = /^[A-Za-z0-9_.:-]{1,128}$/;
2226
+ /**
2227
+ * BAPI-1252: decode a `/jira/parse-status` body's canonical provenance, failing
2228
+ * CLOSED. Only an explicit `verified` state, a literal `true` verified flag, a
2229
+ * full lowercase 40-character source SHA, a well-formed generation, a supported
2230
+ * manifest version, and no refusal reason decode as evidence. Everything else is
2231
+ * one bounded refusal:
2232
+ *
2233
+ * - all six fields absent (an older server) → `canonical_capability_unsupported`;
2234
+ * - an unknown state, an unknown manifest version, a malformed SHA or
2235
+ * generation, a non-boolean flag, or an unknown reason token →
2236
+ * `canonical_capability_unsupported` (the client cannot prove it, so it is not
2237
+ * proof);
2238
+ * - a known refusal reason → that reason;
2239
+ * - any non-verified state → `canonical_index_unverified`.
2240
+ *
2241
+ * Pure and total: it never throws and never consults lifecycle fields such as
2242
+ * `status` or `indexed_commit_sha`.
2243
+ */
2244
+ export function decodeCanonicalProvenance(body) {
2245
+ const unsupported = { verified: false, reason: "canonical_capability_unsupported" };
2246
+ if (body === null || typeof body !== "object" || Array.isArray(body))
2247
+ return unsupported;
2248
+ const obj = body;
2249
+ const present = CANONICAL_PROVENANCE_KEYS.some((key) => key in obj && obj[key] !== undefined);
2250
+ if (!present)
2251
+ return unsupported;
2252
+ const reason = obj.canonical_validity_reason;
2253
+ if (reason !== undefined && reason !== null) {
2254
+ if (typeof reason !== "string")
2255
+ return unsupported;
2256
+ if (!CANONICAL_REFUSAL_REASONS.includes(reason))
2257
+ return unsupported;
2258
+ return { verified: false, reason: reason };
2259
+ }
2260
+ const flag = obj.canonical_validity_verified;
2261
+ if (flag !== undefined && flag !== null && typeof flag !== "boolean")
2262
+ return unsupported;
2263
+ const state = obj.canonical_validity_state;
2264
+ if (typeof state !== "string")
2265
+ return unsupported;
2266
+ if (!CANONICAL_VALIDITY_STATES.includes(state))
2267
+ return unsupported;
2268
+ if (state !== "verified" || flag !== true) {
2269
+ return { verified: false, reason: "canonical_index_unverified" };
2270
+ }
2271
+ const sha = obj.canonical_source_commit_sha;
2272
+ const generation = obj.canonical_generation;
2273
+ const version = obj.canonical_manifest_version;
2274
+ if (typeof sha !== "string" || !FULL_COMMIT_SHA_RE.test(sha))
2275
+ return unsupported;
2276
+ if (typeof generation !== "string" || !CANONICAL_GENERATION_RE.test(generation)) {
2277
+ return unsupported;
2278
+ }
2279
+ if (typeof version !== "string" ||
2280
+ !SUPPORTED_CANONICAL_MANIFEST_VERSIONS.includes(version)) {
2281
+ return unsupported;
2282
+ }
2283
+ return {
2284
+ verified: true,
2285
+ sourceCommitSha: sha,
2286
+ generation,
2287
+ manifestVersion: version,
2288
+ };
2289
+ }
1986
2290
  /**
1987
2291
  * GET `/jira/parse-status?repo_name=<repo>` and return the durable parse-run
1988
2292
  * status. See {@link ParseRepositoryStatusValue} for the full state set (the
1989
2293
  * status surface distinguishes queued/running from terminal succeeded/failed).
1990
2294
  * Throws a sanitized {@link ConductorBridgeApiError} on any transport/auth/server
1991
2295
  * failure. URL + auth are unchanged from the pre-BAPI-527 two-state client.
2296
+ *
2297
+ * BAPI-1252: the body is returned as the server sent it, so a payload from an
2298
+ * older server (no canonical fields) still decodes exactly as before. Canonical
2299
+ * provenance is read from it only through {@link decodeCanonicalProvenance}.
1992
2300
  */
1993
2301
  export async function fetchParseStatus(access, fetchImpl = globalThis.fetch) {
1994
2302
  const url = buildConductorJiraUrl(access.baseUrl, "/parse-status", {
@@ -2092,6 +2400,21 @@ export const CONDUCTOR_REVIEW_ALIGNMENT_STATUSES = new Set([
2092
2400
  "not_configured",
2093
2401
  "invalid",
2094
2402
  ]);
2403
+ /** The closed verdict vocabulary the parser accepts. */
2404
+ export const REQUIRED_CHECKS_VERIFICATIONS = new Set([
2405
+ "verified",
2406
+ "unknown_name",
2407
+ "unverifiable",
2408
+ ]);
2409
+ /** BAPI-1260 — one mandatory-evidence verdict. Only `confirmed` ever holds. */
2410
+ export const ADMISSION_EVIDENCE_VERDICTS = [
2411
+ "confirmed",
2412
+ "denied",
2413
+ "unable_to_verify",
2414
+ "not_applicable",
2415
+ ];
2416
+ /** The evidence schema this client renders. A newer one is shown as unknown, never as held. */
2417
+ export const ADMISSION_EVIDENCE_SCHEMA_VERSION = 1;
2095
2418
  /** Upper bound of the server's capped `fresh_reconciler_count` ("two or more"). */
2096
2419
  export const READINESS_FRESH_RECONCILER_COUNT_CAP = 2;
2097
2420
  const READINESS_SOURCES = new Set(["epic", "project_default", "none"]);
@@ -2139,6 +2462,20 @@ function requireNullableString(o, key) {
2139
2462
  throw readinessShapeError();
2140
2463
  return v;
2141
2464
  }
2465
+ /**
2466
+ * BAPI-1279 — the executor coverage fields. Each is omitted when the server did
2467
+ * not send it (an older deploy), and a present non-boolean fails CLOSED to
2468
+ * `false`: coverage that cannot be read is coverage that is not confirmed.
2469
+ */
2470
+ function parseOptionalExecutorCoverage(o) {
2471
+ const out = {};
2472
+ for (const key of ["process_heartbeat_fresh", "repo_wide_executor_live"]) {
2473
+ if (!(key in o))
2474
+ continue;
2475
+ out[key] = o[key] === true;
2476
+ }
2477
+ return out;
2478
+ }
2142
2479
  function requireNullableBool(o, key) {
2143
2480
  const v = o[key];
2144
2481
  if (v === null || v === undefined)
@@ -2178,6 +2515,89 @@ function requireEnum(o, key, allowed) {
2178
2515
  throw readinessShapeError();
2179
2516
  return v;
2180
2517
  }
2518
+ /** The five BAPI-1254 supervisor keys, which travel as one all-or-none group. */
2519
+ const REQUIRED_CHECKS_VERIFICATION_KEYS = [
2520
+ "required_checks_verification",
2521
+ "required_checks_mismatch_count",
2522
+ "required_checks_unverified_count",
2523
+ "required_checks_evidence_observed_at",
2524
+ "required_checks_evidence_fresh",
2525
+ ];
2526
+ /**
2527
+ * A strict ISO-8601 instant with an EXPLICIT UTC designator (`Z` or `+00:00`).
2528
+ * A timezone-free or offset time is rejected rather than read in local time.
2529
+ */
2530
+ const UTC_TIMESTAMP_PATTERN = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{1,9})?(Z|\+00:00)$/;
2531
+ function requireNonNegativeInt(o, key) {
2532
+ const v = requireInt(o, key);
2533
+ if (v < 0)
2534
+ throw readinessShapeError();
2535
+ return v;
2536
+ }
2537
+ /**
2538
+ * Parse the BAPI-1254 required-check verification group, FAIL-CLOSED.
2539
+ *
2540
+ * All five keys ABSENT is an older server: every field becomes `null`, which
2541
+ * every consumer renders as "could not verify" — absence is never verification.
2542
+ * Any key present makes all five mandatory, and each is checked for exact type:
2543
+ * the verdict against its closed vocabulary, both counts as non-negative
2544
+ * integers, the time as an explicit-UTC instant, freshness as a real boolean.
2545
+ * The server's cross-field invariants are then re-applied, because a
2546
+ * contradictory group (a `verified` with a mismatch, a stale `unknown_name`)
2547
+ * is untrusted input, not a verdict to render.
2548
+ */
2549
+ function parseRequiredChecksVerification(sup, requiredChecksCount) {
2550
+ const present = REQUIRED_CHECKS_VERIFICATION_KEYS.filter((key) => Object.prototype.hasOwnProperty.call(sup, key) && sup[key] !== undefined);
2551
+ if (present.length === 0) {
2552
+ return {
2553
+ required_checks_verification: null,
2554
+ required_checks_mismatch_count: null,
2555
+ required_checks_unverified_count: null,
2556
+ required_checks_evidence_observed_at: null,
2557
+ required_checks_evidence_fresh: null,
2558
+ };
2559
+ }
2560
+ if (present.length !== REQUIRED_CHECKS_VERIFICATION_KEYS.length)
2561
+ throw readinessShapeError();
2562
+ const verification = requireEnum(sup, "required_checks_verification", REQUIRED_CHECKS_VERIFICATIONS);
2563
+ const mismatch = requireNonNegativeInt(sup, "required_checks_mismatch_count");
2564
+ const unverified = requireNonNegativeInt(sup, "required_checks_unverified_count");
2565
+ const observedAt = sup.required_checks_evidence_observed_at;
2566
+ if (typeof observedAt !== "string" ||
2567
+ !UTC_TIMESTAMP_PATTERN.test(observedAt) ||
2568
+ Number.isNaN(Date.parse(observedAt))) {
2569
+ throw readinessShapeError();
2570
+ }
2571
+ const fresh = requireBool(sup, "required_checks_evidence_fresh");
2572
+ if (requiredChecksCount < 0 || mismatch + unverified > requiredChecksCount) {
2573
+ throw readinessShapeError();
2574
+ }
2575
+ if (verification === "verified" &&
2576
+ !(requiredChecksCount > 0 && mismatch === 0 && unverified === 0 && fresh)) {
2577
+ throw readinessShapeError();
2578
+ }
2579
+ if (verification === "unknown_name" && !(fresh && mismatch > 0))
2580
+ throw readinessShapeError();
2581
+ if (verification === "unverifiable" && mismatch > 0)
2582
+ throw readinessShapeError();
2583
+ return {
2584
+ required_checks_verification: verification,
2585
+ required_checks_mismatch_count: mismatch,
2586
+ required_checks_unverified_count: unverified,
2587
+ required_checks_evidence_observed_at: observedAt,
2588
+ required_checks_evidence_fresh: fresh,
2589
+ };
2590
+ }
2591
+ /** Optional positive-integer threshold: absent/null from an older server. */
2592
+ function parseOptionalPositiveInt(o, key) {
2593
+ const v = o[key];
2594
+ if (v === undefined || v === null)
2595
+ return null;
2596
+ const parsed = requireInt(o, key);
2597
+ if (parsed <= 0)
2598
+ throw readinessShapeError();
2599
+ return parsed;
2600
+ }
2181
2601
  /**
2182
2602
  * Validate a raw readiness body into the typed response, FAIL-CLOSED.
2183
2603
  *
@@ -2197,6 +2617,7 @@ export function parseConductorReadinessResponse(body) {
2197
2617
  const rec = requireObject(root.reconciler);
2198
2618
  const exec = requireObject(root.executor);
2199
2619
  const thr = requireObject(root.thresholds);
2620
+ const requiredChecksCount = requireInt(sup, "required_checks_count");
2200
2621
  return {
2201
2622
  repo_name: repoName,
2202
2623
  supervisor: {
@@ -2208,13 +2629,14 @@ export function parseConductorReadinessResponse(body) {
2208
2629
  config_source: requireEnum(sup, "config_source", READINESS_SOURCES),
2209
2630
  config_created_at: requireNullableString(sup, "config_created_at"),
2210
2631
  config_updated_at: requireNullableString(sup, "config_updated_at"),
2211
- required_checks_count: requireInt(sup, "required_checks_count"),
2632
+ required_checks_count: requiredChecksCount,
2212
2633
  required_checks_empty: requireBool(sup, "required_checks_empty"),
2213
2634
  auto_merge_enabled: requireBool(sup, "auto_merge_enabled"),
2214
2635
  merge_approval_required_set: requireBool(sup, "merge_approval_required_set"),
2215
2636
  review_policy_alignment: parseReviewPolicyAlignment(sup),
2216
2637
  // Absent on a pre-BAPI-694 server, where no repository default can exist.
2217
2638
  review_policy_present: sup.review_policy_present === true,
2639
+ ...parseRequiredChecksVerification(sup, requiredChecksCount),
2218
2640
  },
2219
2641
  github: {
2220
2642
  credentials_readable: requireBool(gh, "credentials_readable"),
@@ -2240,18 +2662,91 @@ export function parseConductorReadinessResponse(body) {
2240
2662
  last_seen_at: requireNullableString(exec, "last_seen_at"),
2241
2663
  last_seen_age_seconds: requireNullableInt(exec, "last_seen_age_seconds"),
2242
2664
  ready: requireNullableBool(exec, "ready"),
2665
+ ...parseOptionalExecutorCoverage(exec),
2243
2666
  },
2244
2667
  thresholds: {
2245
2668
  reconciler_stale_after_seconds: requireInt(thr, "reconciler_stale_after_seconds"),
2246
2669
  executor_stale_after_seconds: requireInt(thr, "executor_stale_after_seconds"),
2670
+ required_checks_evidence_max_age_seconds: parseOptionalPositiveInt(thr, "required_checks_evidence_max_age_seconds"),
2247
2671
  },
2248
2672
  review_workflow: parseReviewWorkflow(root),
2249
2673
  conductor_ci_workflow: parseConductorCiWorkflow(root),
2250
2674
  unattended: parseUnattended(root),
2251
2675
  bring_up: parseBringUp(root),
2252
2676
  merge_execution: parseMergeExecution(root),
2677
+ ...(() => {
2678
+ const evidence = parseAdmissionEvidence(root);
2679
+ return evidence === undefined ? {} : { admission_evidence: evidence };
2680
+ })(),
2253
2681
  };
2254
2682
  }
2683
+ const ADMISSION_EVIDENCE_OWNERS = new Set(["operator", "repository_admin", "bridge_operations"]);
2684
+ const BOUNDED_TOKEN = /^[a-z0-9][a-z0-9_.:-]{0,79}$/;
2685
+ /**
2686
+ * Parse the optional BAPI-1260 `admission_evidence` block.
2687
+ *
2688
+ * Absent → `undefined` (the key is then omitted from the parsed response).
2689
+ * Present → validated strictly, EXCEPT that an unrecognized verdict is coerced
2690
+ * to `unable_to_verify` rather than failing the whole readiness read: a newer
2691
+ * server's vocabulary must degrade to "not proven", never to "held" and never
2692
+ * to "no readiness at all". Free-text fields are bounded tokens only, so a
2693
+ * payload cannot smuggle prose into operator output.
2694
+ */
2695
+ function parseAdmissionEvidence(body) {
2696
+ const raw = body.admission_evidence;
2697
+ if (raw === undefined)
2698
+ return undefined;
2699
+ if (raw === null)
2700
+ return null;
2701
+ const o = requireObject(raw);
2702
+ const schemaVersion = requireInt(o, "schema_version");
2703
+ const observedAt = o.observed_at;
2704
+ if (typeof observedAt !== "string")
2705
+ throw readinessShapeError();
2706
+ if (!Array.isArray(o.records) || o.records.length > 32)
2707
+ throw readinessShapeError();
2708
+ const token = (value, nullable) => {
2709
+ if (value === null && nullable)
2710
+ return null;
2711
+ if (typeof value !== "string" || !BOUNDED_TOKEN.test(value))
2712
+ throw readinessShapeError();
2713
+ return value;
2714
+ };
2715
+ const records = o.records.map((entry) => {
2716
+ const r = requireObject(entry);
2717
+ const verdictRaw = r.verdict;
2718
+ const verdict = ADMISSION_EVIDENCE_VERDICTS.includes(verdictRaw)
2719
+ ? verdictRaw
2720
+ : "unable_to_verify";
2721
+ const owner = r.next_action_owner;
2722
+ if (owner !== null && owner !== undefined && !ADMISSION_EVIDENCE_OWNERS.has(owner)) {
2723
+ throw readinessShapeError();
2724
+ }
2725
+ const maxAge = requireInt(r, "max_age_seconds");
2726
+ if (maxAge <= 0)
2727
+ throw readinessShapeError();
2728
+ const at = r.observed_at;
2729
+ if (at !== null && typeof at !== "string")
2730
+ throw readinessShapeError();
2731
+ return {
2732
+ predicate: token(r.predicate, false),
2733
+ verdict,
2734
+ observed_at: at,
2735
+ source: token(r.source, false),
2736
+ source_version: r.source_version === null || r.source_version === undefined
2737
+ ? null
2738
+ : typeof r.source_version === "string" && r.source_version.length <= 80
2739
+ ? r.source_version
2740
+ : (() => {
2741
+ throw readinessShapeError();
2742
+ })(),
2743
+ max_age_seconds: maxAge,
2744
+ reason: r.reason === undefined ? null : token(r.reason, true),
2745
+ next_action_owner: (owner ?? null),
2746
+ };
2747
+ });
2748
+ return { schema_version: schemaVersion, observed_at: observedAt, records };
2749
+ }
2255
2750
  /**
2256
2751
  * Parse the optional `merge_execution` block (BAPI-1135).
2257
2752
  *
@@ -2353,8 +2848,9 @@ function parseConductorCiWorkflow(body) {
2353
2848
  *
2354
2849
  * `notify_webhook_default_verified` is the one tri-state: absent is coerced to
2355
2850
  * `null` rather than to `false`, because "not verified here" and "verified as
2356
- * broken" are different statements and only one of them is evidence. Every other
2357
- * field is a strict boolean and a wrong type throws.
2851
+ * broken" are different statements and only one of them is evidence.
2852
+ * `escalation_email_available` (BAPI-1274) is lenient: anything but `true` is
2853
+ * `false`. Every other field is a strict boolean and a wrong type throws.
2358
2854
  */
2359
2855
  function parseUnattended(body) {
2360
2856
  const raw = body.unattended;
@@ -2368,6 +2864,10 @@ function parseUnattended(body) {
2368
2864
  ? null
2369
2865
  : requireNullableBool(o, "notify_webhook_default_verified"),
2370
2866
  repository_readiness_confirmed: requireBool(o, "repository_readiness_confirmed"),
2867
+ // BAPI-1274 — optional and lenient: absent, null, or any non-boolean
2868
+ // normalizes to `false` rather than throwing, because a server older than
2869
+ // BAPI-1274 omits it and must keep today's behavior.
2870
+ escalation_email_available: o.escalation_email_available === true,
2371
2871
  };
2372
2872
  }
2373
2873
  /**
@@ -2384,6 +2884,15 @@ export async function fetchConductorReadiness(access, fetchImpl = globalThis.fet
2384
2884
  const body = await fetchConductorJsonWithTimeout(url, conductorGetHeaders(access), CONDUCTOR_FETCH_TIMEOUT_MS, fetchImpl);
2385
2885
  return parseConductorReadinessResponse(body);
2386
2886
  }
2887
+ const ROW_OUTCOMES = new Set(["created", "changed", "preserved"]);
2888
+ function optionalRowOutcome(o, key) {
2889
+ const v = o[key];
2890
+ if (v === undefined || v === null)
2891
+ return null;
2892
+ if (typeof v !== "string" || !ROW_OUTCOMES.has(v))
2893
+ throw readinessShapeError();
2894
+ return v;
2895
+ }
2387
2896
  /** Validate a raw bootstrap body, fail-closed (see {@link parseConductorReadinessResponse}). */
2388
2897
  export function parseConductorSupervisorBootstrapResponse(body) {
2389
2898
  const o = requireObject(body);
@@ -2398,6 +2907,8 @@ export function parseConductorSupervisorBootstrapResponse(body) {
2398
2907
  repo_name: repoName,
2399
2908
  setup_written: requireBool(o, "setup_written"),
2400
2909
  config_written: requireBool(o, "config_written"),
2910
+ setup_outcome: optionalRowOutcome(o, "setup_outcome"),
2911
+ config_outcome: optionalRowOutcome(o, "config_outcome"),
2401
2912
  setup_source: requireEnum(o, "setup_source", READINESS_SOURCES),
2402
2913
  config_source: requireEnum(o, "config_source", READINESS_SOURCES),
2403
2914
  setup_created_at: requireNullableString(o, "setup_created_at"),
@@ -2411,14 +2922,167 @@ export function parseConductorSupervisorBootstrapResponse(body) {
2411
2922
  /**
2412
2923
  * POST `/jira/epic-runs/supervisor-bootstrap?repo_name=<repo>`.
2413
2924
  *
2414
- * The ONLY write this client performs. Since BAPI-694 it may also carry the
2415
- * optional repository-default `review_policy`; omitting that field stores no
2416
- * default rather than a neutral one.
2925
+ * Since BAPI-694 it may also carry the optional repository-default
2926
+ * `review_policy`; omitting that field keeps the stored default. Since BAPI-1288
2927
+ * a relaxation is refused with 409 unless `approval` carries the exact token a
2928
+ * {@link previewConductorSupervisorBootstrap} of the SAME request returned.
2929
+ * A 409 is never retried here — see {@link classifySupervisorBootstrapFailure}.
2417
2930
  */
2418
2931
  export async function bootstrapConductorSupervisorDefaults(access, request, fetchImpl) {
2419
2932
  const url = buildConductorJiraUrl(access.baseUrl, `${EPIC_RUNS_API_PREFIX}/supervisor-bootstrap`, {
2420
2933
  repo_name: access.repoName,
2421
2934
  });
2422
- const body = await fetchConductorJsonPostWithTimeout(url, conductorPostHeaders(access), JSON.stringify(request), CONDUCTOR_FETCH_TIMEOUT_MS, fetchImpl);
2935
+ const body = await fetchConductorJsonWithMethodAndTimeout("POST", url, conductorPostHeaders(access), JSON.stringify(request), CONDUCTOR_FETCH_TIMEOUT_MS, fetchImpl, { readErrorField: true });
2423
2936
  return parseConductorSupervisorBootstrapResponse(body);
2424
2937
  }
2938
+ // ---------------------------------------------------------------------------
2939
+ // BAPI-1288 — preview of a protected project-default change
2940
+ // ---------------------------------------------------------------------------
2941
+ /** Closed vocabularies of a server-authored gate-change effect. */
2942
+ const GATE_CHANGE_REQUIREMENTS = new Set([
2943
+ "done_gate",
2944
+ "required_ci_checks",
2945
+ "done_gate_review",
2946
+ "review_policy_default",
2947
+ ]);
2948
+ const GATE_CHANGE_KINDS = new Set(["removed", "weakened", "replaced"]);
2949
+ const GATE_OBLIGATIONS = new Set([
2950
+ "ci_check_green",
2951
+ "sticky_verdict_approved",
2952
+ "native_review_approved",
2953
+ "approval_minimum",
2954
+ "verdictless_review_parks",
2955
+ ]);
2956
+ const CHECK_REF_RE = /^ci-[0-9a-f]{12}$/;
2957
+ /** Bounds the client enforces on server text before it may be displayed. */
2958
+ const MAX_EFFECTS = 16;
2959
+ const MAX_DESCRIPTION_LENGTH = 300;
2960
+ const MAX_CHECK_REFS = 50;
2961
+ const MAX_APPROVAL_TOKEN_LENGTH = 1024;
2962
+ function requireObligationList(o, key) {
2963
+ const v = o[key];
2964
+ if (!Array.isArray(v) || v.length > GATE_OBLIGATIONS.size)
2965
+ throw readinessShapeError();
2966
+ for (const item of v) {
2967
+ if (typeof item !== "string" || !GATE_OBLIGATIONS.has(item))
2968
+ throw readinessShapeError();
2969
+ }
2970
+ return v;
2971
+ }
2972
+ function parseGateChangeEffect(raw) {
2973
+ const o = requireObject(raw);
2974
+ const refs = o.removed_ci_check_refs;
2975
+ if (!Array.isArray(refs) ||
2976
+ refs.length > MAX_CHECK_REFS ||
2977
+ refs.some((r) => typeof r !== "string" || !CHECK_REF_RE.test(r))) {
2978
+ throw readinessShapeError();
2979
+ }
2980
+ const description = o.description;
2981
+ if (typeof description !== "string" ||
2982
+ description.length === 0 ||
2983
+ description.length > MAX_DESCRIPTION_LENGTH ||
2984
+ /[\u0000-\u001F\u007F]/.test(description)) {
2985
+ throw readinessShapeError();
2986
+ }
2987
+ const count = requireInt(o, "removed_ci_check_count");
2988
+ if (count < 0)
2989
+ throw readinessShapeError();
2990
+ return {
2991
+ row: requireEnum(o, "row", new Set(["setup", "config"])),
2992
+ requirement: requireEnum(o, "requirement", GATE_CHANGE_REQUIREMENTS),
2993
+ change: requireEnum(o, "change", GATE_CHANGE_KINDS),
2994
+ removed_obligations: requireObligationList(o, "removed_obligations"),
2995
+ replacement_obligations: requireObligationList(o, "replacement_obligations"),
2996
+ removed_ci_check_count: count,
2997
+ removed_ci_check_refs: refs,
2998
+ stored_min_approvals: requireNullableInt(o, "stored_min_approvals"),
2999
+ proposed_min_approvals: requireNullableInt(o, "proposed_min_approvals"),
3000
+ description,
3001
+ };
3002
+ }
3003
+ /**
3004
+ * Validate a raw preview body, fail-closed.
3005
+ *
3006
+ * Every field is checked against the server's closed contract; anything else —
3007
+ * an unknown outcome, an effect category this client does not know, an
3008
+ * over-long or control-character description, a token without
3009
+ * `approval_required`, a writer or repository that does not match the request —
3010
+ * is a shape error, so an unbounded server string can never reach the operator's
3011
+ * terminal as if it were a vetted description.
3012
+ */
3013
+ export function parseConductorSupervisorBootstrapPreview(body, expectedRepoName) {
3014
+ const o = requireObject(body);
3015
+ if (o.repo_name !== expectedRepoName)
3016
+ throw readinessShapeError();
3017
+ if (o.writer !== "bootstrap")
3018
+ throw readinessShapeError();
3019
+ const names = o.changed_field_names;
3020
+ if (!Array.isArray(names) || names.some((n) => typeof n !== "string" || n.length > 100)) {
3021
+ throw readinessShapeError();
3022
+ }
3023
+ const effectsRaw = o.effects;
3024
+ if (!Array.isArray(effectsRaw) || effectsRaw.length > MAX_EFFECTS)
3025
+ throw readinessShapeError();
3026
+ const effects = effectsRaw.map(parseGateChangeEffect);
3027
+ const approvalRequired = requireBool(o, "approval_required");
3028
+ const token = requireNullableString(o, "approval_token");
3029
+ if (approvalRequired !== effects.length > 0)
3030
+ throw readinessShapeError();
3031
+ if (approvalRequired !== (token !== null))
3032
+ throw readinessShapeError();
3033
+ if (token !== null && (token.length === 0 || token.length > MAX_APPROVAL_TOKEN_LENGTH)) {
3034
+ throw readinessShapeError();
3035
+ }
3036
+ return {
3037
+ repo_name: expectedRepoName,
3038
+ writer: "bootstrap",
3039
+ setup_outcome: requireEnum(o, "setup_outcome", ROW_OUTCOMES),
3040
+ config_outcome: requireEnum(o, "config_outcome", ROW_OUTCOMES),
3041
+ changed_field_names: names,
3042
+ approval_required: approvalRequired,
3043
+ effects,
3044
+ approval_token: token,
3045
+ };
3046
+ }
3047
+ /**
3048
+ * POST `/jira/epic-runs/supervisor-bootstrap/preview?repo_name=<repo>` — what a
3049
+ * bootstrap with `request` WOULD do. Read-only on the server.
3050
+ *
3051
+ * Send the same body to {@link bootstrapConductorSupervisorDefaults} afterwards
3052
+ * (plus `approval` when the preview required it); any difference makes the
3053
+ * server's token check fail with a fresh-preview conflict.
3054
+ */
3055
+ export async function previewConductorSupervisorBootstrap(access, request, fetchImpl) {
3056
+ const url = buildConductorJiraUrl(access.baseUrl, `${EPIC_RUNS_API_PREFIX}/supervisor-bootstrap/preview`, { repo_name: access.repoName });
3057
+ const body = await fetchConductorJsonWithMethodAndTimeout("POST", url, conductorPostHeaders(access), JSON.stringify(request), CONDUCTOR_FETCH_TIMEOUT_MS, fetchImpl, { readErrorField: true });
3058
+ return parseConductorSupervisorBootstrapPreview(body, access.repoName);
3059
+ }
3060
+ const GATE_CHANGE_CODE_TO_KIND = Object.freeze({
3061
+ APPROVAL_REQUIRED: "approval-required",
3062
+ STALE_PREVIEW: "stale-preview",
3063
+ UNKNOWN_STORED_STATE: "unknown-stored-state",
3064
+ NON_RELAXABLE: "non-relaxable",
3065
+ INVALID_PROPOSAL: "invalid-proposal",
3066
+ STATE_UNAVAILABLE: "state-unavailable",
3067
+ });
3068
+ export function classifySupervisorBootstrapFailure(err) {
3069
+ if (!(err instanceof ConductorBridgeApiError))
3070
+ return "network";
3071
+ const mapped = err.errorCode ? GATE_CHANGE_CODE_TO_KIND[err.errorCode] : undefined;
3072
+ if (mapped)
3073
+ return mapped;
3074
+ if (err.kind === "invalid-input")
3075
+ return "unsupported-protocol";
3076
+ if (err.kind === "unauthorized")
3077
+ return "unauthorized";
3078
+ if (err.kind === "timeout")
3079
+ return "timeout";
3080
+ if (err.kind === "network")
3081
+ return "network";
3082
+ if (err.status === 404 || err.status === 405 || err.status === 422) {
3083
+ return "unsupported-protocol";
3084
+ }
3085
+ if (err.kind === "server")
3086
+ return "server";
3087
+ return "rejected";
3088
+ }