@bridge_gpt/mcp-server 0.2.41 → 0.2.43

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 (88) hide show
  1. package/README.md +330 -191
  2. package/build/agent-capabilities/cli.js +2 -1
  3. package/build/agent-launchers/claude-executor-adapter.js +17 -4
  4. package/build/agents.generated.js +2 -2
  5. package/build/claude-review-workflow.js +510 -45
  6. package/build/claude-user-config-doctor.js +42 -11
  7. package/build/cli-release.js +2 -1
  8. package/build/commands.generated.js +6 -5
  9. package/build/conduct-epic/bridge-client.js +354 -113
  10. package/build/conduct-epic/checkpoint-store.js +17 -0
  11. package/build/conduct-epic/cli.js +947 -99
  12. package/build/conduct-epic/cut-protocol.js +327 -0
  13. package/build/conduct-epic/spawn.js +14 -2
  14. package/build/conductor/bridge-api-client.js +148 -1
  15. package/build/conductor/cli.js +109 -1
  16. package/build/conductor/doctor.js +101 -16
  17. package/build/conductor/epic-reconcile.js +72 -19
  18. package/build/conductor/epic-runtime.js +15 -3
  19. package/build/conductor/errors.js +47 -0
  20. package/build/conductor/git-hooks.js +205 -11
  21. package/build/conductor/install-doctor.js +230 -1
  22. package/build/conductor/local-merge.js +130 -28
  23. package/build/conductor/recovery-cli.js +313 -0
  24. package/build/conductor/recovery-operations.js +219 -0
  25. package/build/conductor/tools.js +32 -3
  26. package/build/conductor/worker-ledger-cli.js +27 -1
  27. package/build/conductor-bin.js +20 -16
  28. package/build/credentials-cli.js +3 -2
  29. package/build/docs.generated.js +2 -1
  30. package/build/doctor.js +120 -44
  31. package/build/drive-epic.js +375 -0
  32. package/build/executor/cli.js +48 -1
  33. package/build/executor/env.js +21 -0
  34. package/build/executor/http-client.js +71 -3
  35. package/build/executor/index-scope.js +39 -0
  36. package/build/executor/job-errors.js +9 -0
  37. package/build/executor/job-log-registry.js +69 -0
  38. package/build/executor/job-runner.js +198 -29
  39. package/build/executor/live-worker-registry.js +83 -0
  40. package/build/executor/observation.js +259 -6
  41. package/build/executor/platform.js +147 -3
  42. package/build/executor/process.js +58 -14
  43. package/build/executor/runner.js +454 -48
  44. package/build/executor/test-clock.js +3 -2
  45. package/build/executor/worker-finalization.js +233 -56
  46. package/build/executor/worktree.js +8 -1
  47. package/build/index-scope-contract.js +96 -0
  48. package/build/index.js +2277 -270
  49. package/build/init.js +83 -22
  50. package/build/install-bridge-conductor.js +323 -14
  51. package/build/install-bridge.js +225 -47
  52. package/build/install-doctor.js +23 -9
  53. package/build/install-reexec.js +2 -1
  54. package/build/launcher-config-inspection.js +83 -22
  55. package/build/mcp-host-config.js +331 -67
  56. package/build/mcp-host-targets.js +45 -21
  57. package/build/mcp-identity.js +92 -0
  58. package/build/mcp-install-state.js +94 -1
  59. package/build/mcp-invoke.js +2 -1
  60. package/build/mcp-provisioning.js +45 -12
  61. package/build/mcp-registration-doctor.js +35 -13
  62. package/build/mcp-server-invocation.js +4 -2
  63. package/build/merge-pull-request.js +208 -9
  64. package/build/pipelines.generated.js +305 -15
  65. package/build/plane/cli.js +73 -7
  66. package/build/plane/defaults.js +18 -5
  67. package/build/plane/manifest.js +90 -0
  68. package/build/plane/preflight.js +100 -10
  69. package/build/plane/shutdown.js +71 -3
  70. package/build/plane/test-fakes.js +9 -1
  71. package/build/readme.generated.js +1 -1
  72. package/build/regression-check.js +3 -2
  73. package/build/review-tickets.js +8 -7
  74. package/build/run-unit-tests-launcher.js +149 -6
  75. package/build/schedule-run.js +3 -2
  76. package/build/setup-epic.js +531 -82
  77. package/build/sfcc/tool-wrapper.js +15 -0
  78. package/build/start-tickets-prereqs.js +11 -6
  79. package/build/start-tickets.js +91 -85
  80. package/build/update-check.js +3 -2
  81. package/build/upgrade-advice.js +2 -1
  82. package/build/upgrade-cli.js +50 -18
  83. package/build/version.generated.js +2 -1
  84. package/build/worktree-core.js +31 -17
  85. package/docs/CONDUCTOR.md +22 -0
  86. package/docs/install/mcp-tool-integrations.md +19 -3
  87. package/package.json +2 -2
  88. package/pipelines/greenfield-setup.json +286 -0
@@ -0,0 +1,219 @@
1
+ /**
2
+ * Shared Bridge API operator-recovery operations (BAPI-872).
3
+ *
4
+ * Centralizes stop, abandon, and ticket unpark/adopt-current-head-and-unpark so
5
+ * the conductor CLI recovery verbs (`recovery-cli.ts`) and `plane down`
6
+ * (`../plane/shutdown.ts`, wired through `../plane/cli.ts`) share EXACTLY the
7
+ * same stop semantics, and so this module — never a caller — owns every
8
+ * ticket-recovery `row_version` read, retry, and idempotency-key decision.
9
+ *
10
+ * This module is a pure result-classification layer over the typed Bridge API
11
+ * client (`bridge-api-client.ts`). It prints nothing, never touches argv, and
12
+ * never renders operator-facing text — every result here is a discriminated
13
+ * union carrying only semantic outcomes and identifiers, and deliberately never
14
+ * a `row_version` or other CAS-internal value. Rendering belongs to the two
15
+ * callers: `recovery-cli.ts` and `plane/shutdown.ts`.
16
+ */
17
+ import { randomUUID } from "crypto";
18
+ import { adoptCurrentHeadAndUnparkTicket, ConductorBridgeApiError, fetchEpicRunState, safeDiagnosticMessage, stopEpicRun, unparkEpicTicket, updateEpicRunStatus, } from "./bridge-api-client.js";
19
+ /**
20
+ * Bounded total attempts for a ticket-recovery CAS retry loop. Each attempt
21
+ * begins with a fresh, authoritative state read — so this bounds full
22
+ * read-then-mutate cycles, not raw HTTP calls.
23
+ */
24
+ export const RECOVERY_TICKET_RETRY_LIMIT = 3;
25
+ /**
26
+ * Stop an epic run through the existing `stop` endpoint, classifying every
27
+ * outcome the endpoint can produce: a freshly committed stop, an idempotent
28
+ * repeat, a terminal refusal, or an unreachable/failed API.
29
+ *
30
+ * State is re-read ONLY on a terminal refusal, to report the run's actual
31
+ * `done`/`abandoned` status — never on the ordinary committed or idempotent
32
+ * paths, which need no second read.
33
+ */
34
+ export async function stopEpicRunRecovery(access, options) {
35
+ try {
36
+ const response = await stopEpicRun(access, {
37
+ epicRunId: options.epicRunId,
38
+ reason: options.reason,
39
+ });
40
+ return response.committed
41
+ ? { ok: true, kind: "committed", epicRunId: options.epicRunId }
42
+ : { ok: true, kind: "already-stopped", epicRunId: options.epicRunId };
43
+ }
44
+ catch (err) {
45
+ if (err instanceof ConductorBridgeApiError &&
46
+ err.status === 409 &&
47
+ err.errorCode === "RUN_TERMINAL") {
48
+ const status = await readTerminalRunStatus(access, options.epicRunId);
49
+ return { ok: false, kind: "terminal", epicRunId: options.epicRunId, status };
50
+ }
51
+ return {
52
+ ok: false,
53
+ kind: "unavailable",
54
+ epicRunId: options.epicRunId,
55
+ message: safeDiagnosticMessage(err, "stop request failed"),
56
+ };
57
+ }
58
+ }
59
+ /**
60
+ * Best-effort read of a run's actual status for a terminal-refusal report.
61
+ * Never throws: an unreadable state still renders SOME terminal report rather
62
+ * than losing the stop refusal itself to a secondary read failure.
63
+ */
64
+ async function readTerminalRunStatus(access, epicRunId) {
65
+ try {
66
+ const state = await fetchEpicRunState(access, epicRunId);
67
+ return state.epic_run.status;
68
+ }
69
+ catch {
70
+ return "terminal";
71
+ }
72
+ }
73
+ /**
74
+ * Abandon an epic run through the existing status-CAS PATCH lane
75
+ * (`updateEpicRunStatus`), reading the run's current status first so the PATCH
76
+ * always carries a freshly observed `expectedStatus` — never issued without
77
+ * one. An already-abandoned run is a safe no-op success; a lost CAS (the
78
+ * status moved concurrently) fails safely rather than retrying an unobserved
79
+ * transition or re-issuing an unguarded PATCH.
80
+ */
81
+ export async function abandonEpicRunRecovery(access, options) {
82
+ let state;
83
+ try {
84
+ state = await fetchEpicRunState(access, options.epicRunId);
85
+ }
86
+ catch (err) {
87
+ return {
88
+ ok: false,
89
+ kind: "unavailable",
90
+ epicRunId: options.epicRunId,
91
+ message: safeDiagnosticMessage(err, "could not read run state"),
92
+ };
93
+ }
94
+ const currentStatus = state.epic_run.status;
95
+ if (currentStatus === "abandoned") {
96
+ return { ok: true, kind: "already-abandoned", epicRunId: options.epicRunId };
97
+ }
98
+ try {
99
+ await updateEpicRunStatus(access, {
100
+ epicKey: options.epicRunId,
101
+ status: "abandoned",
102
+ expectedStatus: currentStatus,
103
+ });
104
+ return { ok: true, kind: "abandoned", epicRunId: options.epicRunId };
105
+ }
106
+ catch (err) {
107
+ if (err instanceof ConductorBridgeApiError && err.status === 400) {
108
+ // The status-CAS PATCH refused: the run's status moved concurrently since
109
+ // the read above. Never retried here — an unobserved transition must not
110
+ // be guessed at, and this is precisely why abandon reads state fresh on
111
+ // every invocation rather than caching or reusing a prior read.
112
+ return {
113
+ ok: false,
114
+ kind: "concurrent-change",
115
+ epicRunId: options.epicRunId,
116
+ message: "the run's status changed concurrently; re-check its state and retry",
117
+ };
118
+ }
119
+ return {
120
+ ok: false,
121
+ kind: "unavailable",
122
+ epicRunId: options.epicRunId,
123
+ message: safeDiagnosticMessage(err, "abandon request failed"),
124
+ };
125
+ }
126
+ }
127
+ /**
128
+ * The shared bounded-retry engine behind both ticket recovery operations.
129
+ *
130
+ * Every attempt — including every retry after a CAS conflict — begins with a
131
+ * fresh, authoritative `fetchEpicRunState` read. A conflict's own
132
+ * `current_row_version` is NEVER substituted for that reread: the reread is
133
+ * what proves the version is authoritative, not merely the latest one the
134
+ * conflict payload happened to report. Bounded to
135
+ * {@link RECOVERY_TICKET_RETRY_LIMIT} full read-then-mutate cycles; exhausting
136
+ * it is reported as `concurrent-change-exhausted`, never as an infinite loop.
137
+ *
138
+ * ONE idempotency key is generated per invocation and reused across every
139
+ * retry within it — a distinct recovery-operation call gets its own key.
140
+ */
141
+ async function ticketRecoveryWithRetry(access, options, mutate) {
142
+ const idempotencyKey = randomUUID();
143
+ for (let attempt = 0; attempt < RECOVERY_TICKET_RETRY_LIMIT; attempt += 1) {
144
+ let state;
145
+ try {
146
+ state = await fetchEpicRunState(access, options.epicRunId);
147
+ }
148
+ catch (err) {
149
+ return {
150
+ ok: false,
151
+ kind: "unavailable",
152
+ epicRunId: options.epicRunId,
153
+ ticketKey: options.ticketKey,
154
+ message: safeDiagnosticMessage(err, "could not read run state"),
155
+ };
156
+ }
157
+ const ticket = state.ticket_statuses.find((t) => t.ticket_key === options.ticketKey);
158
+ if (!ticket) {
159
+ return {
160
+ ok: false,
161
+ kind: "ticket-not-found",
162
+ epicRunId: options.epicRunId,
163
+ ticketKey: options.ticketKey,
164
+ };
165
+ }
166
+ let result;
167
+ try {
168
+ result = await mutate(access, {
169
+ epicRunId: options.epicRunId,
170
+ ticketKey: options.ticketKey,
171
+ expectedRowVersion: ticket.row_version,
172
+ idempotencyKey,
173
+ reason: options.reason,
174
+ });
175
+ }
176
+ catch (err) {
177
+ return {
178
+ ok: false,
179
+ kind: "unavailable",
180
+ epicRunId: options.epicRunId,
181
+ ticketKey: options.ticketKey,
182
+ message: safeDiagnosticMessage(err, "recovery request failed"),
183
+ };
184
+ }
185
+ if (result.ok) {
186
+ return {
187
+ ok: true,
188
+ kind: "unparked",
189
+ epicRunId: options.epicRunId,
190
+ ticketKey: options.ticketKey,
191
+ status: result.ticket_status.status,
192
+ };
193
+ }
194
+ // `result.kind === "cas-conflict"`: a concurrent write moved the ticket's
195
+ // row_version between our read and our mutate. Loop back to a fresh read —
196
+ // never reuse `result.current_row_version` here.
197
+ }
198
+ return {
199
+ ok: false,
200
+ kind: "concurrent-change-exhausted",
201
+ epicRunId: options.epicRunId,
202
+ ticketKey: options.ticketKey,
203
+ };
204
+ }
205
+ /**
206
+ * Unpark a parked `needs_human` ticket, with a bounded authoritative-reread
207
+ * retry on a concurrent `row_version` bump.
208
+ */
209
+ export async function unparkEpicTicketWithRetry(access, options) {
210
+ return ticketRecoveryWithRetry(access, options, (a, args) => unparkEpicTicket(a, args));
211
+ }
212
+ /**
213
+ * Adopt the current PR head and unpark a parked ticket, sharing the identical
214
+ * reread-and-retry discipline. The adopt-current-head client — never the
215
+ * ordinary unpark client — is called for every attempt.
216
+ */
217
+ export async function adoptCurrentHeadAndUnparkWithRetry(access, options) {
218
+ return ticketRecoveryWithRetry(access, options, (a, args) => adoptCurrentHeadAndUnparkTicket(a, args));
219
+ }
@@ -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);