@bridge_gpt/mcp-server 0.2.59 → 0.2.60

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 (71) hide show
  1. package/README.md +173 -51
  2. package/build/agent-guidance.generated.js +1 -1
  3. package/build/agent-notes.js +7 -1
  4. package/build/commands.generated.js +4 -4
  5. package/build/conductor/bridge-api-client.js +53 -2
  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 +61 -0
  11. package/build/conductor/merge-execution-eligibility.js +129 -0
  12. package/build/conductor/readiness-sections.js +159 -2
  13. package/build/conductor/readiness.js +95 -14
  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 +16 -0
  18. package/build/docs.generated.js +2 -2
  19. package/build/doctor.js +183 -9
  20. package/build/drive-epic.js +23 -0
  21. package/build/epic-implementer-bin.js +10 -10
  22. package/build/epic-implementer-claude-hook-bin.js +1 -1
  23. package/build/epic-implementer-env.js +43 -14
  24. package/build/executor/ci-rerun-job.js +269 -0
  25. package/build/executor/cli.js +31 -0
  26. package/build/executor/credentials.js +11 -0
  27. package/build/executor/env.js +60 -0
  28. package/build/executor/index.js +21 -4
  29. package/build/executor/job-runner.js +126 -0
  30. package/build/executor/job-types.js +12 -2
  31. package/build/executor/merge-job.js +11 -2
  32. package/build/executor/service-lifecycle.js +136 -0
  33. package/build/executor/service-unit.js +427 -9
  34. package/build/executor/types.js +5 -0
  35. package/build/index.js +194 -179
  36. package/build/install-bridge-conductor.js +201 -8
  37. package/build/install-bridge.js +356 -30
  38. package/build/mcp-install-state.js +42 -4
  39. package/build/merge-pull-request.js +103 -10
  40. package/build/merge-workflow-codeowners.js +224 -0
  41. package/build/merge-workflow-config.js +133 -0
  42. package/build/merge-workflow-drift.js +66 -0
  43. package/build/merge-workflow-install.js +638 -0
  44. package/build/merge-workflow.js +572 -0
  45. package/build/pipelines.generated.js +2 -2
  46. package/build/plane/cli.js +24 -2
  47. package/build/plane/preflight.js +68 -0
  48. package/build/readme.generated.js +1 -1
  49. package/build/run-unit-tests-launcher.js +5 -0
  50. package/build/setup-epic.js +163 -9
  51. package/build/version.generated.js +3 -3
  52. package/package.json +3 -3
  53. package/build/conduct-epic/bridge-client.js +0 -773
  54. package/build/conduct-epic/checkpoint-store.js +0 -542
  55. package/build/conduct-epic/cli.js +0 -3145
  56. package/build/conduct-epic/cut-protocol.js +0 -392
  57. package/build/conduct-epic/lock.js +0 -302
  58. package/build/conduct-epic/pr-state.js +0 -286
  59. package/build/conduct-epic/spawn.js +0 -113
  60. package/build/conductor/cli.js +0 -1278
  61. package/build/conductor-bundle-cli.js +0 -256
  62. package/build/direct-ticket-tools.js +0 -70
  63. package/build/executor/reconcile-overlap-governance.js +0 -129
  64. package/build/implement-epic/bridge-client.js +0 -773
  65. package/build/implement-epic/checkpoint-store.js +0 -542
  66. package/build/implement-epic/cli.js +0 -3158
  67. package/build/implement-epic/cut-protocol.js +0 -392
  68. package/build/implement-epic/lock.js +0 -302
  69. package/build/implement-epic/pr-state.js +0 -286
  70. package/build/implement-epic/spawn.js +0 -113
  71. package/build/plan-epic-conductor-eligibility.js +0 -213
@@ -54,6 +54,32 @@
54
54
  * Neither can produce `complete`: they provision LOCAL capability, and the
55
55
  * terminal outcome still turns only on the live server-observed executor claim.
56
56
  *
57
+ * DURABLE-HOST MODE (BAPI-1137). The executor phase below is the supported
58
+ * bring-up for a *durable host*: an execution home that is not an interactive
59
+ * shell session, so a sleeping or closed operator laptop no longer stalls a run.
60
+ * The client command spelling is unchanged — it is still `install conductor` —
61
+ * and so is every primitive it drives; what this slice does is name the
62
+ * composition and make its boundaries explicit. Durable-host mode IS that
63
+ * composition, and nothing more:
64
+ *
65
+ * planning (pure)
66
+ * → separately consented unit writing
67
+ * → separately consented start
68
+ * → read-only local inspection (service state, session/linger durability)
69
+ * → bounded server-observed readiness
70
+ *
71
+ * `executor install-service` remains the standalone, lifecycle-free generator
72
+ * for operators who deliberately manage lifecycle themselves.
73
+ *
74
+ * HOST OWNERSHIP is an operational selection, not server policy. A durable host
75
+ * may be operator-operated or Bridge-operated, chosen per repository, and both
76
+ * choices run IDENTICAL generation and lifecycle logic: `BAPI_BASE_URL` is the
77
+ * only ownership/location input, while platform paths, unit format, and `PATH`
78
+ * are machine-specific details rather than ownership signals. Nothing about that
79
+ * choice reaches `RunPolicy` or any other server policy payload. Bridge-operated
80
+ * mode is also NON-ALLOCATING: this command can configure a host that has
81
+ * already been assigned, and it never provisions a fleet or allocates a machine.
82
+ *
57
83
  * The executor phase (BAPI-779) is opt-in via `--executor-id` and runs behind
58
84
  * TWO independent consents, per property 3: writing the unit is not consent to
59
85
  * start a persistent daemon. Its shape is:
@@ -70,6 +96,13 @@
70
96
  * non-fatal: the run prints the exact platform-specific start command and
71
97
  * finishes at `complete-with-manual-steps`.
72
98
  *
99
+ * MERGE AUTHORITY IS UNTOUCHED (BAPI-1137). Provisioning a durable host gives
100
+ * the executor no merge-decision route and no direct GitHub write operation: the
101
+ * reconciler remains the sole transition authority and the actuator writes only
102
+ * evidence. Readiness output above must never be worded so as to imply
103
+ * otherwise — `executor_ready` means an executor is claiming work, not that this
104
+ * host can merge anything.
105
+ *
73
106
  * The command does NOT persist a repository-level review policy: that is
74
107
  * BAPI-694's. The policy selected here is per-run and is reported as such.
75
108
  */
@@ -81,6 +114,7 @@ import { MCP_PACKAGE_NAME } from "./mcp-identity.js";
81
114
  import { CLAUDE_REVIEW_WORKFLOW_RELPATH, renderClaudeReviewWorkflow, } from "./claude-review-workflow.js";
82
115
  import { formatExecutorInstallPreflight, } from "./executor/install-preflight.js";
83
116
  import { buildExecutorServiceStartPlan, formatExecutorServiceStartFailure, } from "./executor/service-lifecycle.js";
117
+ import { executorEpicRunIdRefusal, isValidExecutorEpicRunId, } from "./executor/claim-scope.js";
84
118
  import { planExecutorServiceInstall, } from "./executor/service-unit.js";
85
119
  // ---------------------------------------------------------------------------
86
120
  // Options + parsing
@@ -117,6 +151,12 @@ export function getInstallBridgeConductorUsage() {
117
151
  " --executor-id <id> Stable executor id. Supplying it OPTS IN to the",
118
152
  " consented executor-provisioning phase below; the",
119
153
  " phase is skipped entirely without it.",
154
+ " --epic-run-id <id> Claim scope for the generated unit (repeatable).",
155
+ " --repo-wide Claim-scope opt-out: claim from every authorized",
156
+ " repo, unscoped by run. EXACTLY ONE of --epic-run-id",
157
+ " or --repo-wide is required with --executor-id — the",
158
+ " executor refuses to start without one, so a unit",
159
+ " generated without it would fail on every boot.",
120
160
  " --allowed-bot <slug> claude-review workflow: allowed bot login",
121
161
  " --secret-name <NAME> claude-review workflow: OAuth token secret NAME",
122
162
  " --branches a,b claude-review workflow: PR branch triggers",
@@ -152,8 +192,17 @@ export function getInstallBridgeConductorUsage() {
152
192
  "hook consent; a degraded ledger reports the module, NODE_MODULE_VERSION, and",
153
193
  "failure kind, and never triggers an install, rebuild, or migration.",
154
194
  "",
155
- "Executor provisioning: `executor install-service` still generates a unit and",
156
- "nothing else, and remains available for operators who want the file only.",
195
+ "Durable-host mode: this command is the supported bring-up for a persistent,",
196
+ "laptop-independent executor. It composes planning, separately consented unit",
197
+ "writing, separately consented start, read-only local inspection, and bounded",
198
+ "server-observed readiness. `executor install-service` still generates a unit and",
199
+ "nothing else, and remains available for operators who manage lifecycle themselves.",
200
+ "",
201
+ "Host ownership (operator-operated or Bridge-operated) is an operational selection,",
202
+ "not server policy: both run identical generation and lifecycle logic, and",
203
+ "BAPI_BASE_URL is the only ownership/location input. Bridge-operated mode allocates",
204
+ "no host — it configures one already assigned. A repository with no assigned durable",
205
+ "host is NOT ready; escalate that to Bridge operations.",
157
206
  "",
158
207
  "With --executor-id, this command does the whole leg. A preflight checks the platform,",
159
208
  "`wt` / `git` / `claude` on PATH, and an explicit base URL, then reports whether",
@@ -200,6 +249,8 @@ export function parseInstallBridgeConductorArgs(argv) {
200
249
  let epicKey;
201
250
  let planFile;
202
251
  let executorId;
252
+ const epicRunIds = [];
253
+ let repoWide = false;
203
254
  let allowedBot;
204
255
  let secretName;
205
256
  let branches;
@@ -231,6 +282,11 @@ export function parseInstallBridgeConductorArgs(argv) {
231
282
  return { status: "error", message: "--skip-workflow takes no value" };
232
283
  skipWorkflow = true;
233
284
  break;
285
+ case "--repo-wide":
286
+ if (inline !== undefined)
287
+ return { status: "error", message: "--repo-wide takes no value" };
288
+ repoWide = true;
289
+ break;
234
290
  case "--repo":
235
291
  case "--required-check":
236
292
  case "--required-checks":
@@ -238,6 +294,7 @@ export function parseInstallBridgeConductorArgs(argv) {
238
294
  case "--epic-key":
239
295
  case "--plan-file":
240
296
  case "--executor-id":
297
+ case "--epic-run-id":
241
298
  case "--allowed-bot":
242
299
  case "--secret-name":
243
300
  case "--branches": {
@@ -259,6 +316,8 @@ export function parseInstallBridgeConductorArgs(argv) {
259
316
  planFile = value;
260
317
  else if (flag === "--executor-id")
261
318
  executorId = value;
319
+ else if (flag === "--epic-run-id")
320
+ epicRunIds.push(value);
262
321
  else if (flag === "--allowed-bot")
263
322
  allowedBot = value;
264
323
  else if (flag === "--secret-name")
@@ -304,6 +363,47 @@ export function parseInstallBridgeConductorArgs(argv) {
304
363
  "so the bootstrap refuses to write one.",
305
364
  };
306
365
  }
366
+ // BAPI-1137 — the claim scope is validated HERE, at parse time, for the same
367
+ // reason the required-check list is: a unit that cannot start is worse than a
368
+ // refused invocation, and discovering it costs a reboot to notice.
369
+ if (executorId !== undefined) {
370
+ if (epicRunIds.length > 0 && repoWide) {
371
+ return {
372
+ status: "error",
373
+ message: "--epic-run-id and --repo-wide are mutually exclusive: a generated unit carries exactly " +
374
+ "one claim scope.",
375
+ };
376
+ }
377
+ if (epicRunIds.length === 0 && !repoWide) {
378
+ return {
379
+ status: "error",
380
+ message: "--executor-id requires an explicit claim scope: pass --epic-run-id <id> (repeatable) or " +
381
+ "--repo-wide. The executor refuses to start without one, so a unit generated without it " +
382
+ "would install cleanly and then fail on every boot.",
383
+ };
384
+ }
385
+ const seenRunIds = new Set();
386
+ for (const runId of epicRunIds) {
387
+ // The SAME shape check `executor install-service` applies, reused rather
388
+ // than restated: a value that reaches the rendered argument vector is
389
+ // validated at the point of entry, so a malformed id is one refusal here
390
+ // instead of a unit that installs and then fails on every boot.
391
+ if (!isValidExecutorEpicRunId(runId)) {
392
+ return { status: "error", message: executorEpicRunIdRefusal(runId) };
393
+ }
394
+ if (seenRunIds.has(runId)) {
395
+ return { status: "error", message: `Duplicate --epic-run-id: ${runId}` };
396
+ }
397
+ seenRunIds.add(runId);
398
+ }
399
+ }
400
+ else if (epicRunIds.length > 0 || repoWide) {
401
+ return {
402
+ status: "error",
403
+ message: "--epic-run-id / --repo-wide configure the generated executor unit, so they require " +
404
+ "--executor-id.",
405
+ };
406
+ }
307
407
  return {
308
408
  status: "ok",
309
409
  options: {
@@ -319,6 +419,8 @@ export function parseInstallBridgeConductorArgs(argv) {
319
419
  branches,
320
420
  skipWorkflow,
321
421
  executorId,
422
+ ...(epicRunIds.length > 0 ? { epicRunIds } : {}),
423
+ ...(repoWide ? { repoWide: true } : {}),
322
424
  },
323
425
  };
324
426
  }
@@ -435,7 +537,12 @@ function buildExecutorProvisioningPlans(executor, executorId, baseUrl,
435
537
  * access rather than from a flag, so the unit can only ever serve the
436
538
  * repository this install actually authenticated for.
437
539
  */
438
- repos) {
540
+ repos,
541
+ /**
542
+ * The claim scope the unit starts with (BAPI-1137). Exactly one side is set,
543
+ * validated at parse time — a unit without a scope fails on every boot.
544
+ */
545
+ claimScope) {
439
546
  if (executor.platform !== "darwin" && executor.platform !== "linux") {
440
547
  return {
441
548
  ok: false,
@@ -455,6 +562,13 @@ repos) {
455
562
  baseUrl,
456
563
  homeDir: executor.homeDir,
457
564
  envPath: executor.envPath,
565
+ // Forwarded so the supervised process starts with the scope the
566
+ // operator chose. Without it the executor refuses to start (R54) and the
567
+ // service manager restarts it forever.
568
+ ...(claimScope.epicRunIds !== undefined && claimScope.epicRunIds.length > 0
569
+ ? { epicRunIds: claimScope.epicRunIds }
570
+ : {}),
571
+ ...(claimScope.repoWide === true ? { repoWide: true } : {}),
458
572
  }, platform);
459
573
  }
460
574
  catch (err) {
@@ -523,23 +637,102 @@ async function runExecutorProvisioningPhase(deps, executor, plans, access) {
523
637
  return null;
524
638
  }
525
639
  deps.log(" the service manager accepted the unit.");
640
+ // ---- Read-only local inspection (BAPI-1137) ---------------------------
641
+ // Reported as its own fact, deliberately BEFORE and separately from the server
642
+ // observation below. "The manager accepted the unit" and "the manager is
643
+ // running it" and "Bridge has seen it claim" are three different statements,
644
+ // and an operator who reads one as another is exactly how a dead durable host
645
+ // gets called healthy.
646
+ let localState = "unknown";
647
+ if (executor.inspectServiceState) {
648
+ try {
649
+ const inspection = await executor.inspectServiceState(plans.install);
650
+ localState = inspection.state;
651
+ }
652
+ catch {
653
+ // A probe that throws leaves the state unknown. Nothing here is allowed to
654
+ // upgrade "I could not ask" into a state.
655
+ localState = "unknown";
656
+ }
657
+ }
658
+ deps.log(` local service state (read-only): ${localState}`);
659
+ deps.log(" that is a LOCAL fact only — it says nothing about whether Bridge has observed a claim.");
660
+ if (executor.inspectDurability) {
661
+ let durability = null;
662
+ try {
663
+ durability = await executor.inspectDurability(plans.install);
664
+ }
665
+ catch {
666
+ durability = null;
667
+ }
668
+ if (durability === null || durability.prerequisite === "unverifiable") {
669
+ deps.log(" durable session: unverifiable — this run could not establish whether the executor " +
670
+ "survives logout.");
671
+ }
672
+ else if (durability.prerequisite === "configured") {
673
+ deps.log(" durable session: configured — the executor survives logout on this host.");
674
+ }
675
+ else if (durability.prerequisite === "session-bound") {
676
+ // The probe answered, and what it answered is "a login session exists" —
677
+ // not "this survives logout". Reported as the qualified fact it is, with
678
+ // the session prerequisite the operator has to keep true.
679
+ deps.log(" durable session: session-bound — a login session exists, but the executor is NOT " +
680
+ "logout-durable on this host.");
681
+ if (durability.remediation) {
682
+ deps.log(` manual action: ${durability.remediation}`);
683
+ }
684
+ }
685
+ else if (durability.prerequisite === "not-configured") {
686
+ // NOT durably configured, said plainly, with the command the OPERATOR runs.
687
+ // Bridge never runs it: enabling lingering changes how the host behaves
688
+ // after logout, and that is the operator's decision on their own machine.
689
+ deps.log(" durable session: NOT durably configured — the executor stops when you log out.");
690
+ if (durability.remediation) {
691
+ deps.log(` manual action: ${durability.remediation}`);
692
+ }
693
+ }
694
+ else {
695
+ // `not-applicable`. Unreachable in practice — this phase only runs on
696
+ // darwin and linux, both of which have a prerequisite — but stated rather
697
+ // than left as silence, because a fact that renders nothing is
698
+ // indistinguishable from a fact that was never collected.
699
+ deps.log(" durable session: not applicable on this platform.");
700
+ }
701
+ }
526
702
  // ---- Bounded observation ----------------------------------------------
527
703
  // Only reached after a SUCCESSFUL start: polling for an executor that was
528
704
  // never started would burn the whole budget to learn what we already know.
529
705
  deps.log(` watching the server for an executor claim (up to ${Math.round(EXECUTOR_OBSERVATION_BUDGET_MS / 1000)}s)…`);
530
706
  const observation = await observeExecutorReadiness(access, deps.fetchJson, executor);
531
707
  if (observation.status === "observed") {
532
- deps.log(" the server observed this executor.");
708
+ deps.log(` the server observed an executor claim for this repository (requested executor id: ${plans.install.executorId}).`);
709
+ // BAPI-1137, honestly scoped: the readiness contract this client can read is
710
+ // REPOSITORY-scoped — `GET .../conductor-readiness?repo_name=` returns
711
+ // `executor.{liveness_readable,last_seen_at,last_seen_age_seconds,ready}`
712
+ // and carries no executor id, because the server derives liveness from the
713
+ // repository's `executor_jobs` rows. So this observation proves an executor
714
+ // is claiming for this repository; it cannot prove it is THIS executor id.
715
+ // The honest move is to say so rather than to invent a request or response
716
+ // field the endpoint does not implement — see the durable-host runbook's
717
+ // prerequisite note for the server-side contract this needs.
718
+ deps.log(" note: the readiness contract is repository-scoped and carries no executor id, so this");
719
+ deps.log(" confirms an executor is claiming for this repository — not specifically the one just started.");
720
+ deps.log(" if another executor already serves this repository, verify the requested id yourself before");
721
+ deps.log(" treating this host as the one doing the work.");
533
722
  return observation.readiness;
534
723
  }
535
724
  // The honest degradation: name what WAS achieved and what was not, separately.
536
- deps.log(" the unit was written and the service manager started it,");
537
- deps.log(" but the server did not observe an executor claim within the budget " +
538
- `(${observation.attempts} read(s), ${observation.readFailures} unreadable).`);
725
+ deps.log(` started locally, not observed (executor id: ${plans.install.executorId}).`);
726
+ deps.log(` local service state (read-only): ${localState}`);
727
+ deps.log(" the unit was written and the service manager started it, but the server did not observe " +
728
+ `an executor claim within the budget (${observation.attempts} read(s), ` +
729
+ `${observation.readFailures} unreadable).`);
539
730
  deps.log(observation.lastReady === false
540
731
  ? " the server last reported this repository's executor as stale/not ready."
541
732
  : " the server has not yet seen any executor claim for this repository.");
542
733
  deps.log(" executor_ready is NOT claimed — only a live server observation can produce it.");
734
+ deps.log(" escalate to the host's operating party (operator-operated) or to Bridge operations " +
735
+ "(Bridge-operated) with the repository name and the executor id above.");
543
736
  return null;
544
737
  }
545
738
  // ---------------------------------------------------------------------------
@@ -847,7 +1040,7 @@ export async function runInstallBridgeConductorCli(argv, deps) {
847
1040
  // could not be resolved the doctor above has already produced a fatal
848
1041
  // section, so this fallback only matters for the narrow window where a
849
1042
  // caller injected a non-fatal doctor.
850
- const planned = buildExecutorProvisioningPlans(executor, options.executorId, baseUrl, access ? [access.repoName] : []);
1043
+ const planned = buildExecutorProvisioningPlans(executor, options.executorId, baseUrl, access ? [access.repoName] : [], { epicRunIds: options.epicRunIds, repoWide: options.repoWide });
851
1044
  if (!planned.ok) {
852
1045
  deps.errorLog("");
853
1046
  deps.errorLog(`Executor provisioning refused — ${planned.error}`);