@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.
- package/README.md +6 -6
- package/build/commands.generated.js +1 -1
- package/build/conduct-epic/checkpoint-store.js +50 -4
- package/build/conduct-epic/cli.js +74 -6
- package/build/conductor/done-gate.js +39 -1
- package/build/conductor/git-ci-types.js +34 -0
- package/build/index.js +9 -9
- package/build/merge-pull-request.js +143 -16
- package/build/readme.generated.js +1 -1
- package/build/version.generated.js +2 -2
- package/package.json +1 -1
|
@@ -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
|
-
|
|
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 = {
|
|
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 =
|
|
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";
|