@bridge_gpt/mcp-server 0.2.39 → 0.2.42

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 (74) hide show
  1. package/README.md +10 -10
  2. package/build/agent-capabilities/cli.js +2 -1
  3. package/build/agent-launchers/claude-executor-adapter.js +17 -4
  4. package/build/claude-user-config-doctor.js +42 -11
  5. package/build/cli-release.js +2 -1
  6. package/build/commands.generated.js +4 -4
  7. package/build/conduct-epic/bridge-client.js +354 -113
  8. package/build/conduct-epic/checkpoint-store.js +75 -2
  9. package/build/conduct-epic/cli.js +795 -109
  10. package/build/conduct-epic/cut-protocol.js +327 -0
  11. package/build/conduct-epic/pr-state.js +113 -24
  12. package/build/conduct-epic/spawn.js +14 -2
  13. package/build/conductor/bridge-api-client.js +27 -1
  14. package/build/conductor/cli.js +46 -1
  15. package/build/conductor/doctor.js +101 -16
  16. package/build/conductor/epic-reconcile.js +72 -19
  17. package/build/conductor/epic-runtime.js +15 -3
  18. package/build/conductor/errors.js +47 -0
  19. package/build/conductor/git-hooks.js +205 -11
  20. package/build/conductor/install-doctor.js +230 -1
  21. package/build/conductor/local-merge.js +130 -28
  22. package/build/conductor/tools.js +32 -3
  23. package/build/conductor/worker-ledger-cli.js +27 -1
  24. package/build/conductor-bin.js +15 -15
  25. package/build/credentials-cli.js +3 -2
  26. package/build/doctor.js +107 -41
  27. package/build/executor/cli.js +48 -1
  28. package/build/executor/env.js +21 -0
  29. package/build/executor/index-scope.js +39 -0
  30. package/build/executor/job-log-registry.js +69 -0
  31. package/build/executor/job-runner.js +148 -26
  32. package/build/executor/live-worker-registry.js +83 -0
  33. package/build/executor/observation.js +167 -6
  34. package/build/executor/platform.js +147 -3
  35. package/build/executor/process.js +58 -14
  36. package/build/executor/runner.js +235 -48
  37. package/build/executor/test-clock.js +3 -2
  38. package/build/index-scope-contract.js +96 -0
  39. package/build/index.js +153 -204
  40. package/build/init.js +83 -22
  41. package/build/install-bridge-conductor.js +323 -14
  42. package/build/install-bridge.js +202 -38
  43. package/build/install-doctor.js +23 -9
  44. package/build/install-reexec.js +2 -1
  45. package/build/launcher-config-inspection.js +83 -22
  46. package/build/mcp-host-config.js +331 -67
  47. package/build/mcp-host-targets.js +45 -21
  48. package/build/mcp-identity.js +92 -0
  49. package/build/mcp-install-state.js +94 -1
  50. package/build/mcp-invoke.js +2 -1
  51. package/build/mcp-provisioning.js +45 -12
  52. package/build/mcp-registration-doctor.js +35 -13
  53. package/build/mcp-server-invocation.js +4 -2
  54. package/build/merge-pull-request.js +208 -9
  55. package/build/pipelines.generated.js +3 -3
  56. package/build/plane/defaults.js +4 -1
  57. package/build/plane/preflight.js +81 -10
  58. package/build/plane/test-fakes.js +9 -1
  59. package/build/readme.generated.js +1 -1
  60. package/build/regression-check.js +3 -2
  61. package/build/review-tickets.js +8 -7
  62. package/build/run-unit-tests-launcher.js +74 -1
  63. package/build/schedule-run.js +3 -2
  64. package/build/setup-epic.js +453 -78
  65. package/build/sfcc/tool-wrapper.js +15 -0
  66. package/build/start-tickets-prereqs.js +11 -6
  67. package/build/start-tickets.js +91 -85
  68. package/build/update-check.js +3 -2
  69. package/build/upgrade-advice.js +2 -1
  70. package/build/upgrade-cli.js +50 -18
  71. package/build/version.generated.js +1 -1
  72. package/docs/CONDUCTOR.md +22 -0
  73. package/docs/install/mcp-tool-integrations.md +19 -3
  74. package/package.json +2 -2
@@ -485,38 +485,42 @@ export function makeLocalMergeExecutor(options = {}, deps = {}) {
485
485
  }
486
486
  // 3. Revalidate required CI green for the EXACT expected head, waiting briefly
487
487
  // for transient post-remediation CI to turn green before failing ci_not_green.
488
- const ciWait = await waitForRequiredChecksGreen(pollCi, access, expectedSha, requiredChecks, ciWaitTimeoutMs, ciWaitPollIntervalMs, sleep, now, signal);
489
- if (!ciWait.ok) {
490
- // Any `ci_poll_*` reason (the legacy `ci_poll_failed` plus the distinct
491
- // BAPI-577 kinds) is an AMBIGUOUS infrastructure read failure a merge that
492
- // GitHub already accepted could be masked by a flaky poll. Re-verify the
493
- // merged state before reporting. ci_not_green and merge_aborted are
494
- // deterministic (CI is red / the job was aborted); they never verify and
495
- // never proceed to a provider merge.
496
- if (isAmbiguousCiPollFailureReason(ciWait.reason)) {
488
+ // Skipped under `skipCiWait` (BAPI-832), where the server's approval tail
489
+ // revalidated these same checks for this same head moments ago.
490
+ if (!options.skipCiWait) {
491
+ const ciWait = await waitForRequiredChecksGreen(pollCi, access, expectedSha, requiredChecks, ciWaitTimeoutMs, ciWaitPollIntervalMs, sleep, now, signal);
492
+ if (!ciWait.ok) {
493
+ // Any `ci_poll_*` reason (the legacy `ci_poll_failed` plus the distinct
494
+ // BAPI-577 kinds) is an AMBIGUOUS infrastructure read failure a merge that
495
+ // GitHub already accepted could be masked by a flaky poll. Re-verify the
496
+ // merged state before reporting. ci_not_green and merge_aborted are
497
+ // deterministic (CI is red / the job was aborted); they never verify and
498
+ // never proceed to a provider merge.
499
+ if (isAmbiguousCiPollFailureReason(ciWait.reason)) {
500
+ const verified = await verifyMergedStateAfterAmbiguousFailure();
501
+ if (verified)
502
+ return verified;
503
+ }
504
+ return fail(ciWait.reason, ciWait.pollFailure);
505
+ }
506
+ // 3b. Re-read PR head/state after the CI wait — the head can drift, the PR can
507
+ // close, or the merge can already have landed while we waited for CI.
508
+ const secondRead = await readPrMergeState(run, ghEnv, pr);
509
+ if (!secondRead.ok) {
497
510
  const verified = await verifyMergedStateAfterAmbiguousFailure();
498
511
  if (verified)
499
512
  return verified;
513
+ return fail(secondRead.reason);
514
+ }
515
+ if (isMergedAtExpectedHead(secondRead.state, secondRead.headOid, expectedSha)) {
516
+ return buildAlreadyMergedResponse(request, baseDetails, secondRead.mergeCommitOid);
517
+ }
518
+ if (typeof secondRead.headOid !== "string" || secondRead.headOid.toLowerCase() !== expectedSha.toLowerCase()) {
519
+ return fail("head_drift");
520
+ }
521
+ if (typeof secondRead.state === "string" && secondRead.state.toUpperCase() !== "OPEN") {
522
+ return fail("pr_not_open");
500
523
  }
501
- return fail(ciWait.reason, ciWait.pollFailure);
502
- }
503
- // 3b. Re-read PR head/state after the CI wait — the head can drift, the PR can
504
- // close, or the merge can already have landed while we waited for CI.
505
- const secondRead = await readPrMergeState(run, ghEnv, pr);
506
- if (!secondRead.ok) {
507
- const verified = await verifyMergedStateAfterAmbiguousFailure();
508
- if (verified)
509
- return verified;
510
- return fail(secondRead.reason);
511
- }
512
- if (isMergedAtExpectedHead(secondRead.state, secondRead.headOid, expectedSha)) {
513
- return buildAlreadyMergedResponse(request, baseDetails, secondRead.mergeCommitOid);
514
- }
515
- if (typeof secondRead.headOid !== "string" || secondRead.headOid.toLowerCase() !== expectedSha.toLowerCase()) {
516
- return fail("head_drift");
517
- }
518
- if (typeof secondRead.state === "string" && secondRead.state.toUpperCase() !== "OPEN") {
519
- return fail("pr_not_open");
520
524
  }
521
525
  // 4. Provider merge. Final abort guard: if the overall timeout / ownership
522
526
  // abandonment fired during the CI wait or the post-CI re-read, do NOT fire
@@ -619,3 +623,101 @@ function buildConflictResponse(request, baseDetails, expectedSha, mergeability)
619
623
  { type: "merge.conflict", status: "failed", reason, details: conflictDetails },
620
624
  ]);
621
625
  }
626
+ /**
627
+ * Probe whether this host can merge with `gh` at all, BEFORE any merge is
628
+ * attempted.
629
+ *
630
+ * Without this the two operator-fixable failures are invisible: a missing `gh`
631
+ * and a logged-out `gh` both make `gh pr view` exit non-zero, which this module
632
+ * reports as `gh_pr_view_failed` — a reason that names no fix. Splitting them
633
+ * out is the difference between "run `gh auth login`" and "something went wrong
634
+ * reading the pull request".
635
+ *
636
+ * Deliberately async and built on the same `run` seam as the merge itself. The
637
+ * read-only equivalent in `conductor/doctor.ts` (`inspectLocalMerge`) probes
638
+ * synchronously, which is fine for a diagnostic command but would block the MCP
639
+ * server's event loop for up to its full timeout in the middle of a tool call.
640
+ * (This module's static contract forbids synchronous process execution outright,
641
+ * so the token itself cannot appear here — not even in a comment.)
642
+ */
643
+ export async function preflightLocalGh(run, ghEnv) {
644
+ let version;
645
+ try {
646
+ version = await run("gh", ["--version"], ghEnv);
647
+ }
648
+ catch {
649
+ return { ok: false, reason: "local_gh_unavailable" };
650
+ }
651
+ if (version.status !== 0)
652
+ return { ok: false, reason: "local_gh_unavailable" };
653
+ let auth;
654
+ try {
655
+ auth = await run("gh", ["auth", "status"], ghEnv);
656
+ }
657
+ catch {
658
+ return { ok: false, reason: "local_gh_unauthenticated" };
659
+ }
660
+ if (auth.status !== 0)
661
+ return { ok: false, reason: "local_gh_unauthenticated" };
662
+ return { ok: true };
663
+ }
664
+ /**
665
+ * Execute a merge the server has ALREADY approved, using the operator's `gh`.
666
+ *
667
+ * This is the local half of BAPI-832's split: the server ran the whole
668
+ * deterministic decision tail (action key, lease, head-SHA drift guard, review
669
+ * gate, workflow-file guard, CI revalidation) and returned
670
+ * `approved_for_local_execution` because its GitHub App installation is
671
+ * `contents: read` and cannot perform the write. Only the write happens here.
672
+ *
673
+ * It reuses {@link makeLocalMergeExecutor} rather than reimplementing the merge,
674
+ * so the head-drift guard, the `--match-head-commit` binding, and the entire
675
+ * ambiguity-resolution ladder are the same proven code v2's `merge` job runs.
676
+ * `skipCiWait` is set because the server revalidated CI moments ago.
677
+ *
678
+ * This function grants no authority of its own. It must only ever be called with
679
+ * a request built from a live approval for that exact PR and head SHA.
680
+ */
681
+ export async function runApprovedLocalMerge(request, options = {}, deps = {}) {
682
+ const ghEnv = {
683
+ ...deps.env,
684
+ GH_PROMPT_DISABLED: "1",
685
+ GH_NO_UPDATE_NOTIFIER: "1",
686
+ };
687
+ const rawRun = deps.runCommand ?? defaultRunCommand;
688
+ const run = (cmd, args, env) => Promise.resolve(rawRun(cmd, args, env, deps.signal));
689
+ const preflight = await preflightLocalGh(run, ghEnv);
690
+ if (!preflight.ok) {
691
+ return buildResponse(request, "failed", preflight.reason, false, [
692
+ {
693
+ type: "merge.failed",
694
+ status: "failed",
695
+ reason: preflight.reason,
696
+ details: {
697
+ action_key: request.action_key,
698
+ repo: request.repo_name,
699
+ pr_number: request.pr_number,
700
+ expected_head_sha: request.expected_head_sha,
701
+ executor: "local",
702
+ },
703
+ },
704
+ ]);
705
+ }
706
+ const executor = makeLocalMergeExecutor({ method: options.method, skipCiWait: true }, deps);
707
+ // `access` is only ever used by the CI poll, which `skipCiWait` bypasses. The
708
+ // throwing stub makes that structural, not a comment: if a future edit ever
709
+ // reaches the poll from this path, it fails loudly in tests instead of
710
+ // silently polling with an unusable access record.
711
+ const unusedAccess = {
712
+ get baseUrl() {
713
+ throw new Error("runApprovedLocalMerge must not poll CI");
714
+ },
715
+ get apiKey() {
716
+ throw new Error("runApprovedLocalMerge must not poll CI");
717
+ },
718
+ get repoName() {
719
+ throw new Error("runApprovedLocalMerge must not poll CI");
720
+ },
721
+ };
722
+ return executor(unusedAccess, request);
723
+ }
@@ -10,7 +10,7 @@
10
10
  */
11
11
  import { z } from "zod";
12
12
  import { SEMANTIC_EVENT_TYPES } from "./taxonomy.js";
13
- import { ConductorValidationError, toConductorErrorEnvelope } from "./errors.js";
13
+ import { ConductorValidationError, ConductorWorkerContextRequiredError, toConductorErrorEnvelope, } from "./errors.js";
14
14
  import { emitConductorEvent, pollConductorEvents, waitForConductorEvent, getSupervisorSnapshot, sendWorkerMessage, } from "./store.js";
15
15
  import { normalizePrNumber, normalizeSha } from "./git-ci-types.js";
16
16
  import { waitForDoneGate, resolveDispatchRunIdForBinding } from "./pr-ci-producer.js";
@@ -266,6 +266,15 @@ function registerWaitForDoneGateTool(registerTool) {
266
266
  // deterministic id are derived purely and the id is forwarded to the CLI,
267
267
  // so dedup happens server-side on the events.id UNIQUE constraint — the
268
268
  // worker path performs NO in-process ledger read or write.
269
+ //
270
+ // BAPI-772: this emit leg inherits the runtime contract from
271
+ // `resolveWorkerLedgerCliRuntime`, which is reached only when an emit is
272
+ // actually attempted — read-only gate work (binding resolution, CI
273
+ // polling) is never preemptively rejected. In a plain session with no
274
+ // worker env at all, that resolver raises WORKER_CONTEXT_REQUIRED, so the
275
+ // gate explains itself instead of surfacing an opaque 503; a partially
276
+ // configured or corrupted worker runtime still fails loud with
277
+ // LEDGER_SUBPROCESS_RUNTIME_UNAVAILABLE. Nothing falls back in-process.
269
278
  emitIfNew: (input, dimensions) => emitConductorEventIfNewViaCli(input, dimensions),
270
279
  });
271
280
  return jsonResult({
@@ -351,8 +360,28 @@ function registerCheckMessagesTool(registerTool) {
351
360
  limit: z.number().int().positive().max(100).optional().describe("Max messages to deliver/ack (default 10, max 100)."),
352
361
  },
353
362
  }, withConductorToolErrorHandling(async (args) => {
354
- const runId = args.run_id ?? process.env.BAPI_CONDUCTOR_RUN_ID ?? "";
355
- const workerId = args.worker_id ?? process.env.BAPI_CONDUCTOR_WORKER_ID ?? "";
363
+ const runIdArg = args.run_id;
364
+ const workerIdArg = args.worker_id;
365
+ const runIdEnv = process.env.BAPI_CONDUCTOR_RUN_ID;
366
+ const workerIdEnv = process.env.BAPI_CONDUCTOR_WORKER_ID;
367
+ // BAPI-772: a call with NEITHER argument supplied and NEITHER env var set is
368
+ // a plain MCP session — nothing is broken, the tool is simply worker-scoped.
369
+ // Return the typed guidance envelope instead of an opaque validation error.
370
+ // Any other shape (an explicit blank id, one id present and the other not, a
371
+ // partially-set worker env) is still malformed identity and keeps the
372
+ // existing fail-loud VALIDATION_ERROR: a broken worker context must never be
373
+ // relabeled as an ordinary session.
374
+ if (runIdArg === undefined &&
375
+ workerIdArg === undefined &&
376
+ runIdEnv === undefined &&
377
+ workerIdEnv === undefined) {
378
+ throw new ConductorWorkerContextRequiredError([
379
+ "BAPI_CONDUCTOR_RUN_ID",
380
+ "BAPI_CONDUCTOR_WORKER_ID",
381
+ ]);
382
+ }
383
+ const runId = runIdArg ?? runIdEnv ?? "";
384
+ const workerId = workerIdArg ?? workerIdEnv ?? "";
356
385
  if (runId.trim().length === 0 || workerId.trim().length === 0) {
357
386
  throw new ConductorValidationError("Conductor worker identity is unavailable: provide run_id + worker_id, or set BAPI_CONDUCTOR_RUN_ID and BAPI_CONDUCTOR_WORKER_ID.");
358
387
  }
@@ -16,6 +16,12 @@
16
16
  * to the worker's own `process.execPath`. Falling back would re-introduce the
17
17
  * native load into the worker Node — the exact failure mode this slice removes.
18
18
  *
19
+ * BAPI-772 adds ONE distinction on top of that contract, and it changes no
20
+ * fallback behavior: when BOTH worker variables are undefined there is no worker
21
+ * at all (a plain MCP session), which raises the typed
22
+ * {@link ConductorWorkerContextRequiredError} guidance envelope instead. Every
23
+ * partially-configured or invalid runtime keeps failing loud exactly as before.
24
+ *
19
25
  * Security: argv is always a fixed LIST (`execFile`, never `shell: true`, never a
20
26
  * command string). Event JSON payloads cross via STDIN (`--data-json-stdin`), so
21
27
  * raw payloads/secrets never enter the process argument list. Every failure is
@@ -24,7 +30,7 @@
24
30
  */
25
31
  import * as nodeChildProcess from "node:child_process";
26
32
  import { isAbsolute as pathIsAbsolute } from "node:path";
27
- import { ConductorLedgerSubprocessRuntimeError, } from "./errors.js";
33
+ import { ConductorLedgerSubprocessRuntimeError, ConductorWorkerContextRequiredError, } from "./errors.js";
28
34
  import { makeProducerDedupeKey, makeStableProducerEventId, } from "./producer-ledger.js";
29
35
  /** The env key that carries the captured conductor Node executable path. */
30
36
  export const CONDUCTOR_NODE_PATH_ENV = "CONDUCTOR_NODE_PATH";
@@ -60,10 +66,30 @@ function nonEmpty(value) {
60
66
  * non-empty. On any failure this throws a typed
61
67
  * {@link ConductorLedgerSubprocessRuntimeError}; it NEVER falls back to
62
68
  * `process.execPath`.
69
+ *
70
+ * BAPI-772 splits ABSENCE from BREAKAGE, and only at the top:
71
+ *
72
+ * - BOTH variables undefined → {@link ConductorWorkerContextRequiredError}.
73
+ * There is no worker runtime because there is no worker: this is a plain MCP
74
+ * session, and the caller deserves guidance, not a 503.
75
+ * - anything else — one variable present and the other not, an empty or
76
+ * whitespace-only value, a relative path, an unusable CLI file — keeps the
77
+ * existing fail-loud `LEDGER_SUBPROCESS_RUNTIME_UNAVAILABLE` mapping. A
78
+ * half-configured or corrupted worker runtime is a real fault and must never
79
+ * be relabeled as an ordinary plain session.
63
80
  */
64
81
  export function resolveWorkerLedgerCliRuntime(deps = {}) {
65
82
  const env = deps.env ?? process.env;
66
83
  const isAbsolute = deps.isAbsolute ?? pathIsAbsolute;
84
+ // `undefined` specifically — an env var SET to "" is present-but-invalid and
85
+ // falls through to the fail-loud path below.
86
+ if (env[CONDUCTOR_NODE_PATH_ENV] === undefined &&
87
+ env[BAPI_CONDUCTOR_CLI_FILE_ENV] === undefined) {
88
+ throw new ConductorWorkerContextRequiredError([
89
+ CONDUCTOR_NODE_PATH_ENV,
90
+ BAPI_CONDUCTOR_CLI_FILE_ENV,
91
+ ]);
92
+ }
67
93
  const nodePathRaw = env[CONDUCTOR_NODE_PATH_ENV];
68
94
  if (!nonEmpty(nodePathRaw)) {
69
95
  throw new ConductorLedgerSubprocessRuntimeError("missing", CONDUCTOR_NODE_PATH_ENV);