@bridge_gpt/mcp-server 0.2.59 → 0.2.62

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 (78) hide show
  1. package/README.md +181 -51
  2. package/build/agent-guidance.generated.js +1 -1
  3. package/build/agent-notes.js +7 -1
  4. package/build/commands.generated.js +6 -6
  5. package/build/conductor/bridge-api-client.js +406 -4
  6. package/build/conductor/doctor.js +255 -53
  7. package/build/conductor/epic-runtime.js +11 -2
  8. package/build/conductor/install-doctor.js +24 -2
  9. package/build/conductor/local-merge.js +319 -26
  10. package/build/conductor/merge-execution-contract.js +86 -0
  11. package/build/conductor/merge-execution-eligibility.js +130 -0
  12. package/build/conductor/readiness-sections.js +256 -16
  13. package/build/conductor/readiness.js +203 -29
  14. package/build/conductor/recovery-operations.js +8 -2
  15. package/build/connect-bitbucket-api.js +6 -1
  16. package/build/connect-bitbucket.js +398 -88
  17. package/build/credential-store.js +172 -0
  18. package/build/docs.generated.js +2 -2
  19. package/build/doctor.js +277 -15
  20. package/build/drive-epic.js +147 -12
  21. package/build/epic-implementer-bin.js +15 -15
  22. package/build/epic-implementer-claude-hook-bin.js +1 -1
  23. package/build/epic-implementer-env.js +43 -14
  24. package/build/epic-integration-pr.js +61 -15
  25. package/build/epic-launch-profile.js +455 -0
  26. package/build/executor/ci-rerun-job.js +269 -0
  27. package/build/executor/cli.js +40 -0
  28. package/build/executor/credentials.js +28 -1
  29. package/build/executor/env.js +60 -0
  30. package/build/executor/http-client.js +45 -1
  31. package/build/executor/index.js +21 -4
  32. package/build/executor/integration-job.js +759 -0
  33. package/build/executor/job-runner.js +218 -9
  34. package/build/executor/job-types.js +12 -2
  35. package/build/executor/merge-job.js +11 -2
  36. package/build/executor/observation.js +114 -7
  37. package/build/executor/runner.js +101 -1
  38. package/build/executor/service-lifecycle.js +136 -0
  39. package/build/executor/service-unit.js +427 -9
  40. package/build/executor/types.js +48 -0
  41. package/build/index.js +479 -240
  42. package/build/install-bridge-conductor.js +599 -14
  43. package/build/install-bridge.js +448 -46
  44. package/build/mcp-install-state.js +42 -4
  45. package/build/merge-pull-request.js +103 -10
  46. package/build/merge-workflow-codeowners.js +224 -0
  47. package/build/merge-workflow-config.js +187 -0
  48. package/build/merge-workflow-drift.js +97 -0
  49. package/build/merge-workflow-install.js +638 -0
  50. package/build/merge-workflow.js +956 -0
  51. package/build/pipelines.generated.js +3 -3
  52. package/build/plane/cli.js +24 -2
  53. package/build/plane/preflight.js +68 -0
  54. package/build/plane/supervisor.js +6 -0
  55. package/build/readme.generated.js +1 -1
  56. package/build/run-unit-tests-launcher.js +5 -0
  57. package/build/setup-epic.js +263 -38
  58. package/build/version.generated.js +3 -3
  59. package/package.json +3 -3
  60. package/build/conduct-epic/bridge-client.js +0 -773
  61. package/build/conduct-epic/checkpoint-store.js +0 -542
  62. package/build/conduct-epic/cli.js +0 -3145
  63. package/build/conduct-epic/cut-protocol.js +0 -392
  64. package/build/conduct-epic/lock.js +0 -302
  65. package/build/conduct-epic/pr-state.js +0 -286
  66. package/build/conduct-epic/spawn.js +0 -113
  67. package/build/conductor/cli.js +0 -1278
  68. package/build/conductor-bundle-cli.js +0 -256
  69. package/build/direct-ticket-tools.js +0 -70
  70. package/build/executor/reconcile-overlap-governance.js +0 -129
  71. package/build/implement-epic/bridge-client.js +0 -773
  72. package/build/implement-epic/checkpoint-store.js +0 -542
  73. package/build/implement-epic/cli.js +0 -3158
  74. package/build/implement-epic/cut-protocol.js +0 -392
  75. package/build/implement-epic/lock.js +0 -302
  76. package/build/implement-epic/pr-state.js +0 -286
  77. package/build/implement-epic/spawn.js +0 -113
  78. package/build/plan-epic-conductor-eligibility.js +0 -213
@@ -11,7 +11,8 @@
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";
@@ -63,6 +64,87 @@ export async function resolveConductorBridgeApiAccess(deps = {}) {
63
64
  : CONDUCTOR_DEFAULT_BASE_URL;
64
65
  return { ok: true, access: { repoName, apiKey: credResult.credentials.apiKey, baseUrl } };
65
66
  }
67
+ /**
68
+ * The STRONGER launch-resolution contract (BAPI-1260), used by `drive-epic` and
69
+ * by a `setup-epic` that was handed a launch selection. The order is the point:
70
+ *
71
+ * 1. the selected profile is read and validated (host-local, read-only);
72
+ * 2. the TARGET is decided from `--base-url`, the profile, and `BAPI_BASE_URL`
73
+ * — conflicts refuse, and an absent target is refused, never defaulted to
74
+ * {@link CONDUCTOR_DEFAULT_BASE_URL};
75
+ * 3. the repository is decided (profile binding, then `--repo`, then the
76
+ * shared resolver);
77
+ * 4. only then is a credential looked up, coupled to that target AND that
78
+ * repository ({@link resolveEpicLaunchCredential}).
79
+ *
80
+ * Nothing is sent anywhere by this function. {@link resolveConductorBridgeApiAccess}
81
+ * is deliberately left as it is for every other client.
82
+ */
83
+ export async function resolveEpicLaunchBridgeApiAccess(selection, deps = {}) {
84
+ const env = deps.env ?? process.env;
85
+ const cwd = deps.cwd ?? process.cwd();
86
+ const homedir = deps.homedir ?? os.homedir;
87
+ const platform = deps.platform ?? process.platform;
88
+ const readFileImpl = deps.readFile ?? ((p) => readFile(p, "utf-8"));
89
+ const statImpl = deps.stat ?? ((p) => stat(p));
90
+ let profile = null;
91
+ if (selection.profileName !== undefined) {
92
+ const read = await readEpicLaunchProfile(selection.profileName, {
93
+ env,
94
+ homedir,
95
+ readFile: readFileImpl,
96
+ host: (deps.hostname ?? os.hostname)(),
97
+ });
98
+ if (!read.ok)
99
+ return { ok: false, kind: "profile-unusable", error: read.message };
100
+ profile = read.profile;
101
+ }
102
+ const target = resolveEpicLaunchTarget({
103
+ flagBaseUrl: selection.baseUrl,
104
+ flagRepo: deps.repoName,
105
+ profile,
106
+ env,
107
+ });
108
+ if (!target.ok)
109
+ return { ok: false, kind: target.kind, error: target.message };
110
+ const repoName = target.repoName ||
111
+ deps.repoName?.trim() ||
112
+ (await resolveStartTicketsRepoName({ env, cwd, readFile: readFileImpl }));
113
+ if (!repoName) {
114
+ return {
115
+ ok: false,
116
+ kind: "repo-missing",
117
+ error: "could not resolve repo name (set BAPI_REPO_NAME or add a valid .bridge/config)",
118
+ };
119
+ }
120
+ let cred;
121
+ try {
122
+ cred = await resolveEpicLaunchCredential(repoName, target.baseUrl, {
123
+ env,
124
+ homedir,
125
+ platform,
126
+ readFile: readFileImpl,
127
+ stat: statImpl,
128
+ });
129
+ }
130
+ catch {
131
+ return { ok: false, kind: "credentials-unavailable", error: "failed to resolve Bridge API credentials" };
132
+ }
133
+ if (!cred.ok) {
134
+ const bindingFailure = cred.kind === "binding-unbound" || cred.kind === "binding-mismatch" || cred.kind === "ambient-key-unbound";
135
+ // The binding messages are fixed sentences naming the target KEY and the
136
+ // store path only; the generic store failures stay generic as before.
137
+ return bindingFailure
138
+ ? { ok: false, kind: "credential-unbound", error: cred.error }
139
+ : { ok: false, kind: "credentials-unavailable", error: "Bridge API credentials unavailable" };
140
+ }
141
+ return {
142
+ ok: true,
143
+ access: { repoName, apiKey: cred.apiKey, baseUrl: target.baseUrl },
144
+ targetSource: target.source,
145
+ profile,
146
+ };
147
+ }
66
148
  /**
67
149
  * Build a `${baseUrl}/jira${apiPath}` URL with query params, trimming trailing
68
150
  * slashes on the base so there is never a double slash after the host. Query
@@ -364,8 +446,15 @@ export function safeDiagnosticMessage(err, fallback) {
364
446
  }
365
447
  return fallback;
366
448
  }
367
- /** GET auth headers. The API key travels ONLY in a header, never in the URL. */
368
- function conductorGetHeaders(access) {
449
+ /**
450
+ * GET auth headers. The API key travels ONLY in a header, never in the URL.
451
+ *
452
+ * Exported (BAPI-1133) so a caller outside this module — `setup-epic.ts`'s
453
+ * {@link ConductorBridgeApiAccess}-based adapter for
454
+ * `resolveMergeExecutionMode`'s narrow dependency shape — can build the same
455
+ * headers without a second copy of this one-line mapping.
456
+ */
457
+ export function conductorGetHeaders(access) {
369
458
  return { "X-API-Key": access.apiKey };
370
459
  }
371
460
  /**
@@ -1976,12 +2065,109 @@ export async function getEpicPlan(access, epicKey, planVersion, fetchImpl = glob
1976
2065
  const parsed = await fetchConductorJsonWithTimeout(url, conductorGetHeaders(access), CONDUCTOR_FETCH_TIMEOUT_MS, fetchImpl);
1977
2066
  return parsed;
1978
2067
  }
2068
+ /**
2069
+ * BAPI-1252: the backend's closed canonical refusal vocabulary, mirrored as
2070
+ * string literals (the Python source is `api/models/canonical_index_state.py`;
2071
+ * a test pins the two together). A refusal carries exactly one of these and
2072
+ * nothing else — no SHA, generation, owner, or server error text.
2073
+ */
2074
+ export const CANONICAL_REFUSAL_REASONS = [
2075
+ "canonical_validity_unavailable",
2076
+ "canonical_index_unverified",
2077
+ "canonical_source_missing",
2078
+ "canonical_generation_mismatch",
2079
+ "canonical_manifest_mismatch",
2080
+ "canonical_index_advanced",
2081
+ "canonical_capability_unsupported",
2082
+ ];
2083
+ /** The canonical validity states the backend can report. */
2084
+ export const CANONICAL_VALIDITY_STATES = ["unverified", "writing", "verified"];
2085
+ /** The only canonical manifest versions this client accepts as evidence. */
2086
+ export const SUPPORTED_CANONICAL_MANIFEST_VERSIONS = ["v1"];
2087
+ const CANONICAL_PROVENANCE_KEYS = [
2088
+ "canonical_validity_state",
2089
+ "canonical_validity_verified",
2090
+ "canonical_source_commit_sha",
2091
+ "canonical_generation",
2092
+ "canonical_manifest_version",
2093
+ "canonical_validity_reason",
2094
+ ];
2095
+ const FULL_COMMIT_SHA_RE = /^[0-9a-f]{40}$/;
2096
+ const CANONICAL_GENERATION_RE = /^[A-Za-z0-9_.:-]{1,128}$/;
2097
+ /**
2098
+ * BAPI-1252: decode a `/jira/parse-status` body's canonical provenance, failing
2099
+ * CLOSED. Only an explicit `verified` state, a literal `true` verified flag, a
2100
+ * full lowercase 40-character source SHA, a well-formed generation, a supported
2101
+ * manifest version, and no refusal reason decode as evidence. Everything else is
2102
+ * one bounded refusal:
2103
+ *
2104
+ * - all six fields absent (an older server) → `canonical_capability_unsupported`;
2105
+ * - an unknown state, an unknown manifest version, a malformed SHA or
2106
+ * generation, a non-boolean flag, or an unknown reason token →
2107
+ * `canonical_capability_unsupported` (the client cannot prove it, so it is not
2108
+ * proof);
2109
+ * - a known refusal reason → that reason;
2110
+ * - any non-verified state → `canonical_index_unverified`.
2111
+ *
2112
+ * Pure and total: it never throws and never consults lifecycle fields such as
2113
+ * `status` or `indexed_commit_sha`.
2114
+ */
2115
+ export function decodeCanonicalProvenance(body) {
2116
+ const unsupported = { verified: false, reason: "canonical_capability_unsupported" };
2117
+ if (body === null || typeof body !== "object" || Array.isArray(body))
2118
+ return unsupported;
2119
+ const obj = body;
2120
+ const present = CANONICAL_PROVENANCE_KEYS.some((key) => key in obj && obj[key] !== undefined);
2121
+ if (!present)
2122
+ return unsupported;
2123
+ const reason = obj.canonical_validity_reason;
2124
+ if (reason !== undefined && reason !== null) {
2125
+ if (typeof reason !== "string")
2126
+ return unsupported;
2127
+ if (!CANONICAL_REFUSAL_REASONS.includes(reason))
2128
+ return unsupported;
2129
+ return { verified: false, reason: reason };
2130
+ }
2131
+ const flag = obj.canonical_validity_verified;
2132
+ if (flag !== undefined && flag !== null && typeof flag !== "boolean")
2133
+ return unsupported;
2134
+ const state = obj.canonical_validity_state;
2135
+ if (typeof state !== "string")
2136
+ return unsupported;
2137
+ if (!CANONICAL_VALIDITY_STATES.includes(state))
2138
+ return unsupported;
2139
+ if (state !== "verified" || flag !== true) {
2140
+ return { verified: false, reason: "canonical_index_unverified" };
2141
+ }
2142
+ const sha = obj.canonical_source_commit_sha;
2143
+ const generation = obj.canonical_generation;
2144
+ const version = obj.canonical_manifest_version;
2145
+ if (typeof sha !== "string" || !FULL_COMMIT_SHA_RE.test(sha))
2146
+ return unsupported;
2147
+ if (typeof generation !== "string" || !CANONICAL_GENERATION_RE.test(generation)) {
2148
+ return unsupported;
2149
+ }
2150
+ if (typeof version !== "string" ||
2151
+ !SUPPORTED_CANONICAL_MANIFEST_VERSIONS.includes(version)) {
2152
+ return unsupported;
2153
+ }
2154
+ return {
2155
+ verified: true,
2156
+ sourceCommitSha: sha,
2157
+ generation,
2158
+ manifestVersion: version,
2159
+ };
2160
+ }
1979
2161
  /**
1980
2162
  * GET `/jira/parse-status?repo_name=<repo>` and return the durable parse-run
1981
2163
  * status. See {@link ParseRepositoryStatusValue} for the full state set (the
1982
2164
  * status surface distinguishes queued/running from terminal succeeded/failed).
1983
2165
  * Throws a sanitized {@link ConductorBridgeApiError} on any transport/auth/server
1984
2166
  * failure. URL + auth are unchanged from the pre-BAPI-527 two-state client.
2167
+ *
2168
+ * BAPI-1252: the body is returned as the server sent it, so a payload from an
2169
+ * older server (no canonical fields) still decodes exactly as before. Canonical
2170
+ * provenance is read from it only through {@link decodeCanonicalProvenance}.
1985
2171
  */
1986
2172
  export async function fetchParseStatus(access, fetchImpl = globalThis.fetch) {
1987
2173
  const url = buildConductorJiraUrl(access.baseUrl, "/parse-status", {
@@ -2085,10 +2271,27 @@ export const CONDUCTOR_REVIEW_ALIGNMENT_STATUSES = new Set([
2085
2271
  "not_configured",
2086
2272
  "invalid",
2087
2273
  ]);
2274
+ /** The closed verdict vocabulary the parser accepts. */
2275
+ export const REQUIRED_CHECKS_VERIFICATIONS = new Set([
2276
+ "verified",
2277
+ "unknown_name",
2278
+ "unverifiable",
2279
+ ]);
2280
+ /** BAPI-1260 — one mandatory-evidence verdict. Only `confirmed` ever holds. */
2281
+ export const ADMISSION_EVIDENCE_VERDICTS = [
2282
+ "confirmed",
2283
+ "denied",
2284
+ "unable_to_verify",
2285
+ "not_applicable",
2286
+ ];
2287
+ /** The evidence schema this client renders. A newer one is shown as unknown, never as held. */
2288
+ export const ADMISSION_EVIDENCE_SCHEMA_VERSION = 1;
2088
2289
  /** Upper bound of the server's capped `fresh_reconciler_count` ("two or more"). */
2089
2290
  export const READINESS_FRESH_RECONCILER_COUNT_CAP = 2;
2090
2291
  const READINESS_SOURCES = new Set(["epic", "project_default", "none"]);
2091
2292
  const ACTIONS_LEVELS = new Set(["write", "read", "none", "unknown"]);
2293
+ /** The one scope the server reports for the merge-execution block (BAPI-1135). */
2294
+ const MERGE_EXECUTION_BRANCH_SCOPES = new Set(["default_branch"]);
2092
2295
  function readinessShapeError() {
2093
2296
  // Deliberately carries NO field name, value, or payload excerpt: a malformed
2094
2297
  // body is untrusted input and its contents must not reach output.
@@ -2169,6 +2372,89 @@ function requireEnum(o, key, allowed) {
2169
2372
  throw readinessShapeError();
2170
2373
  return v;
2171
2374
  }
2375
+ /** The five BAPI-1254 supervisor keys, which travel as one all-or-none group. */
2376
+ const REQUIRED_CHECKS_VERIFICATION_KEYS = [
2377
+ "required_checks_verification",
2378
+ "required_checks_mismatch_count",
2379
+ "required_checks_unverified_count",
2380
+ "required_checks_evidence_observed_at",
2381
+ "required_checks_evidence_fresh",
2382
+ ];
2383
+ /**
2384
+ * A strict ISO-8601 instant with an EXPLICIT UTC designator (`Z` or `+00:00`).
2385
+ * A timezone-free or offset time is rejected rather than read in local time.
2386
+ */
2387
+ const UTC_TIMESTAMP_PATTERN = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{1,9})?(Z|\+00:00)$/;
2388
+ function requireNonNegativeInt(o, key) {
2389
+ const v = requireInt(o, key);
2390
+ if (v < 0)
2391
+ throw readinessShapeError();
2392
+ return v;
2393
+ }
2394
+ /**
2395
+ * Parse the BAPI-1254 required-check verification group, FAIL-CLOSED.
2396
+ *
2397
+ * All five keys ABSENT is an older server: every field becomes `null`, which
2398
+ * every consumer renders as "could not verify" — absence is never verification.
2399
+ * Any key present makes all five mandatory, and each is checked for exact type:
2400
+ * the verdict against its closed vocabulary, both counts as non-negative
2401
+ * integers, the time as an explicit-UTC instant, freshness as a real boolean.
2402
+ * The server's cross-field invariants are then re-applied, because a
2403
+ * contradictory group (a `verified` with a mismatch, a stale `unknown_name`)
2404
+ * is untrusted input, not a verdict to render.
2405
+ */
2406
+ function parseRequiredChecksVerification(sup, requiredChecksCount) {
2407
+ const present = REQUIRED_CHECKS_VERIFICATION_KEYS.filter((key) => Object.prototype.hasOwnProperty.call(sup, key) && sup[key] !== undefined);
2408
+ if (present.length === 0) {
2409
+ return {
2410
+ required_checks_verification: null,
2411
+ required_checks_mismatch_count: null,
2412
+ required_checks_unverified_count: null,
2413
+ required_checks_evidence_observed_at: null,
2414
+ required_checks_evidence_fresh: null,
2415
+ };
2416
+ }
2417
+ if (present.length !== REQUIRED_CHECKS_VERIFICATION_KEYS.length)
2418
+ throw readinessShapeError();
2419
+ const verification = requireEnum(sup, "required_checks_verification", REQUIRED_CHECKS_VERIFICATIONS);
2420
+ const mismatch = requireNonNegativeInt(sup, "required_checks_mismatch_count");
2421
+ const unverified = requireNonNegativeInt(sup, "required_checks_unverified_count");
2422
+ const observedAt = sup.required_checks_evidence_observed_at;
2423
+ if (typeof observedAt !== "string" ||
2424
+ !UTC_TIMESTAMP_PATTERN.test(observedAt) ||
2425
+ Number.isNaN(Date.parse(observedAt))) {
2426
+ throw readinessShapeError();
2427
+ }
2428
+ const fresh = requireBool(sup, "required_checks_evidence_fresh");
2429
+ if (requiredChecksCount < 0 || mismatch + unverified > requiredChecksCount) {
2430
+ throw readinessShapeError();
2431
+ }
2432
+ if (verification === "verified" &&
2433
+ !(requiredChecksCount > 0 && mismatch === 0 && unverified === 0 && fresh)) {
2434
+ throw readinessShapeError();
2435
+ }
2436
+ if (verification === "unknown_name" && !(fresh && mismatch > 0))
2437
+ throw readinessShapeError();
2438
+ if (verification === "unverifiable" && mismatch > 0)
2439
+ throw readinessShapeError();
2440
+ return {
2441
+ required_checks_verification: verification,
2442
+ required_checks_mismatch_count: mismatch,
2443
+ required_checks_unverified_count: unverified,
2444
+ required_checks_evidence_observed_at: observedAt,
2445
+ required_checks_evidence_fresh: fresh,
2446
+ };
2447
+ }
2448
+ /** Optional positive-integer threshold: absent/null from an older server. */
2449
+ function parseOptionalPositiveInt(o, key) {
2450
+ const v = o[key];
2451
+ if (v === undefined || v === null)
2452
+ return null;
2453
+ const parsed = requireInt(o, key);
2454
+ if (parsed <= 0)
2455
+ throw readinessShapeError();
2456
+ return parsed;
2457
+ }
2172
2458
  /**
2173
2459
  * Validate a raw readiness body into the typed response, FAIL-CLOSED.
2174
2460
  *
@@ -2188,6 +2474,7 @@ export function parseConductorReadinessResponse(body) {
2188
2474
  const rec = requireObject(root.reconciler);
2189
2475
  const exec = requireObject(root.executor);
2190
2476
  const thr = requireObject(root.thresholds);
2477
+ const requiredChecksCount = requireInt(sup, "required_checks_count");
2191
2478
  return {
2192
2479
  repo_name: repoName,
2193
2480
  supervisor: {
@@ -2199,13 +2486,14 @@ export function parseConductorReadinessResponse(body) {
2199
2486
  config_source: requireEnum(sup, "config_source", READINESS_SOURCES),
2200
2487
  config_created_at: requireNullableString(sup, "config_created_at"),
2201
2488
  config_updated_at: requireNullableString(sup, "config_updated_at"),
2202
- required_checks_count: requireInt(sup, "required_checks_count"),
2489
+ required_checks_count: requiredChecksCount,
2203
2490
  required_checks_empty: requireBool(sup, "required_checks_empty"),
2204
2491
  auto_merge_enabled: requireBool(sup, "auto_merge_enabled"),
2205
2492
  merge_approval_required_set: requireBool(sup, "merge_approval_required_set"),
2206
2493
  review_policy_alignment: parseReviewPolicyAlignment(sup),
2207
2494
  // Absent on a pre-BAPI-694 server, where no repository default can exist.
2208
2495
  review_policy_present: sup.review_policy_present === true,
2496
+ ...parseRequiredChecksVerification(sup, requiredChecksCount),
2209
2497
  },
2210
2498
  github: {
2211
2499
  credentials_readable: requireBool(gh, "credentials_readable"),
@@ -2235,11 +2523,125 @@ export function parseConductorReadinessResponse(body) {
2235
2523
  thresholds: {
2236
2524
  reconciler_stale_after_seconds: requireInt(thr, "reconciler_stale_after_seconds"),
2237
2525
  executor_stale_after_seconds: requireInt(thr, "executor_stale_after_seconds"),
2526
+ required_checks_evidence_max_age_seconds: parseOptionalPositiveInt(thr, "required_checks_evidence_max_age_seconds"),
2238
2527
  },
2239
2528
  review_workflow: parseReviewWorkflow(root),
2240
2529
  conductor_ci_workflow: parseConductorCiWorkflow(root),
2241
2530
  unattended: parseUnattended(root),
2242
2531
  bring_up: parseBringUp(root),
2532
+ merge_execution: parseMergeExecution(root),
2533
+ ...(() => {
2534
+ const evidence = parseAdmissionEvidence(root);
2535
+ return evidence === undefined ? {} : { admission_evidence: evidence };
2536
+ })(),
2537
+ };
2538
+ }
2539
+ const ADMISSION_EVIDENCE_OWNERS = new Set(["operator", "repository_admin", "bridge_operations"]);
2540
+ const BOUNDED_TOKEN = /^[a-z0-9][a-z0-9_.:-]{0,79}$/;
2541
+ /**
2542
+ * Parse the optional BAPI-1260 `admission_evidence` block.
2543
+ *
2544
+ * Absent → `undefined` (the key is then omitted from the parsed response).
2545
+ * Present → validated strictly, EXCEPT that an unrecognized verdict is coerced
2546
+ * to `unable_to_verify` rather than failing the whole readiness read: a newer
2547
+ * server's vocabulary must degrade to "not proven", never to "held" and never
2548
+ * to "no readiness at all". Free-text fields are bounded tokens only, so a
2549
+ * payload cannot smuggle prose into operator output.
2550
+ */
2551
+ function parseAdmissionEvidence(body) {
2552
+ const raw = body.admission_evidence;
2553
+ if (raw === undefined)
2554
+ return undefined;
2555
+ if (raw === null)
2556
+ return null;
2557
+ const o = requireObject(raw);
2558
+ const schemaVersion = requireInt(o, "schema_version");
2559
+ const observedAt = o.observed_at;
2560
+ if (typeof observedAt !== "string")
2561
+ throw readinessShapeError();
2562
+ if (!Array.isArray(o.records) || o.records.length > 32)
2563
+ throw readinessShapeError();
2564
+ const token = (value, nullable) => {
2565
+ if (value === null && nullable)
2566
+ return null;
2567
+ if (typeof value !== "string" || !BOUNDED_TOKEN.test(value))
2568
+ throw readinessShapeError();
2569
+ return value;
2570
+ };
2571
+ const records = o.records.map((entry) => {
2572
+ const r = requireObject(entry);
2573
+ const verdictRaw = r.verdict;
2574
+ const verdict = ADMISSION_EVIDENCE_VERDICTS.includes(verdictRaw)
2575
+ ? verdictRaw
2576
+ : "unable_to_verify";
2577
+ const owner = r.next_action_owner;
2578
+ if (owner !== null && owner !== undefined && !ADMISSION_EVIDENCE_OWNERS.has(owner)) {
2579
+ throw readinessShapeError();
2580
+ }
2581
+ const maxAge = requireInt(r, "max_age_seconds");
2582
+ if (maxAge <= 0)
2583
+ throw readinessShapeError();
2584
+ const at = r.observed_at;
2585
+ if (at !== null && typeof at !== "string")
2586
+ throw readinessShapeError();
2587
+ return {
2588
+ predicate: token(r.predicate, false),
2589
+ verdict,
2590
+ observed_at: at,
2591
+ source: token(r.source, false),
2592
+ source_version: r.source_version === null || r.source_version === undefined
2593
+ ? null
2594
+ : typeof r.source_version === "string" && r.source_version.length <= 80
2595
+ ? r.source_version
2596
+ : (() => {
2597
+ throw readinessShapeError();
2598
+ })(),
2599
+ max_age_seconds: maxAge,
2600
+ reason: r.reason === undefined ? null : token(r.reason, true),
2601
+ next_action_owner: (owner ?? null),
2602
+ };
2603
+ });
2604
+ return { schema_version: schemaVersion, observed_at: observedAt, records };
2605
+ }
2606
+ /**
2607
+ * Parse the optional `merge_execution` block (BAPI-1135).
2608
+ *
2609
+ * Same older-server rule as every optional block above: absent (or null) is
2610
+ * `null`, and a PRESENT block is validated strictly — every boolean through
2611
+ * `requireBool`, the scope through the shared enum parser, and the version as
2612
+ * either `null` or a non-negative integer through `requireInt`.
2613
+ *
2614
+ * A malformed PRESENT block or a malformed present FIELD throws, and is never
2615
+ * degraded to `null`. The distinction is load-bearing: `null` means "this server
2616
+ * does not report it", which is a server-version fact, while a block that claims
2617
+ * to carry merge eligibility and cannot be read is untrusted input. Silently
2618
+ * turning the second into the first would let a corrupt body masquerade as an
2619
+ * older deploy, and the two have different remediations.
2620
+ */
2621
+ function parseMergeExecution(body) {
2622
+ const raw = body.merge_execution;
2623
+ if (raw === undefined || raw === null)
2624
+ return null;
2625
+ const o = requireObject(raw);
2626
+ const version = o.merge_workflow_version;
2627
+ let parsedVersion;
2628
+ if (version === null || version === undefined) {
2629
+ parsedVersion = null;
2630
+ }
2631
+ else {
2632
+ parsedVersion = requireInt(o, "merge_workflow_version");
2633
+ if (parsedVersion < 0)
2634
+ throw readinessShapeError();
2635
+ }
2636
+ return {
2637
+ branch_scope: requireEnum(o, "branch_scope", MERGE_EXECUTION_BRANCH_SCOPES),
2638
+ branch_protection_probe_succeeded: requireBool(o, "branch_protection_probe_succeeded"),
2639
+ branch_protection_actions_bot_admitted: requireBool(o, "branch_protection_actions_bot_admitted"),
2640
+ merge_workflow_probe_succeeded: requireBool(o, "merge_workflow_probe_succeeded"),
2641
+ merge_workflow_present: requireBool(o, "merge_workflow_present"),
2642
+ merge_workflow_enabled: requireBool(o, "merge_workflow_enabled"),
2643
+ merge_workflow_version: parsedVersion,
2644
+ merge_workflow_token_write_authority: requireBool(o, "merge_workflow_token_write_authority"),
2243
2645
  };
2244
2646
  }
2245
2647
  /**