@bridge_gpt/mcp-server 0.2.43 → 0.2.45

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.
@@ -40,7 +40,7 @@ import { getDefaultSpawnTerminalTabForPlatform, detectTerminal, createDefaultSta
40
40
  import { resolveWorktrunkBinary } from "../start-tickets-prereqs.js";
41
41
  import { resolveRequiredStartTicketsRepoName } from "../start-tickets-repo.js";
42
42
  import { bootstrapIndexScope, getConfigFieldBaseBranch, getConductorReadiness, getIndexScopeLifecycle, getIndexScopeStatus, getEffectiveSupervisorConfig, getEffectiveSupervisorSetup, getEpicRunState, getParseStatus, getPrReviewStatus, heartbeatIndexScope, pollCiChecks, putSupervisorConfigDefaults, reclaimIndexScope, recoverIndexScope, resolveCiChecks, retireIndexScope, } from "./bridge-client.js";
43
- import { appendTicketJournal, createInitialConductEpicCheckpoint, readConductEpicCheckpoint, resolveConductEpicCheckpointPath, resolveConductEpicLockPath, writeConductEpicCheckpointAtomic, CONDUCT_EPIC_TICKET_STATUSES, } from "./checkpoint-store.js";
43
+ import { appendTicketJournal, createInitialConductEpicCheckpoint, readConductEpicCheckpoint, resolveConductEpicCheckpointPath, resolveConductEpicLockPath, writeConductEpicCheckpointAtomic, CONDUCT_EPIC_REVIEW_VERDICTLESS_CEILING, CONDUCT_EPIC_TICKET_STATUSES, } from "./checkpoint-store.js";
44
44
  import { acquireConductEpicLock, inspectConductEpicLock, isConductEpicLockOwnerAlive, } from "./lock.js";
45
45
  import { discoverConductEpicPrState, discoverTicketWorktree, parseGitWorktreePorcelain, } from "./pr-state.js";
46
46
  import { spawnConductEpicAgentTab, CONDUCT_EPIC_AGENTS, } from "./spawn.js";
@@ -75,6 +75,8 @@ const TICKET_FIELDS = [
75
75
  "spawned_at",
76
76
  "parse_requested_at",
77
77
  "parse_requested_for_sha",
78
+ "review_verdictless_observations",
79
+ "review_verdictless_for_sha",
78
80
  "respawns",
79
81
  "conflict_attempts",
80
82
  "counters.sessions_spawned",
@@ -1404,6 +1406,22 @@ export async function runConductEpicStatus(deps, options) {
1404
1406
  let doneGateRequired = null;
1405
1407
  let reviewOptedIn = false;
1406
1408
  let reviewSource = null;
1409
+ let reviewDisposition = null;
1410
+ // BAPI-915. An UNREADABLE review policy is not an absent one.
1411
+ //
1412
+ // Before this, an unparseable `done_gate_config` yielded `conditions: []`,
1413
+ // which set `reviewOptedIn = false` — so the pilot read a malformed review
1414
+ // policy as "no review opt-in" and merged on CI alone. That is precisely the
1415
+ // accidental fail-open the Python `INVALID` sentinel exists to prevent, and it
1416
+ // cannot coexist with this ticket's invariant that no malformed input can
1417
+ // select `fail_open`.
1418
+ //
1419
+ // The parser's own `reason` draws the line: `unset` and `disabled` are a
1420
+ // genuine operator opt-out and stay opted out; `malformed` and every
1421
+ // `invalid: …` reason are a policy we could not read, and surface as
1422
+ // opted-in / unavailable / config-invalid so the command's Row 10 handles them
1423
+ // and — having no readable condition — parks under the default disposition.
1424
+ let reviewConfigInvalid = false;
1407
1425
  if (access !== null) {
1408
1426
  const setup = await getEffectiveSupervisorSetup(access, deps.fetchImpl);
1409
1427
  if (!setup.ok) {
@@ -1411,6 +1429,14 @@ export async function runConductEpicStatus(deps, options) {
1411
1429
  }
1412
1430
  else {
1413
1431
  const gate = parseDoneGateConfig(setup.value.done_gate_config);
1432
+ if (gate.reason === "malformed" || gate.reason.startsWith("invalid:")) {
1433
+ reviewConfigInvalid = true;
1434
+ reviewOptedIn = true;
1435
+ // No readable condition exists, so no disposition may be read from one.
1436
+ // `null` leaves the command's effective default at `park`.
1437
+ reviewSource = null;
1438
+ reviewDisposition = null;
1439
+ }
1414
1440
  for (const condition of gate.conditions) {
1415
1441
  if (condition.type === "required_ci_checks_green") {
1416
1442
  doneGateRequired = [...condition.required_checks];
@@ -1418,6 +1444,7 @@ export async function runConductEpicStatus(deps, options) {
1418
1444
  else if (condition.type === "review_state") {
1419
1445
  reviewOptedIn = true;
1420
1446
  reviewSource = condition.source;
1447
+ reviewDisposition = condition.verdictless_disposition ?? null;
1421
1448
  }
1422
1449
  }
1423
1450
  }
@@ -1425,21 +1452,52 @@ export async function runConductEpicStatus(deps, options) {
1425
1452
  const ci = access === null || pr?.head_sha == null
1426
1453
  ? null
1427
1454
  : await collectCiFacts(deps, access, pr.head_sha, doneGateRequired, checkpoint.ci_last_poll, probeErrors);
1455
+ // BAPI-915: the three additional fields are appended to every shape this
1456
+ // sub-object takes, so a consumer never has to branch on whether they exist.
1457
+ const reviewPolicyFacts = {
1458
+ verdictless_disposition: reviewDisposition,
1459
+ verdictless_ceiling: CONDUCT_EPIC_REVIEW_VERDICTLESS_CEILING,
1460
+ config_invalid: reviewConfigInvalid,
1461
+ };
1428
1462
  let review = {
1429
1463
  opted_in: reviewOptedIn,
1430
1464
  source: reviewSource,
1431
1465
  available: null,
1432
1466
  verdict: null,
1433
1467
  head_sha: null,
1468
+ ...reviewPolicyFacts,
1434
1469
  };
1435
- if (access !== null && reviewOptedIn && pr?.number != null) {
1470
+ // A config we could not read is reported as opted-in and UNAVAILABLE without
1471
+ // any read being attempted: there is no condition to evaluate a review
1472
+ // against, so `available: false` is the honest answer rather than `null`.
1473
+ if (reviewConfigInvalid) {
1474
+ review = {
1475
+ opted_in: true,
1476
+ source: null,
1477
+ available: false,
1478
+ verdict: null,
1479
+ head_sha: null,
1480
+ ...reviewPolicyFacts,
1481
+ };
1482
+ }
1483
+ else if (access !== null && reviewOptedIn && pr?.number != null) {
1436
1484
  const status = await getPrReviewStatus(access, pr.number, deps.fetchImpl);
1437
1485
  if (!status.ok) {
1438
1486
  probeErrors.push({ probe: "review", reason: status.error });
1439
- review = { opted_in: true, source: reviewSource, available: null, verdict: null, head_sha: null };
1487
+ review = {
1488
+ opted_in: true,
1489
+ source: reviewSource,
1490
+ available: null,
1491
+ verdict: null,
1492
+ head_sha: null,
1493
+ ...reviewPolicyFacts,
1494
+ };
1440
1495
  }
1441
1496
  else {
1442
- review = normalizeReviewStatus(status.value, reviewOptedIn, reviewSource);
1497
+ review = {
1498
+ ...normalizeReviewStatus(status.value, reviewOptedIn, reviewSource),
1499
+ ...reviewPolicyFacts,
1500
+ };
1443
1501
  }
1444
1502
  }
1445
1503
  let parse = null;
@@ -1744,6 +1802,8 @@ export function projectConductEpicTicketFacts(ticket, discoveredBranch) {
1744
1802
  spawned_at: ticket.spawned_at,
1745
1803
  parse_requested_at: ticket.parse_requested_at,
1746
1804
  parse_requested_for_sha: ticket.parse_requested_for_sha,
1805
+ review_verdictless_observations: ticket.review_verdictless_observations,
1806
+ review_verdictless_for_sha: ticket.review_verdictless_for_sha,
1747
1807
  respawns: ticket.respawns,
1748
1808
  conflict_attempts: ticket.conflict_attempts,
1749
1809
  counters: { ...ticket.counters },
@@ -2064,7 +2124,11 @@ function applyFieldAssignment(checkpoint, ticketIndex, assignment) {
2064
2124
  return null;
2065
2125
  }
2066
2126
  case "parse_requested_at":
2067
- case "parse_requested_for_sha": {
2127
+ case "parse_requested_for_sha":
2128
+ // BAPI-915: the head the verdictless counter is bound to. Reuses this
2129
+ // nullable-string branch so a tick can clear the binding as deliberately as
2130
+ // it sets it — the counter resets by writing `null` here, not by hand-editing.
2131
+ case "review_verdictless_for_sha": {
2068
2132
  // The durable half of the post-merge causal barrier (BAPI-825/A2). Both
2069
2133
  // follow the module's nullable-string convention so a tick can clear them
2070
2134
  // as deliberately as it sets them — an operator re-running a parse writes
@@ -2080,7 +2144,11 @@ function applyFieldAssignment(checkpoint, ticketIndex, assignment) {
2080
2144
  return null;
2081
2145
  }
2082
2146
  case "respawns":
2083
- case "conflict_attempts": {
2147
+ case "conflict_attempts":
2148
+ // BAPI-915: the head-bound verdictless observation count. Reuses this
2149
+ // non-negative-integer branch, so a negative or non-integer value is
2150
+ // refused by the same parser that guards every other per-ticket counter.
2151
+ case "review_verdictless_observations": {
2084
2152
  const parsed = parseIntegerField(value);
2085
2153
  if (parsed === null)
2086
2154
  return `${name} must be a non-negative integer.`;
@@ -10,7 +10,7 @@
10
10
  * `repo + pr_number + head_sha` binding. This module is pure (no I/O) and never
11
11
  * throws for caller input.
12
12
  */
13
- import { DEFAULT_GATE_NAME, REQUIRED_CI_CHECKS_GREEN, REVIEW_STATE, normalizeCheckName, normalizeSha, stableJsonHash, } from "./git-ci-types.js";
13
+ import { DEFAULT_GATE_NAME, REQUIRED_CI_CHECKS_GREEN, REVIEW_STATE, VERDICTLESS_DISPOSITIONS, normalizeCheckName, normalizeSha, stableJsonHash, } from "./git-ci-types.js";
14
14
  // ---------------------------------------------------------------------------
15
15
  // Config parsing
16
16
  // ---------------------------------------------------------------------------
@@ -148,6 +148,20 @@ function parseReviewStateCondition(entry) {
148
148
  return null;
149
149
  condition.logic = "and";
150
150
  }
151
+ // BAPI-915: exact match against the closed set, with the same all-or-nothing
152
+ // posture every field above uses. No case folding and no trimming, so "Park"
153
+ // and " park" are refused rather than repaired — a value we had to guess at is
154
+ // not a value the operator configured. A malformed spelling invalidates the
155
+ // WHOLE entry (`return null`) instead of dropping just this field, which is
156
+ // what makes "no malformed input can ever select fail_open" true: the only way
157
+ // to reach `fail_open` is to spell it exactly.
158
+ if (entry.verdictless_disposition !== undefined) {
159
+ if (typeof entry.verdictless_disposition !== "string" ||
160
+ !VERDICTLESS_DISPOSITIONS.includes(entry.verdictless_disposition)) {
161
+ return null;
162
+ }
163
+ condition.verdictless_disposition = entry.verdictless_disposition;
164
+ }
151
165
  // For combination source, at least one sub-source must be active; otherwise
152
166
  // the condition would trivially pass with an empty failures array (fail-open).
153
167
  if (condition.source === "combination") {
@@ -241,6 +255,18 @@ export function parseDoneGateConfig(value) {
241
255
  r.min_approvals = c.min_approvals;
242
256
  if (c.logic !== undefined)
243
257
  r.logic = c.logic;
258
+ // BAPI-915: the disposition belongs in the gate IDENTITY because the
259
+ // identity is signed into the merge action key (`merge-pull-request.ts`,
260
+ // `makeMergeActionKey`), and the disposition changes WHAT THE GATE ADMITS.
261
+ // Two configs that differ only in disposition are two different gates and
262
+ // must not share an action key.
263
+ //
264
+ // The `!== undefined` guard is load-bearing in the other direction: every
265
+ // config written before this field existed must hash BYTE-IDENTICALLY to
266
+ // what it hashed before, or every stored action key changes at once. That
267
+ // is why the effective `park` default is never materialized here.
268
+ if (c.verdictless_disposition !== undefined)
269
+ r.verdictless_disposition = c.verdictless_disposition;
244
270
  return r;
245
271
  }),
246
272
  });
@@ -453,6 +479,18 @@ export function normalizeReviewSnapshot(raw) {
453
479
  * Deterministically evaluate a review-state condition against a normalized snapshot.
454
480
  * Pure and fail-closed: a null snapshot or any unknown/missing source yields
455
481
  * `passed: false` and `changesRequested: false` (never a false positive).
482
+ *
483
+ * **`condition.verdictless_disposition` is deliberately IGNORED here (BAPI-915).**
484
+ * This function answers one question — does the review evidence satisfy the
485
+ * configured source? — and it decides `gate.met` for the v2 done-gate producer
486
+ * path. A disposition is a decision about what to do when the answer is "no", not
487
+ * a different answer; letting it flip a verdictless evaluation to `passed` would
488
+ * silently turn a v2 done gate fail-open for consumers that never asked for one,
489
+ * and those consumers cannot tell an evaluated pass from a waived one. The two
490
+ * merge seams apply the disposition themselves, where the waiver is recorded and
491
+ * visible. A test pins this inertness — a given snapshot must yield the same
492
+ * `{passed, changesRequested, reason}` under `park`, under `fail_open`, and with
493
+ * the field absent.
456
494
  */
457
495
  export function evaluateReviewCondition(condition, snapshot) {
458
496
  if (snapshot === null) {
@@ -143,3 +143,37 @@ export function stableJsonHash(value) {
143
143
  const json = JSON.stringify(canonical) ?? "null";
144
144
  return createHash("sha256").update(json).digest("hex");
145
145
  }
146
+ /**
147
+ * BAPI-915 — the terminal disposition a bounded verdictless-review ceiling commits.
148
+ *
149
+ * `park` is the pre-BAPI-915 behaviour and the effective default whenever the
150
+ * field is absent, so every configuration written before this existed keeps its
151
+ * exact terminal handling. `fail_open` is the CONFIGURED exception: it treats a
152
+ * readable, same-head, verdictless review as opted out for that decision and lets
153
+ * CI evidence alone carry the merge.
154
+ *
155
+ * The two literals are exported as named constants so no consumer spells them
156
+ * inline — a typo in one consumer would otherwise silently disagree with the
157
+ * parser about which disposition is configured. The Python mirror lives in
158
+ * `api/library/epic_conductor/conductor_readiness.py` and must carry the same
159
+ * closed set.
160
+ */
161
+ export const VERDICTLESS_DISPOSITION_PARK = "park";
162
+ export const VERDICTLESS_DISPOSITION_FAIL_OPEN = "fail_open";
163
+ /** Every accepted disposition spelling, for exact-match validation. */
164
+ export const VERDICTLESS_DISPOSITIONS = [
165
+ VERDICTLESS_DISPOSITION_PARK,
166
+ VERDICTLESS_DISPOSITION_FAIL_OPEN,
167
+ ];
168
+ /**
169
+ * BAPI-915 — the TypeScript mirror of v2's
170
+ * `MERGE_REVIEW_WAIVED_BY_DEGRADATION_REASON`, whose Python owner is
171
+ * `api/models/epic_run.py`. ONE spelling exists across both conductors on
172
+ * purpose: an operator greps a single string to find every merge that advanced
173
+ * on a waiver rather than on a verdict.
174
+ *
175
+ * It never claims the pull request was reviewed or approved — it names the
176
+ * degradation exactly. `tests/pytest/library/epic_conductor/` carries the
177
+ * cross-language pin that keeps this literal equal to the Python one.
178
+ */
179
+ export const MERGE_REVIEW_WAIVED_BY_DEGRADATION_REASON = "review_waived_verdictless_fail_open";