@bridge_gpt/mcp-server 0.2.52 → 0.2.54

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 (52) hide show
  1. package/README.md +121 -15
  2. package/build/agent-launchers/claude.js +3 -3
  3. package/build/agent-launchers/prompt.js +8 -11
  4. package/build/base-ref.js +33 -9
  5. package/build/bounded-wait.js +174 -0
  6. package/build/commands.generated.js +7 -5
  7. package/build/conductor/bridge-api-client.js +97 -8
  8. package/build/conductor/cli.js +23 -0
  9. package/build/conductor/doctor.js +428 -5
  10. package/build/conductor/epic-runtime.js +133 -97
  11. package/build/conductor/install-doctor.js +65 -656
  12. package/build/conductor/readiness-cli.js +152 -0
  13. package/build/conductor/readiness-sections.js +666 -0
  14. package/build/conductor/readiness.js +795 -0
  15. package/build/conductor/run-branch.js +137 -0
  16. package/build/conductor/test-run-branch-vectors.js +165 -0
  17. package/build/conductor/tools.js +56 -3
  18. package/build/conductor-bin.js +21 -17
  19. package/build/doctor.js +68 -1
  20. package/build/drive-epic.js +287 -51
  21. package/build/executor/claim-scope.js +104 -0
  22. package/build/executor/cli.js +14 -25
  23. package/build/executor/env-file-guard.js +82 -3
  24. package/build/executor/job-runner.js +60 -0
  25. package/build/index.js +4496 -4697
  26. package/build/install-doctor.js +154 -2
  27. package/build/local-artifact-storage.js +130 -0
  28. package/build/pipelines.generated.js +17 -10
  29. package/build/plane/alembic-head.js +40 -11
  30. package/build/plane/build-freshness.js +22 -11
  31. package/build/plane/cli.js +285 -36
  32. package/build/plane/manifest.js +209 -1
  33. package/build/plane/member-roster.js +70 -0
  34. package/build/plane/preflight.js +363 -48
  35. package/build/plane/shutdown.js +14 -1
  36. package/build/plane/status.js +35 -1
  37. package/build/plane/supervisor.js +546 -164
  38. package/build/plane/types.js +61 -2
  39. package/build/polling-policy.js +72 -0
  40. package/build/readiness-check.js +412 -0
  41. package/build/readme.generated.js +1 -1
  42. package/build/review-generation.js +219 -0
  43. package/build/run-unit-tests-launcher.js +5 -0
  44. package/build/setup-epic.js +514 -23
  45. package/build/ticket-key-utils.js +4 -3
  46. package/build/ticket-review-artifact-gate.js +461 -0
  47. package/build/upgrade-cli.js +5 -26
  48. package/build/version.generated.js +3 -3
  49. package/docs/install/mcp-tool-integrations.md +23 -1
  50. package/package.json +2 -2
  51. package/pipelines/{full-automation.json → idea-to-pr.json} +1 -1
  52. package/pipelines/review-ticket.json +17 -4
@@ -39,6 +39,7 @@ import { createDefaultStartTicketsDeps, orchestrateStartTickets } from "../start
39
39
  import { orchestrateReviewTickets } from "../review-tickets.js";
40
40
  import { createStartTicketsConductorContext, provisionConductorHooksForRows, emitStartTicketsRunStarted, } from "../start-tickets-conductor.js";
41
41
  import { validateBranchName } from "../base-ref.js";
42
+ import { resolveDeclaredRunBaseBranch } from "./run-branch.js";
42
43
  // ---------------------------------------------------------------------------
43
44
  // Constants
44
45
  // ---------------------------------------------------------------------------
@@ -1108,15 +1109,26 @@ export async function runEpicTick(options, deps = {}) {
1108
1109
  // tickets, gated by quiescence. Invert the impl-dispatch maps so ledger
1109
1110
  // events can be attributed back to a ticket for local-first PR binding.
1110
1111
  const dispatchedBackstopPolicy = resolveDispatchedBackstopPolicy(epicRunState.epic_run.policy_json);
1111
- // BAPI-586: resolve the run's configured base branch so the done-gate pass
1112
- // can catch a wrong-base PR (one not targeting the run base) at
1113
- // reconciliation time — the backstop to the executor's own finalization
1114
- // guard. A malformed configured base defaults to undefined (base check
1115
- // skipped) rather than failing the whole tick; dispatch already fails
1116
- // closed on a malformed base separately.
1117
- const runBaseResolution = resolveConfiguredRunBaseBranch(epicRunState.epic_run.policy_json);
1112
+ // BAPI-586/BAPI-1127: resolve the run's EFFECTIVE base branch so the
1113
+ // done-gate pass can catch a wrong-base PR (one not targeting the run
1114
+ // base) at reconciliation time — the backstop to the executor's own
1115
+ // finalization guard.
1116
+ //
1117
+ // BAPI-1127 moved branch-name validation out of the shared declaration
1118
+ // resolver and into this operational wrapper, so this call site now owns
1119
+ // the fail-closed disposition it used to inherit. It fails CLOSED: when
1120
+ // the declared base is not a usable ref the pass is SKIPPED entirely
1121
+ // rather than run with the base comparison silently disabled. A pass that
1122
+ // cannot verify a PR's base cannot honestly admit one as done, and the
1123
+ // wrong-base PR this check exists to catch is exactly what would slip
1124
+ // through. The skip happens BEFORE any PR binding resolution, CI
1125
+ // observation, or provider call, and reports only the failed rule.
1126
+ const runBaseResolution = resolveEffectiveRunBaseBranch(epicRunState.epic_run.policy_json);
1127
+ if (!runBaseResolution.ok) {
1128
+ errorLog(`[epic-tick] done-gate pass skipped for ${epic_key}: ${runBaseResolution.error}`);
1129
+ }
1118
1130
  const doneGateExpectedBaseBranch = runBaseResolution.ok
1119
- ? runBaseResolution.baseBranch
1131
+ ? runBaseResolution.effectiveBaseBranch
1120
1132
  : undefined;
1121
1133
  const ticketForRunId = new Map();
1122
1134
  for (const [tk, rid] of ticketRunIdMap)
@@ -1134,63 +1146,65 @@ export async function runEpicTick(options, deps = {}) {
1134
1146
  policy: dispatchedBackstopPolicy,
1135
1147
  resolveImplRunId: resolveTicketRunId,
1136
1148
  });
1137
- await runConductorDoneGatePass(observed.ticket_statuses, {
1138
- observePrCi: observePrCiSeamFn,
1139
- // BAPI-586: the run base so the done-gate pass fails a wrong-base PR
1140
- // before CI/review evaluation (reconciliation backstop to executor
1141
- // finalization).
1142
- expectedBaseBranch: doneGateExpectedBaseBranch,
1143
- resolvePrBinding,
1144
- // BAPI-525 Change B: local-first binding + quiescence gate for the new
1145
- // dispatched/running admission (policy default OFF no behavior change).
1146
- resolveActivePrBinding,
1147
- dispatchedBackstopPolicy,
1148
- resolveActiveTicketQuiescence,
1149
- // BAPI-487: re-evaluate a blocked ticket only when its PR head advanced
1150
- // past the head recorded on its latest blocking signal.
1151
- resolveBlockedHeadSha: (tk) => observed.ticket_blocked_heads?.get(tk) ?? null,
1152
- resolveRunId: resolveTicketRunId,
1153
- resolveWorkerId: resolveDoneGateWorkerId,
1154
- // BAPI-494: convert a detected conflict into a durable, head-scoped
1155
- // `merge.conflict` ledger event stamped with the ticket's dispatch run/worker
1156
- // so the fold correlates it and the remediation pass redispatches. Emitted via
1157
- // the shared injectable emitter, idempotent per conflict head. Folded next tick
1158
- // (this pass runs after rebuildObservedState), matching the gate.met latency.
1159
- emitConflictSignal: (input) => {
1160
- emitConductorEventFn({
1161
- source: MERGE_CONFLICT_EVENT_SOURCE,
1162
- type: "merge.conflict",
1163
- subject: input.ticketKey,
1164
- run_id: input.runId,
1165
- worker_id: input.workerId,
1166
- producer: MERGE_CONFLICT_EVENT_PRODUCER,
1167
- observed_via: "supervisor",
1168
- time: new Date(nowFn()).toISOString(),
1169
- data: {
1170
- summary: `PR #${input.prNumber} for ${input.ticketKey} is not mergeable`,
1171
- status: "blocked",
1172
- reason: "merge.conflict",
1173
- details: {
1174
- epic_key,
1175
- ticket_key: input.ticketKey,
1176
- repo: input.repoName,
1177
- pr_number: input.prNumber,
1178
- head_sha: input.headSha,
1179
- mergeable: input.mergeable,
1180
- mergeStateStatus: input.mergeStateStatus,
1149
+ if (runBaseResolution.ok) {
1150
+ await runConductorDoneGatePass(observed.ticket_statuses, {
1151
+ observePrCi: observePrCiSeamFn,
1152
+ // BAPI-586: the run base so the done-gate pass fails a wrong-base PR
1153
+ // before CI/review evaluation (reconciliation backstop to executor
1154
+ // finalization).
1155
+ expectedBaseBranch: doneGateExpectedBaseBranch,
1156
+ resolvePrBinding,
1157
+ // BAPI-525 Change B: local-first binding + quiescence gate for the new
1158
+ // dispatched/running admission (policy default OFF ⇒ no behavior change).
1159
+ resolveActivePrBinding,
1160
+ dispatchedBackstopPolicy,
1161
+ resolveActiveTicketQuiescence,
1162
+ // BAPI-487: re-evaluate a blocked ticket only when its PR head advanced
1163
+ // past the head recorded on its latest blocking signal.
1164
+ resolveBlockedHeadSha: (tk) => observed.ticket_blocked_heads?.get(tk) ?? null,
1165
+ resolveRunId: resolveTicketRunId,
1166
+ resolveWorkerId: resolveDoneGateWorkerId,
1167
+ // BAPI-494: convert a detected conflict into a durable, head-scoped
1168
+ // `merge.conflict` ledger event stamped with the ticket's dispatch run/worker
1169
+ // so the fold correlates it and the remediation pass redispatches. Emitted via
1170
+ // the shared injectable emitter, idempotent per conflict head. Folded next tick
1171
+ // (this pass runs after rebuildObservedState), matching the gate.met latency.
1172
+ emitConflictSignal: (input) => {
1173
+ emitConductorEventFn({
1174
+ source: MERGE_CONFLICT_EVENT_SOURCE,
1175
+ type: "merge.conflict",
1176
+ subject: input.ticketKey,
1177
+ run_id: input.runId,
1178
+ worker_id: input.workerId,
1179
+ producer: MERGE_CONFLICT_EVENT_PRODUCER,
1180
+ observed_via: "supervisor",
1181
+ time: new Date(nowFn()).toISOString(),
1182
+ data: {
1183
+ summary: `PR #${input.prNumber} for ${input.ticketKey} is not mergeable`,
1184
+ status: "blocked",
1185
+ reason: "merge.conflict",
1186
+ details: {
1187
+ epic_key,
1188
+ ticket_key: input.ticketKey,
1189
+ repo: input.repoName,
1190
+ pr_number: input.prNumber,
1191
+ head_sha: input.headSha,
1192
+ mergeable: input.mergeable,
1193
+ mergeStateStatus: input.mergeStateStatus,
1194
+ },
1181
1195
  },
1182
- },
1183
- }, {
1184
- event_type: "merge.conflict",
1185
- run_id: input.runId ?? undefined,
1186
- commit_sha: input.headSha,
1187
- });
1188
- },
1189
- access,
1190
- env,
1191
- log,
1192
- errorLog,
1193
- });
1196
+ }, {
1197
+ event_type: "merge.conflict",
1198
+ run_id: input.runId ?? undefined,
1199
+ commit_sha: input.headSha,
1200
+ });
1201
+ },
1202
+ access,
1203
+ env,
1204
+ log,
1205
+ errorLog,
1206
+ });
1207
+ }
1194
1208
  const maxSeqForRun = (runId) => {
1195
1209
  let maxSeq = 0;
1196
1210
  for (const ev of localEvents) {
@@ -1554,39 +1568,46 @@ export async function runEpicTick(options, deps = {}) {
1554
1568
  }
1555
1569
  }
1556
1570
  /**
1557
- * BAPI-586: resolve the epic run's configured base branch from `policy_json`
1558
- * (`base_branch`, else `baseBranch`), mirroring the server-side reconciler's
1559
- * spec-review base resolution. The authoritative run base — NOT a hard-coded
1560
- * `main` is what every fresh dispatch and PR must target.
1571
+ * BAPI-586/BAPI-1127: resolve the epic run's EFFECTIVE base branch the branch
1572
+ * every fresh dispatch cuts from and every child PR targets.
1573
+ *
1574
+ * Two layers, deliberately kept apart:
1561
1575
  *
1562
- * - absent/empty `main` (the legacy/default value).
1563
- * - present and a valid branch name → that branch (e.g. `develop`).
1564
- * - present but malformed (non-string, `..`, `.lock`, control chars, leading `-`)
1565
- * a contract failure so dispatch fails CLOSED rather than emitting a malformed
1566
- * base (a wrong base is exactly the BAPI-586 defect).
1576
+ * 1. DECLARATION, delegated in full to the dependency-free
1577
+ * {@link resolveDeclaredRunBaseBranch} leaf, which is the shared cross-runtime
1578
+ * contract this function does not get a second opinion about. The ordered
1579
+ * `base_branch` `baseBranch` scan, the skip-and-continue treatment of a
1580
+ * non-string or blank candidate, and the trim all live there.
1581
+ * 2. OPERATIONAL POLICY, which is this function's own and is NOT shared with
1582
+ * Python: an undeclared policy defaults to `main`, and the branch that will
1583
+ * actually be handed to git is validated before it is returned.
1584
+ *
1585
+ * The frozen dispositions (BAPI-1127) show up here as follows:
1586
+ *
1587
+ * - A NON-STRING candidate is absent, not fatal. `{base_branch: 42,
1588
+ * baseBranch: "epic/X"}` now resolves to `epic/X`; before BAPI-1127 it failed
1589
+ * closed, and `{base_branch: "", baseBranch: "epic/X"}` reported `main` — a
1590
+ * different real branch from the one the server provisioned.
1591
+ * - A MALFORMED declared name does NOT fall back to `main`. Silently
1592
+ * substituting the default for a bad declaration is how a wrong base becomes
1593
+ * invisible (the BAPI-586 defect), so it fails closed and dispatch refuses.
1594
+ * Only an ACTUAL absence of a declaration reaches the default.
1595
+ *
1596
+ * Validation happens here, once, ahead of both operational consumers (dispatch
1597
+ * and the done gate) rather than inside the shared resolver, so that Python and
1598
+ * TypeScript agree about what a policy declares while each keeps its own
1599
+ * judgment about what is safe to use. The failure carries only the rule that
1600
+ * failed — never the policy document or an unbounded branch value.
1567
1601
  */
1568
- export function resolveConfiguredRunBaseBranch(policyJson) {
1602
+ export function resolveEffectiveRunBaseBranch(policyJson) {
1569
1603
  const DEFAULT_BASE = "main";
1570
- if (!policyJson || typeof policyJson !== "object") {
1571
- return { ok: true, baseBranch: DEFAULT_BASE };
1572
- }
1573
- const raw = policyJson.base_branch ??
1574
- policyJson.baseBranch;
1575
- if (raw === undefined || raw === null) {
1576
- return { ok: true, baseBranch: DEFAULT_BASE };
1577
- }
1578
- if (typeof raw !== "string") {
1579
- return { ok: false, error: "epic run policy base_branch is present but is not a string." };
1580
- }
1581
- const trimmed = raw.trim();
1582
- if (trimmed.length === 0) {
1583
- return { ok: true, baseBranch: DEFAULT_BASE };
1584
- }
1585
- const validationError = validateBranchName(trimmed);
1604
+ const declaredBaseBranch = resolveDeclaredRunBaseBranch(policyJson);
1605
+ const effectiveBaseBranch = declaredBaseBranch ?? DEFAULT_BASE;
1606
+ const validationError = validateBranchName(effectiveBaseBranch);
1586
1607
  if (validationError) {
1587
1608
  return { ok: false, error: `epic run policy base_branch is invalid: ${validationError}` };
1588
1609
  }
1589
- return { ok: true, baseBranch: trimmed };
1610
+ return { ok: true, declaredBaseBranch, effectiveBaseBranch };
1590
1611
  }
1591
1612
  /**
1592
1613
  * Build the production EpicRuntimeDeps for use inside `runEpicTickCommand`.
@@ -1615,13 +1636,23 @@ export async function buildProductionEpicRuntimeDeps(epicKey) {
1615
1636
  // error (observe-only would already default to `main`); a malformed configured
1616
1637
  // base is captured as an error and re-raised at dispatch time so a bad base
1617
1638
  // never silently dispatches.
1618
- let cachedRunBase = { ok: true, baseBranch: "main" };
1639
+ //
1640
+ // BAPI-1127: the fetch-failure fallback declares NOTHING and dispatches from
1641
+ // `main`, which is what "we could not read the policy" honestly means — the
1642
+ // two values are kept separate so a later reader cannot mistake the default
1643
+ // for a declaration the run never made.
1644
+ const UNDECLARED_MAIN = {
1645
+ ok: true,
1646
+ declaredBaseBranch: undefined,
1647
+ effectiveBaseBranch: "main",
1648
+ };
1649
+ let cachedRunBase = UNDECLARED_MAIN;
1619
1650
  try {
1620
1651
  const runState = await fetchEpicRunState(access, epicKey);
1621
- cachedRunBase = resolveConfiguredRunBaseBranch(runState.epic_run.policy_json);
1652
+ cachedRunBase = resolveEffectiveRunBaseBranch(runState.epic_run.policy_json);
1622
1653
  }
1623
1654
  catch {
1624
- cachedRunBase = { ok: true, baseBranch: "main" };
1655
+ cachedRunBase = UNDECLARED_MAIN;
1625
1656
  }
1626
1657
  // Shared closure state populated by fetchPlan and consumed by dispatchSeam.
1627
1658
  let cachedPlanVersion = 0;
@@ -1687,12 +1718,17 @@ export async function buildProductionEpicRuntimeDeps(epicKey) {
1687
1718
  if (cachedPlanVersion === 0) {
1688
1719
  throw new Error(`dispatchSeam called before fetchPlan for epic ${ek} ticket ${tk}; cachedPlanVersion is 0`);
1689
1720
  }
1690
- // BAPI-586: fail CLOSED on a malformed configured run base rather than
1691
- // dispatching a worker (and opening a PR) against a bad base.
1721
+ // BAPI-586/BAPI-1127: fail CLOSED on a malformed configured run base rather
1722
+ // than dispatching a worker (and opening a PR) against a bad base. The check
1723
+ // is here — ahead of the dispatch claim, the spawn command, and any git or
1724
+ // provider work — because `resolveEffectiveRunBaseBranch` validates the ref
1725
+ // but this is the boundary that actually uses it. Only `effectiveBaseBranch`
1726
+ // (declaration, else `main`) is handed on; the declaration itself stays
1727
+ // separately readable on `cachedRunBase` and is never overwritten.
1692
1728
  if (!cachedRunBase.ok) {
1693
1729
  throw new Error(`invalid configured base branch for epic ${ek}: ${cachedRunBase.error}`);
1694
1730
  }
1695
- const runBaseBranch = cachedRunBase.baseBranch;
1731
+ const runBaseBranch = cachedRunBase.effectiveBaseBranch;
1696
1732
  // BAPI-441: a remediation re-dispatch (attempt > 0) reuses the existing
1697
1733
  // branch/worktree (resume mode) and claims an attempt-scoped dispatch key so
1698
1734
  // it is not deduped against the original epic dispatch.