@bridge_gpt/mcp-server 0.2.45 → 0.2.48

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 (42) hide show
  1. package/README.md +103 -86
  2. package/build/agent-capabilities/default-deps.js +2 -2
  3. package/build/agent-launchers/claude.js +10 -19
  4. package/build/agent-launchers/cursor.js +4 -12
  5. package/build/agent-launchers/prompt.js +117 -0
  6. package/build/agents.generated.js +1 -1
  7. package/build/commands.generated.js +16 -22
  8. package/build/conduct-epic/bridge-client.js +73 -0
  9. package/build/conduct-epic/cli.js +152 -6
  10. package/build/conductor/cli.js +6 -7
  11. package/build/conductor/doctor.js +13 -116
  12. package/build/conductor/tools.js +18 -349
  13. package/build/conductor-bin.js +6 -30
  14. package/build/docs.generated.js +2 -2
  15. package/build/executor/deps.js +1 -0
  16. package/build/executor/service-lifecycle.js +6 -6
  17. package/build/executor/service-unit.js +13 -16
  18. package/build/index.js +253 -759
  19. package/build/init.js +15 -17
  20. package/build/install-doctor.js +1 -1
  21. package/build/learn-tool-gating.js +283 -0
  22. package/build/mcp-profile.js +13 -3
  23. package/build/mcp-server-invocation.js +14 -0
  24. package/build/pipelines.generated.js +20 -140
  25. package/build/platform-escaping.js +72 -0
  26. package/build/readme.generated.js +1 -1
  27. package/build/review-tickets.js +1 -1
  28. package/build/run-unit-tests-launcher.js +0 -1
  29. package/build/sfcc/register.js +41 -31
  30. package/build/sfcc/registration-inventory.js +44 -20
  31. package/build/start-tickets-conductor.js +2 -2
  32. package/build/start-tickets.js +8 -38
  33. package/build/tool-surface-gating.js +9 -3
  34. package/build/update-status.js +15 -0
  35. package/build/version.generated.js +2 -2
  36. package/docs/CONDUCTOR.md +10 -12
  37. package/docs/install/mcp-tool-integrations.md +9 -55
  38. package/package.json +2 -2
  39. package/pipelines/idea-to-ticket.json +2 -2
  40. package/pipelines/review-ticket.json +9 -8
  41. package/pipelines/check-ci-ticket.json +0 -36
  42. package/pipelines/pr-ticket.json +0 -24
@@ -1,41 +1,20 @@
1
1
  /**
2
2
  * Conductor MCP tool registration.
3
3
  *
4
- * Exposes the local SQLite ledger operations as strict, Zod-backed MCP tools
5
- * that follow the existing Bridge MCP return convention
6
- * (`{ content: [{ type: "text", text }] }`). All ledger access is LOCAL — these
7
- * tools never round-trip through the Bridge API HTTP helpers. Every handler is
4
+ * Registers the conductor's single MCP tool, `get_epic_snapshot`, which reads
5
+ * the server-side Epic Run snapshot through the Bridge API. Its handler is
8
6
  * wrapped by {@link withConductorToolErrorHandling} so failures surface as
9
7
  * sanitized, structured JSON without raw payloads, stack traces, or secrets.
8
+ *
9
+ * BAPI-909: the local SQLite ledger, mailbox, event, and done-gate MCP tools
10
+ * (`emit_event`, `poll_events`, `wait_for_event`, `get_supervisor_snapshot`,
11
+ * `wait_for_done_gate`, `send_message`, `check_messages`) were removed. The
12
+ * conductor CLI and git hooks remain the ledger's real readers and writers —
13
+ * those paths are untouched and do not go through MCP.
10
14
  */
11
15
  import { z } from "zod";
12
- import { SEMANTIC_EVENT_TYPES } from "./taxonomy.js";
13
- import { ConductorValidationError, ConductorWorkerContextRequiredError, toConductorErrorEnvelope, } from "./errors.js";
14
- import { emitConductorEvent, pollConductorEvents, waitForConductorEvent, getSupervisorSnapshot, sendWorkerMessage, } from "./store.js";
15
- import { normalizePrNumber, normalizeSha } from "./git-ci-types.js";
16
- import { waitForDoneGate, resolveDispatchRunIdForBinding } from "./pr-ci-producer.js";
17
- // BAPI-527: worker-facing ledger operations run through the conductor CLI
18
- // subprocess (under the captured conductor Node), NOT the in-process SQLite store,
19
- // so the worker Node never loads the `better-sqlite3` native binary. `store.ts`'s
20
- // `checkWorkerMessages` is intentionally NOT imported here anymore.
21
- import { checkWorkerMessagesViaCli, emitConductorEventIfNewViaCli } from "./worker-ledger-cli.js";
16
+ import { ConductorValidationError, toConductorErrorEnvelope } from "./errors.js";
22
17
  import { resolveConductorBridgeApiAccess, fetchEpicRunState, ConductorBridgeApiError } from "./bridge-api-client.js";
23
- /** Build a Zod enum from the semantic taxonomy so arbitrary types are rejected up front. */
24
- export function buildEventTypeZodEnum() {
25
- return z.enum(SEMANTIC_EVENT_TYPES);
26
- }
27
- /** Allowlisted, parameterized read filter schema (shared by poll + wait). */
28
- const EventFilterSchema = z
29
- .object({
30
- type: buildEventTypeZodEnum().optional(),
31
- types: z.array(buildEventTypeZodEnum()).optional(),
32
- source: z.string().optional(),
33
- run_id: z.string().optional(),
34
- worker_id: z.string().optional(),
35
- subject: z.string().optional(),
36
- producer: z.string().optional(),
37
- })
38
- .strict();
39
18
  function jsonResult(value) {
40
19
  return { content: [{ type: "text", text: JSON.stringify(value, null, 2) }] };
41
20
  }
@@ -57,128 +36,6 @@ export function withConductorToolErrorHandling(handler) {
57
36
  }
58
37
  };
59
38
  }
60
- function registerEmitEventTool(registerTool) {
61
- registerTool("emit_event", {
62
- annotations: {
63
- readOnlyHint: false,
64
- destructiveHint: false,
65
- idempotentHint: false,
66
- openWorldHint: false,
67
- },
68
- description: "Append a semantic coordination event to the LOCAL conductor ledger (~/.config/bridge/events.db). " +
69
- "This is a local, append-only event store for multi-agent coordination — it does NOT call the Bridge API. " +
70
- "Only the fixed semantic event taxonomy is accepted (e.g. run.started, agent.notification, ci.passed). " +
71
- "Place tool-native fields (branch, commitSha, etc.) under data.raw — non-allowlisted top-level data keys are rejected. " +
72
- "Secrets are redacted before storage and large payloads must be passed by reference (data.payload_ref / data.references).",
73
- inputSchema: {
74
- source: z.string().describe("Logical producer of the event (e.g. 'claude-code', 'git-hook')."),
75
- type: buildEventTypeZodEnum().describe("Semantic event type from the fixed conductor taxonomy."),
76
- subject: z.string().optional().describe("Optional subject the event is about (e.g. a ticket key)."),
77
- run_id: z.string().optional().describe("Optional run/session identifier this event belongs to."),
78
- worker_id: z.string().optional().describe("Optional worker/agent identifier."),
79
- producer: z.string().optional().describe("Optional finer-grained producer identity."),
80
- schema_version: z.number().int().positive().optional().describe("Event schema version (default 1)."),
81
- time: z.string().optional().describe("Optional ISO-8601 event time (defaults to now)."),
82
- data: z
83
- .record(z.string(), z.unknown())
84
- .optional()
85
- .describe("Normalized event data. Allowed top-level keys: summary, status, message, details, reason, metrics, labels, references, payload_ref, raw. Tool-native fields go under 'raw'."),
86
- confidence: z.number().min(0).max(1).optional().describe("Optional confidence in [0,1]."),
87
- observed_via: z.string().optional().describe("Optional channel the event was observed through."),
88
- },
89
- }, withConductorToolErrorHandling(async (args) => {
90
- const result = await emitConductorEvent({
91
- source: args.source,
92
- type: args.type,
93
- subject: args.subject,
94
- run_id: args.run_id,
95
- worker_id: args.worker_id,
96
- producer: args.producer,
97
- schema_version: args.schema_version,
98
- time: args.time,
99
- data: args.data ?? {},
100
- confidence: args.confidence,
101
- observed_via: args.observed_via,
102
- });
103
- return jsonResult(result);
104
- }));
105
- }
106
- function registerPollEventsTool(registerTool) {
107
- registerTool("poll_events", {
108
- annotations: {
109
- readOnlyHint: true,
110
- destructiveHint: false,
111
- idempotentHint: true,
112
- openWorldHint: false,
113
- },
114
- description: "Read ordered events from the LOCAL conductor ledger starting at an inclusive 'since_seq' cursor. " +
115
- "Returns compact metadata-first summaries by default (data.raw omitted; raw_keys surfaced) and a 'next_seq' cursor to pass on the next call. " +
116
- "Set data_mode='full' to retrieve complete (redacted) event data. Local read-only; does not call the Bridge API.",
117
- inputSchema: {
118
- since_seq: z.number().int().nonnegative().optional().describe("Inclusive sequence cursor (default 1)."),
119
- filter: EventFilterSchema.optional().describe("Optional allowlisted filter."),
120
- data_mode: z.enum(["summary", "full"]).optional().describe("Projection mode (default 'summary')."),
121
- limit: z.number().int().positive().optional().describe("Max events to return (default 100, max 1000)."),
122
- },
123
- }, withConductorToolErrorHandling(async (args) => {
124
- const result = await pollConductorEvents({
125
- since_seq: args.since_seq ?? 1,
126
- filter: args.filter,
127
- data_mode: args.data_mode ?? "summary",
128
- limit: args.limit,
129
- });
130
- return jsonResult(result);
131
- }));
132
- }
133
- function registerWaitForEventTool(registerTool) {
134
- registerTool("wait_for_event", {
135
- annotations: {
136
- readOnlyHint: true,
137
- destructiveHint: false,
138
- idempotentHint: true,
139
- openWorldHint: false,
140
- },
141
- description: "Long-poll the LOCAL conductor ledger: block up to 'timeout_ms' (bounded, max 120000) until events matching the filter appear at/after 'since_seq'. " +
142
- "Returns the same shape as poll_events plus 'timed_out'. SQLite locks are never held between polls. Local read-only; does not call the Bridge API.",
143
- inputSchema: {
144
- since_seq: z.number().int().nonnegative().optional().describe("Inclusive sequence cursor (default 1)."),
145
- filter: EventFilterSchema.optional().describe("Optional allowlisted filter."),
146
- data_mode: z.enum(["summary", "full"]).optional().describe("Projection mode (default 'summary')."),
147
- timeout_ms: z.number().int().nonnegative().optional().describe("Max wait in ms (bounded, max 120000)."),
148
- limit: z.number().int().positive().optional().describe("Max events to return (default 100, max 1000)."),
149
- },
150
- }, withConductorToolErrorHandling(async (args) => {
151
- const result = await waitForConductorEvent({
152
- since_seq: args.since_seq ?? 1,
153
- filter: args.filter,
154
- data_mode: args.data_mode ?? "summary",
155
- timeout_ms: args.timeout_ms,
156
- limit: args.limit,
157
- });
158
- return jsonResult(result);
159
- }));
160
- }
161
- function registerGetSupervisorSnapshotTool(registerTool) {
162
- registerTool("get_supervisor_snapshot", {
163
- annotations: {
164
- readOnlyHint: true,
165
- destructiveHint: false,
166
- idempotentHint: true,
167
- openWorldHint: false,
168
- },
169
- description: "Read the supervisor projection for a run_id from the LOCAL conductor ledger. " +
170
- "The projection is maintained by the conductor supervisor runtime (`conductor supervise --run-id <id>`), " +
171
- "which owns the deterministic worker watchdog state; this tool ONLY reads that projection and never derives state from raw events. " +
172
- "Returns { run_id, status, projection } where projection is null and status is 'unknown' when no projection exists yet. " +
173
- "Local read-only; does not call the Bridge API.",
174
- inputSchema: {
175
- run_id: z.string().describe("The run/session identifier to read the supervisor projection for."),
176
- },
177
- }, withConductorToolErrorHandling(async (args) => {
178
- const result = await getSupervisorSnapshot(args.run_id);
179
- return jsonResult(result);
180
- }));
181
- }
182
39
  function registerGetEpicSnapshotTool(registerTool) {
183
40
  registerTool("get_epic_snapshot", {
184
41
  annotations: {
@@ -213,206 +70,18 @@ function registerGetEpicSnapshotTool(registerTool) {
213
70
  }
214
71
  }));
215
72
  }
216
- const SHA_PATTERN = /^[0-9a-fA-F]{40}$|^[0-9a-fA-F]{64}$/;
217
- function registerWaitForDoneGateTool(registerTool) {
218
- registerTool("wait_for_done_gate", {
219
- annotations: {
220
- // Read/write + non-idempotent: this tool can EMIT conductor events
221
- // (git.pr_opened, ci.passed/failed, gate.met) as a side effect of observing.
222
- readOnlyHint: false,
223
- destructiveHint: false,
224
- idempotentHint: false,
225
- openWorldHint: true,
226
- },
227
- description: "Bounded wait for the conductor done-gate on a pull request. Resolves the PR number + immutable head SHA " +
228
- "once, polls CI for that SHA on a clamped interval, and emits conductor events (git.pr_opened, ci.passed/ci.failed, " +
229
- "and gate.met when the configured required CI checks are green). " +
230
- "This EMITS conductor coordination events only — it does NOT merge the PR, transition Jira, or mutate any repository state. " +
231
- "Fails closed: an unset/disabled/malformed conductor_done_gate config never produces gate.met.",
232
- inputSchema: {
233
- repo_name: z.string().optional().describe("Optional repo name override (defaults to BAPI_REPO_NAME/.bridge/config)."),
234
- pr_number: z.number().int().positive().optional().describe("Optional explicit PR number (positive integer)."),
235
- head_sha: z
236
- .string()
237
- .regex(SHA_PATTERN)
238
- .optional()
239
- .describe("Optional explicit head SHA (40- or 64-character hex)."),
240
- timeout_ms: z.number().int().nonnegative().optional().describe("Max wait in ms (clamped, max 120000)."),
241
- poll_interval_ms: z.number().int().nonnegative().optional().describe("CI poll interval in ms (clamped)."),
242
- worktree_path: z.string().optional().describe("Optional worktree path to resolve git/PR context from."),
243
- },
244
- }, withConductorToolErrorHandling(async (args) => {
245
- // Defensive boundary validation (mirrors the schema) so an invalid identifier
246
- // is a sanitized 400 and never reaches waitForDoneGate.
247
- if (args.pr_number !== undefined && normalizePrNumber(args.pr_number) === null) {
248
- throw new ConductorValidationError("'pr_number' must be a positive integer.");
249
- }
250
- if (args.head_sha !== undefined && normalizeSha(args.head_sha) === null) {
251
- throw new ConductorValidationError("'head_sha' must be a 40- or 64-character hex SHA.");
252
- }
253
- const result = await waitForDoneGate({
254
- repoName: args.repo_name,
255
- prNumber: args.pr_number,
256
- headSha: args.head_sha,
257
- timeoutMs: args.timeout_ms,
258
- pollIntervalMs: args.poll_interval_ms,
259
- worktreePath: args.worktree_path,
260
- }, {
261
- resolveRunId: resolveDispatchRunIdForBinding,
262
- // BAPI-527: route the gate's ENTIRE dedup+emit through the conductor CLI
263
- // subprocess. Injecting `emitIfNew` (not just the write sink) replaces the
264
- // in-process `emitConductorEventIfNew`, whose `eventAlreadyExists` poll
265
- // pre-check would load better-sqlite3 in the worker Node. The dedupe key +
266
- // deterministic id are derived purely and the id is forwarded to the CLI,
267
- // so dedup happens server-side on the events.id UNIQUE constraint — the
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.
278
- emitIfNew: (input, dimensions) => emitConductorEventIfNewViaCli(input, dimensions),
279
- });
280
- return jsonResult({
281
- gate_met: result.gate_met,
282
- timed_out: result.timed_out,
283
- reason: result.reason,
284
- repo: result.repo,
285
- pr_number: result.pr_number,
286
- head_sha: result.head_sha,
287
- gate_event_summary: result.gate_event_summary,
288
- });
289
- }));
290
- }
291
- function registerSendMessageTool(registerTool) {
292
- registerTool("send_message", {
293
- annotations: {
294
- readOnlyHint: false,
295
- destructiveHint: false,
296
- // Enqueueing the same idempotency key (run_id+worker_id+type+cause_seq)
297
- // never inserts a second message, so the tool is idempotent.
298
- idempotentHint: true,
299
- openWorldHint: false,
300
- },
301
- description: "Enqueue a typed, auditable message for ONE worker through the LOCAL cooperative conductor relay " +
302
- "(~/.config/bridge/events.db). The supervisor sends; the worker reads/acknowledges later via check_messages. " +
303
- "This is COOPERATIVE — it does NOT inject into, mutate, or prompt-inject a live worker session. " +
304
- "Idempotent: a duplicate idempotency key (run_id+worker_id+type+cause_seq) does not enqueue a second message, " +
305
- "and a same-type message inside the cooldown window is suppressed. Local only; does not call the Bridge API.",
306
- inputSchema: {
307
- run_id: z.string().min(1).describe("Run/session identifier the message is scoped to."),
308
- worker_id: z.string().min(1).describe("Target worker/agent identifier."),
309
- type: z.string().min(1).describe("Typed message kind (e.g. 'supervisor.worker_stalled')."),
310
- cause_seq: z
311
- .number()
312
- .int()
313
- .nonnegative()
314
- .describe("Idempotency cause sequence (the supervisor's last_seq at decision time)."),
315
- payload: z
316
- .record(z.string(), z.unknown())
317
- .optional()
318
- .default({})
319
- .describe("Optional compact payload. Allowed top-level keys: summary, status, message, details, reason, metrics, labels, references, payload_ref, raw."),
320
- available_at: z.string().optional().describe("Optional ISO-8601 time the message becomes available (default now)."),
321
- cooldown_ms: z
322
- .number()
323
- .int()
324
- .nonnegative()
325
- .optional()
326
- .describe("Optional per-call cooldown override in ms (falls back to the configured cooldown)."),
327
- },
328
- }, withConductorToolErrorHandling(async (args) => {
329
- const result = await sendWorkerMessage({
330
- run_id: args.run_id,
331
- worker_id: args.worker_id,
332
- type: args.type,
333
- cause_seq: args.cause_seq,
334
- payload: args.payload ?? {},
335
- available_at: args.available_at,
336
- cooldown_ms: args.cooldown_ms,
337
- });
338
- return jsonResult(result);
339
- }));
340
- }
341
- function registerCheckMessagesTool(registerTool) {
342
- registerTool("check_messages", {
343
- annotations: {
344
- // Reads AND acknowledges (mutates pending -> acked), so not read-only and
345
- // not idempotent: the first call returns+acks pending messages, later
346
- // calls return none.
347
- readOnlyHint: false,
348
- destructiveHint: false,
349
- idempotentHint: false,
350
- openWorldHint: false,
351
- },
352
- description: "Worker checkpoint poll for the LOCAL cooperative conductor relay. Call this at natural checkpoints to read " +
353
- "any supervisor messages addressed to this worker. Returned messages are ACKNOWLEDGED by this call and are " +
354
- "NOT redelivered on later polls. This is cooperative polling — it is NOT live prompt injection. " +
355
- "run_id/worker_id default to BAPI_CONDUCTOR_RUN_ID / BAPI_CONDUCTOR_WORKER_ID from the environment when omitted. " +
356
- "Local only; does not call the Bridge API.",
357
- inputSchema: {
358
- run_id: z.string().optional().describe("Run identifier (defaults to BAPI_CONDUCTOR_RUN_ID)."),
359
- worker_id: z.string().optional().describe("Worker identifier (defaults to BAPI_CONDUCTOR_WORKER_ID)."),
360
- limit: z.number().int().positive().max(100).optional().describe("Max messages to deliver/ack (default 10, max 100)."),
361
- },
362
- }, withConductorToolErrorHandling(async (args) => {
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 ?? "";
385
- if (runId.trim().length === 0 || workerId.trim().length === 0) {
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.");
387
- }
388
- // BAPI-527: read+ack through the conductor CLI subprocess (captured
389
- // conductor Node), NOT the in-process store, so the worker Node never loads
390
- // better-sqlite3. A missing/invalid CONDUCTOR_NODE_PATH surfaces as the
391
- // typed LEDGER_SUBPROCESS_RUNTIME_UNAVAILABLE envelope via the wrapper below.
392
- const result = await checkWorkerMessagesViaCli({
393
- runId,
394
- workerId,
395
- limit: args.limit,
396
- });
397
- return jsonResult(result);
398
- }));
399
- }
400
73
  /**
401
- * Register all conductor MCP tools through the host's `registerTool` wrapper.
402
- * Keeping registration in this single module keeps `index.ts` thin. The host's
403
- * `registerTool` is the SDK's heavily-generic signature, so it is accepted as
404
- * `unknown` here and narrowed once to {@link RegisterToolFn} — our handlers
405
- * return the same `{ content: [{ type: "text", text }] }` shape every Bridge
406
- * tool uses, just expressed with a simpler local type.
74
+ * Register the conductor MCP tool surface through the host's `registerTool`
75
+ * wrapper. Keeping registration in this single module keeps `index.ts` thin.
76
+ * The host's `registerTool` is the SDK's heavily-generic signature, so it is
77
+ * accepted as `unknown` here and narrowed once to {@link RegisterToolFn} — our
78
+ * handler returns the same `{ content: [{ type: "text", text }] }` shape every
79
+ * Bridge tool uses, just expressed with a simpler local type.
80
+ *
81
+ * BAPI-909 reduced this registrar to `get_epic_snapshot`. It stays a registrar
82
+ * rather than a bare call so the `conductor` group keeps one registration seam.
407
83
  */
408
84
  export function registerConductorTools(registerTool) {
409
85
  const reg = registerTool;
410
- registerEmitEventTool(reg);
411
- registerPollEventsTool(reg);
412
- registerWaitForEventTool(reg);
413
- registerGetSupervisorSnapshotTool(reg);
414
86
  registerGetEpicSnapshotTool(reg);
415
- registerWaitForDoneGateTool(reg);
416
- registerSendMessageTool(reg);
417
- registerCheckMessagesTool(reg);
418
87
  }