@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
@@ -19,6 +19,7 @@ import os from "node:os";
19
19
  import { runHeartbeatLoop } from "./heartbeat.js";
20
20
  import { isExecutorNamedError, toExecutorFailure, secretFreeErrorMessage, MissingVerdictArtifact, PreSpawnVerification, RequiredMcpRegistration, McpSurfaceMismatch, WorkerStartupFatal, WORKER_STARTUP_FATAL_MESSAGE, formatMcpSurfaceMismatch, ClaudeNotAuthenticated, CLAUDE_NOT_AUTHENTICATED_MESSAGE, ExecutorAdapterUnavailable, ExecutorNamedError, WorktreeBusy, StaleArtifactCleanupFailed, WORKTREE_BUSY_MESSAGE, WorkerEnvFilePresent, WORKER_ENV_FILE_PRESENT_MESSAGE, } from "./job-errors.js";
21
21
  import { buildConductorMergeAccessForExecutorJob, buildDefaultMergeLocalDeps, runExecutorMergeJob, MERGE_RETRYABLE, MERGE_FAILED, } from "./merge-job.js";
22
+ import { createProductionCiRerunDeps, runCiRerunJob, } from "./ci-rerun-job.js";
22
23
  import { createObservationState } from "./observation.js";
23
24
  import { getWorktreeMcpRegistrationTargets, normalizeWorktreePathForRegistration, provisionMcpRegistrationForWorktree, } from "../mcp-provisioning.js";
24
25
  import { resolveExecutorAgentAdapter } from "../agent-launchers/executor-adapter-registry.js";
@@ -615,6 +616,16 @@ control) {
615
616
  if (job.job_type === "smoke") {
616
617
  return runSmokeJob(job, httpClient, options, deps, ownership, observation, control);
617
618
  }
619
+ // BAPI-1160: `ci_rerun` is a deterministic executor action, dispatched here
620
+ // beside `merge`/`smoke` and BEFORE the generic spawn path. It ensures no
621
+ // worktree and spawns no worker, and it must NEVER fall back to one: spawning
622
+ // an LLM worker to "fix" a failure the server already proved to be
623
+ // infrastructure is exactly the behavior this job type replaces. `job-types.ts`
624
+ // keeps it out of both `SPAWN_JOB_TYPES` and `RECOVERY_JOB_TYPES`, so reaching
625
+ // the spawn path below is structurally impossible rather than merely unlikely.
626
+ if (job.job_type === "ci_rerun") {
627
+ return runCiRerunExecutorJob(job, httpClient, options, deps, ownership, observation, seams, control);
628
+ }
618
629
  // BAPI-1152: `reconcile_overlap` takes its OWN branch, after `merge`/`smoke`
619
630
  // and before the generic spawn path. It needs a prepared, locked worktree like
620
631
  // a spawn job, but its body is the reconcile module's state machine: the
@@ -652,6 +663,121 @@ control) {
652
663
  });
653
664
  return { status: "failed", reason: "unsupported_job_type" };
654
665
  }
666
+ /**
667
+ * Run a deterministic `ci_rerun` job (BAPI-1160).
668
+ *
669
+ * Supervised through the SAME ownership, heartbeat, dead-man, and timeout
670
+ * protocol as {@link runMergeJob} — via {@link createFlowProcess} — so a
671
+ * deterministic job is not a second, weaker lifecycle. The body itself spawns no
672
+ * subprocess of its own beyond the bounded `gh` invocations inside
673
+ * `ci-rerun-job.ts`, and there is NO fallback to a worker on failure.
674
+ *
675
+ * The completion carries only the job's fact envelope. It names no gate,
676
+ * requests no transition, and asserts no verdict: the reconciler re-observes the
677
+ * head independently and remains the sole transition authority (R14 rule 3).
678
+ */
679
+ async function runCiRerunExecutorJob(job, httpClient, options, deps, ownership, observation, seams, control) {
680
+ // Resolve + enforce payload.timeout_seconds BEFORE any provider work, exactly
681
+ // as the merge branch does.
682
+ const timeout = resolveJobTimeoutSeconds(job, options.defaultJobTimeoutSeconds);
683
+ if (!timeout.ok) {
684
+ await httpClient.fail(job, {
685
+ error_kind: "ContractError.Timeout",
686
+ error_message: timeout.error,
687
+ classification: "crashed",
688
+ });
689
+ return { status: "failed", reason: "timeout_contract" };
690
+ }
691
+ const runCiRerun = seams.runCiRerun ??
692
+ ((payload) => runCiRerunJob(payload, createProductionCiRerunDeps()));
693
+ const flow = createFlowProcess(() => runCiRerun(job.payload), (result) => result.ok);
694
+ const procResult = await superviseProcess({
695
+ job,
696
+ httpClient,
697
+ options,
698
+ deps,
699
+ ownership,
700
+ observation,
701
+ proc: flow.proc,
702
+ timeoutSeconds: timeout.timeoutSeconds,
703
+ collectTelemetry: async () => ({}),
704
+ control,
705
+ });
706
+ if (ownership.abandoned) {
707
+ return { status: "abandoned", reason: ownership.abandonReason };
708
+ }
709
+ if (procResult.classification === "timeout") {
710
+ const terminal = await sendTerminalMutationWithAuthReport({
711
+ kind: "fail",
712
+ send: () => httpClient.fail(job, {
713
+ error_kind: "CiRerunFailed",
714
+ error_message: "ci_rerun exceeded its job timeout",
715
+ classification: "timeout",
716
+ telemetry: observation.snapshot(),
717
+ }),
718
+ deps,
719
+ options,
720
+ ownership,
721
+ log: deps.log,
722
+ }, job, control);
723
+ return terminalToRunResult(terminal, "failed");
724
+ }
725
+ const outcome = flow.getOutcome();
726
+ if (outcome === undefined) {
727
+ const terminal = await sendTerminalMutationWithAuthReport({
728
+ kind: "fail",
729
+ send: () => httpClient.fail(job, {
730
+ error_kind: "CiRerunFailed",
731
+ error_message: "ci_rerun completed without an outcome",
732
+ classification: "crashed",
733
+ telemetry: observation.snapshot(),
734
+ }),
735
+ deps,
736
+ options,
737
+ ownership,
738
+ log: deps.log,
739
+ }, job, control);
740
+ return terminalToRunResult(terminal, "failed");
741
+ }
742
+ if (outcome.ok) {
743
+ const completion = {
744
+ job_type: "ci_rerun",
745
+ exit_code: 0,
746
+ classification: "clean_exit",
747
+ // The FACT ENVELOPE, verbatim. No gate, no transition, no verdict — the
748
+ // backend model is `extra="forbid"`, so anything else fails validation
749
+ // rather than being silently ignored.
750
+ result: outcome.result,
751
+ telemetry: observation.snapshot(),
752
+ };
753
+ const terminal = await sendTerminalMutationWithAuthReport({
754
+ kind: "complete",
755
+ send: () => httpClient.complete(job, completion),
756
+ deps,
757
+ options,
758
+ ownership,
759
+ log: deps.log,
760
+ }, job, control);
761
+ return terminalToRunResult(terminal, "completed");
762
+ }
763
+ // A refused or failed re-run. The reserved infrastructure attempt stays
764
+ // CONSUMED — see `ci-rerun-job.ts` — and the reason is a closed token, never
765
+ // provider text.
766
+ const terminal = await sendTerminalMutationWithAuthReport({
767
+ kind: "fail",
768
+ send: () => httpClient.fail(job, {
769
+ error_kind: outcome.failure.errorKind,
770
+ error_message: outcome.failure.reason,
771
+ classification: "crashed",
772
+ telemetry: observation.snapshot(),
773
+ }),
774
+ deps,
775
+ options,
776
+ ownership,
777
+ log: deps.log,
778
+ }, job, control);
779
+ return terminalToRunResult(terminal, "failed");
780
+ }
655
781
  /**
656
782
  * Adapt a `runMerge(...)` promise into an {@link OwnedChildProcess} so the merge
657
783
  * flow is supervised by the SAME heartbeat/deadman/timeout path as spawn/smoke
@@ -26,6 +26,15 @@
26
26
  * resolution, but it does NOT re-enter `/implement-ticket`: it always carries an
27
27
  * explicit server-rendered `payload.prompt`, which wins over the synthesis in
28
28
  * `worker-command.ts`, and `job-runner.ts` dispatches it to its own body.
29
+ *
30
+ * BAPI-1160: `ci_rerun` is DELIBERATELY ABSENT from this set and from
31
+ * `SPAWN_JOB_TYPES` below. It is a deterministic actuator — one bounded
32
+ * `gh run rerun --failed` and a bounded poll — with no worktree, no branch to
33
+ * reuse, no prompt, and no worker. It has a dedicated dispatch branch in
34
+ * `job-runner.ts` beside `merge` and `smoke`, ahead of the generic spawn path,
35
+ * and it must NEVER fall back to a worker subprocess: spawning an LLM worker to
36
+ * "fix" a failure the server already PROVED to be infrastructure is the exact
37
+ * behavior this job type exists to replace.
29
38
  */
30
39
  export const RECOVERY_JOB_TYPES = new Set([
31
40
  "remediate",
@@ -37,8 +46,9 @@ export const RECOVERY_JOB_TYPES = new Set([
37
46
  * Real headless-spawn job types (worktree + worker LLM): the first-pass
38
47
  * `implement`, the `resume` continuation (own pre-spawn protocol), the
39
48
  * `spec_review` verdict job, and the recovery jobs. `merge` (deterministic,
40
- * no worktree/worker) and `smoke` (no-op acceptance) are intentionally EXCLUDED —
41
- * they have dedicated dispatch branches BEFORE the generic spawn path.
49
+ * no worktree/worker), `smoke` (no-op acceptance), and BAPI-1160's `ci_rerun`
50
+ * (deterministic, no worktree/worker) are intentionally EXCLUDED — they have
51
+ * dedicated dispatch branches BEFORE the generic spawn path.
42
52
  *
43
53
  * BAPI-1152: `reconcile_overlap` is here because it needs a prepared, locked
44
54
  * worktree and MAY spawn a worker — but only CONDITIONALLY. A clean merge whose
@@ -21,7 +21,7 @@
21
21
  * {@link MERGE_FAILED} `error_kind` (via {@link classifyMergeFailureErrorKind})
22
22
  * so the companion re-fire lane can route retry-vs-rebase without string parsing.
23
23
  */
24
- import { makeLocalMergeExecutor, resolveLocalMergeMethod, } from "../conductor/local-merge.js";
24
+ import { makeLocalMergeExecutor, resolveLocalMergeCredential, resolveLocalMergeMethod, } from "../conductor/local-merge.js";
25
25
  import { resolveConductorBridgeApiAccess, } from "../conductor/bridge-api-client.js";
26
26
  import { createProductionEpicIntegrationGhRunner, ensureEpicIntegrationPullRequest, formatEpicIntegrationPullRequestOutcome, EPIC_BRANCH_PREFIX, } from "../epic-integration-pr.js";
27
27
  import { secretFreeErrorMessage } from "./job-errors.js";
@@ -353,5 +353,14 @@ export async function runExecutorMergeJob(job, seams) {
353
353
  * unchanged (no App token is ever placed here).
354
354
  */
355
355
  export function buildDefaultMergeLocalDeps(deps, signal) {
356
- return { env: deps.env, signal };
356
+ // BAPI-1136: the executor's merge job runs on the operator's own machine, which
357
+ // is typed `local`, so it reads the local configuration key and no other. The
358
+ // environment is still passed for the allowlisted operational base (PATH, HOME,
359
+ // certificates); it no longer supplies the identity, and an ambient GH_TOKEN in
360
+ // it is dropped rather than inherited.
361
+ return {
362
+ env: deps.env,
363
+ credential: resolveLocalMergeCredential(deps.env, "local"),
364
+ signal,
365
+ };
357
366
  }
@@ -36,6 +36,24 @@
36
36
  * The command forms address user-domain (per-user) services directly:
37
37
  * `launchctl` against `gui/<uid>/<label>` on darwin, and `systemctl --user`
38
38
  * on linux.
39
+ *
40
+ * FOUR INDEPENDENT FACTS (BAPI-1137). A durable host is described by four
41
+ * separate observations, and no one of them implies another:
42
+ *
43
+ * 1. **Unit-content compliance** — what the generated/on-disk unit DECLARES
44
+ * (`service-unit.ts`). A perfect unit file can be sitting unloaded.
45
+ * 2. **Service-manager state** — what the manager is running RIGHT NOW
46
+ * ({@link inspectExecutorServiceState}). The manager may still be running a
47
+ * previously loaded definition rather than the current bytes on disk.
48
+ * 3. **Session persistence** — whether the host keeps running it once nobody is
49
+ * logged in ({@link inspectExecutorServiceDurability}). An `active` service
50
+ * with lingering off is active exactly until the operator logs out.
51
+ * 4. **Server-observed readiness** — whether BRIDGE has seen an executor claim.
52
+ * Only this one can produce `executor_ready`, and rule 4 above is why:
53
+ * nothing local, however green, is evidence for it.
54
+ *
55
+ * They are reported separately everywhere, and collapsing any pair of them would
56
+ * reintroduce exactly the dishonest-green this module is built to refuse.
39
57
  */
40
58
  import { posixShellQuote } from "../platform-escaping.js";
41
59
  /**
@@ -62,6 +80,10 @@ function renderCommand(command) {
62
80
  /**
63
81
  * Build the platform-specific start sequence.
64
82
  *
83
+ * UNCHANGED by BAPI-1137: start remains separately consented, one-shot, and
84
+ * non-retrying. The durability inspection added lower in this file is read-only
85
+ * and has no path to this function or to {@link startExecutorService}.
86
+ *
65
87
  * darwin: `launchctl bootstrap gui/<uid> <plist-path>`
66
88
  * linux: `systemctl --user daemon-reload`, then
67
89
  * `systemctl --user enable --now <unit-name>`
@@ -267,3 +289,117 @@ export async function inspectExecutorServiceState(target, run) {
267
289
  return { state: "inactive", probe };
268
290
  return { state: "unknown", probe };
269
291
  }
292
+ /**
293
+ * The Linux remediation, verbatim and in one place so the lifecycle module, the
294
+ * installer, and doctor cannot print three subtly different commands.
295
+ */
296
+ export const EXECUTOR_LINGER_REMEDIATION = "run `loginctl enable-linger $USER` yourself to keep this user's services running " +
297
+ "without an active login session (Bridge never runs it for you)";
298
+ /**
299
+ * The macOS prerequisite. A LaunchAgent bootstrapped into `gui/<uid>` lives in a
300
+ * user-login domain, which `launchctl(1)` documents as created when the user logs
301
+ * in at the GUI — so it needs that login to exist, and after a reboot it needs
302
+ * the user to log in again (automatic login, or a human at the keyboard).
303
+ */
304
+ export const EXECUTOR_MACOS_SESSION_REMEDIATION = "keep this user logged in at the GUI — a launchd LaunchAgent runs in the gui/<uid> " +
305
+ "login domain, so after a reboot the user must log in again (e.g. enable automatic " +
306
+ "login) for the executor to come back";
307
+ /**
308
+ * Build the strictly read-only durability prerequisite probe for a target.
309
+ *
310
+ * linux: `loginctl show-user <uid> --property=Linger` — a QUERY. `show-user`
311
+ * reads properties; it cannot enable or disable anything, which is what makes it
312
+ * safe to run unprompted where `enable-linger` would not be.
313
+ * darwin: `launchctl print gui/<uid>` — the login domain itself, not a service
314
+ * inside it. A zero exit means the GUI login domain exists for this uid.
315
+ *
316
+ * PURE: consults no runner, no environment, and no filesystem, so an
317
+ * incompletely described target is refused before anything could be executed.
318
+ */
319
+ export function buildExecutorServiceDurabilityProbe(target) {
320
+ if (typeof target.uid !== "number" || !Number.isInteger(target.uid) || target.uid < 0) {
321
+ return null;
322
+ }
323
+ if (target.platform === "linux") {
324
+ return {
325
+ stage: "loginctl show-user --property=Linger",
326
+ executable: "loginctl",
327
+ args: ["show-user", String(target.uid), "--property=Linger"],
328
+ };
329
+ }
330
+ if (target.platform === "darwin") {
331
+ return {
332
+ stage: "launchctl print gui",
333
+ executable: "launchctl",
334
+ args: ["print", `gui/${target.uid}`],
335
+ };
336
+ }
337
+ return null;
338
+ }
339
+ /**
340
+ * Inspect the local session/linger durability prerequisite. STRICTLY read-only:
341
+ * it issues exactly one query and never enables lingering, starts a service,
342
+ * reloads a manager, or writes anything.
343
+ *
344
+ * Everything the runner returns beyond a recognized state token is DISCARDED —
345
+ * stdout past the token, stderr entirely, and any thrown value — because a
346
+ * service manager's output can name paths, units, and environment values that
347
+ * have no place in operator-facing text.
348
+ */
349
+ export async function inspectExecutorServiceDurability(target, run) {
350
+ if (target.platform !== "darwin" && target.platform !== "linux") {
351
+ return { prerequisite: "not-applicable", probe: null, remediation: null };
352
+ }
353
+ const probe = buildExecutorServiceDurabilityProbe(target);
354
+ if (!probe)
355
+ return { prerequisite: "unverifiable", probe: null, remediation: null };
356
+ let exitCode;
357
+ let stdout;
358
+ try {
359
+ const result = await run(probe.executable, probe.args);
360
+ exitCode = typeof result?.exitCode === "number" ? result.exitCode : 1;
361
+ stdout = typeof result?.stdout === "string" ? result.stdout : "";
362
+ }
363
+ catch {
364
+ return { prerequisite: "unverifiable", probe, remediation: null };
365
+ }
366
+ if (target.platform === "darwin") {
367
+ // The login domain either exists for this uid or it does not; launchctl
368
+ // exposes no third answer here, and a non-zero exit is reported as an
369
+ // unavailable session rather than dressed up as a confirmed absence of
370
+ // durability the probe did not establish.
371
+ //
372
+ // A ZERO exit is NOT `configured`. It says the gui/<uid> domain exists at
373
+ // this instant — which it does whenever an operator runs `install conductor`
374
+ // or `doctor` from their own logged-in session — and a LaunchAgent in that
375
+ // domain is tied to the login, not to the machine being powered on. Calling
376
+ // it `configured` would let a renderer that means "survives logout" assert
377
+ // the opposite of what launchd guarantees, so darwin gets its own state and
378
+ // carries the session prerequisite as the thing to keep true.
379
+ if (exitCode === 0) {
380
+ return {
381
+ prerequisite: "session-bound",
382
+ probe,
383
+ remediation: EXECUTOR_MACOS_SESSION_REMEDIATION,
384
+ };
385
+ }
386
+ return {
387
+ prerequisite: "not-configured",
388
+ probe,
389
+ remediation: EXECUTOR_MACOS_SESSION_REMEDIATION,
390
+ };
391
+ }
392
+ if (exitCode !== 0)
393
+ return { prerequisite: "unverifiable", probe, remediation: null };
394
+ // `loginctl show-user --property=Linger` prints exactly `Linger=yes|no`. Only
395
+ // those two tokens are mapped; anything else is unverifiable rather than a
396
+ // guess derived from a format this module does not recognize.
397
+ const match = stdout.match(/^\s*Linger=(\S+)\s*$/m);
398
+ const value = match?.[1]?.toLowerCase();
399
+ if (value === "yes")
400
+ return { prerequisite: "configured", probe, remediation: null };
401
+ if (value === "no") {
402
+ return { prerequisite: "not-configured", probe, remediation: EXECUTOR_LINGER_REMEDIATION };
403
+ }
404
+ return { prerequisite: "unverifiable", probe, remediation: null };
405
+ }