@bridge_gpt/mcp-server 0.2.9 → 0.2.12

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 (43) hide show
  1. package/README.md +59 -7
  2. package/build/commands.generated.js +6 -6
  3. package/build/conductor/bridge-api-client.js +263 -35
  4. package/build/conductor/cli.js +38 -17
  5. package/build/conductor/doctor.js +35 -2
  6. package/build/conductor/done-gate.js +301 -58
  7. package/build/conductor/epic-reconcile.js +318 -4
  8. package/build/conductor/epic-runtime.js +382 -18
  9. package/build/conductor/epic-state.js +188 -15
  10. package/build/conductor/errors.js +12 -0
  11. package/build/conductor/git-ci-types.js +16 -0
  12. package/build/conductor/git-producer.js +4 -4
  13. package/build/conductor/merge-ledger.js +7 -7
  14. package/build/conductor/pr-ci-producer.js +118 -19
  15. package/build/conductor/pr-review-producer.js +116 -0
  16. package/build/conductor/producer-ledger.js +5 -5
  17. package/build/conductor/spec-review-producer.js +88 -0
  18. package/build/conductor/store.js +105 -26
  19. package/build/conductor/supervisor-ledger.js +2 -2
  20. package/build/conductor/supervisor-merge.js +5 -5
  21. package/build/conductor/supervisor-message-relay.js +32 -1
  22. package/build/conductor/supervisor-runtime.js +10 -10
  23. package/build/conductor/taxonomy.js +8 -0
  24. package/build/conductor/tools.js +7 -7
  25. package/build/conductor-bin.js +12350 -19
  26. package/build/conductor-claude-hook-bin.js +167 -17
  27. package/build/decision-page-schema.js +26 -0
  28. package/build/doctor.js +200 -0
  29. package/build/index.js +23696 -4351
  30. package/build/init.js +481 -0
  31. package/build/install-bridge.js +772 -0
  32. package/build/mcp-profile.js +43 -0
  33. package/build/pipelines.generated.js +70 -48
  34. package/build/readme.generated.js +1 -1
  35. package/build/start-tickets-conductor.js +1 -0
  36. package/build/start-tickets.js +186 -10
  37. package/build/upgrade-cli.js +154 -0
  38. package/build/version.generated.js +1 -1
  39. package/package.json +7 -4
  40. package/pipelines/check-ci-ticket.json +2 -2
  41. package/pipelines/implement-ticket.json +2 -2
  42. package/pipelines/learn-repository.json +84 -42
  43. package/smoke-test/SMOKE-TEST.md +11 -17
@@ -1,21 +1,171 @@
1
1
  #!/usr/bin/env node
2
- /**
3
- * Standalone Claude Code → conductor lifecycle hook bin.
4
- *
5
- * Registered into a spawned Claude worker's `.claude/settings.local.json` by
6
- * `start-tickets`. Claude Code pipes the native hook payload to this process on
7
- * stdin; the runner maps it to a conductor event and shells to the `conductor`
8
- * CLI. It is intentionally non-blocking: {@link runClaudeConductorHookCli}
9
- * always returns `0`, and any unexpected top-level failure here also exits `0`
10
- * so the Claude tool loop is never blocked by hook problems.
11
- */
12
- import { runClaudeConductorHookCli } from "./conductor/claude-hook.js";
13
- let exitCode = 0;
14
- try {
15
- exitCode = runClaudeConductorHookCli();
2
+
3
+ // src/conductor/claude-hook.ts
4
+ import { spawnSync } from "node:child_process";
5
+ import { readFileSync } from "node:fs";
6
+ var CONDUCTOR_HOOK_EMIT_FAILED_WARNING = "Warning: conductor hook emit failed.";
7
+ function resolveClaudeHookEventName(payload) {
8
+ if (payload === null || typeof payload !== "object" || Array.isArray(payload)) {
9
+ return null;
10
+ }
11
+ const record = payload;
12
+ const candidates = ["hook_event_name", "hookEventName", "event_name", "event"];
13
+ for (const key of candidates) {
14
+ const value = record[key];
15
+ if (typeof value === "string" && value.trim().length > 0) {
16
+ return value.trim();
17
+ }
18
+ }
19
+ return null;
20
+ }
21
+ function mapClaudeHookEventToSemanticType(eventName) {
22
+ switch (eventName) {
23
+ case "SessionStart":
24
+ return "run.started";
25
+ case "Stop":
26
+ return "run.stopped";
27
+ case "SubagentStop":
28
+ return "run.stopped";
29
+ case "Notification":
30
+ return "agent.notification";
31
+ case "PreToolUse":
32
+ return "tool.intent";
33
+ default:
34
+ return null;
35
+ }
36
+ }
37
+ var REQUIRED_IDENTITY_ENV = [
38
+ "BAPI_CONDUCTOR_RUN_ID",
39
+ "BAPI_CONDUCTOR_WORKER_ID",
40
+ "BAPI_CONDUCTOR_TICKET_KEY",
41
+ "BAPI_CONDUCTOR_WORKTREE_PATH"
42
+ ];
43
+ function nonEmpty(value) {
44
+ return typeof value === "string" && value.trim().length > 0;
45
+ }
46
+ function buildClaudeHookConductorEvent(payload, env) {
47
+ const eventName = resolveClaudeHookEventName(payload);
48
+ const type = mapClaudeHookEventToSemanticType(eventName);
49
+ if (type === null) {
50
+ return null;
51
+ }
52
+ if (env.BAPI_CONDUCTOR_ENABLED !== "1") {
53
+ return null;
54
+ }
55
+ for (const key of REQUIRED_IDENTITY_ENV) {
56
+ if (!nonEmpty(env[key])) {
57
+ return null;
58
+ }
59
+ }
60
+ const ticketKey = env.BAPI_CONDUCTOR_TICKET_KEY.trim();
61
+ const worktreePath = env.BAPI_CONDUCTOR_WORKTREE_PATH.trim();
62
+ const details = {
63
+ ticket_key: ticketKey,
64
+ worktree_path: worktreePath
65
+ };
66
+ if (nonEmpty(env.BAPI_CONDUCTOR_REPO_NAME)) {
67
+ details.repo = env.BAPI_CONDUCTOR_REPO_NAME.trim();
68
+ }
69
+ const raw = payload !== null && typeof payload === "object" && !Array.isArray(payload) ? payload : { payload };
70
+ return {
71
+ source: "claude-code",
72
+ type,
73
+ subject: ticketKey,
74
+ run_id: env.BAPI_CONDUCTOR_RUN_ID.trim(),
75
+ worker_id: env.BAPI_CONDUCTOR_WORKER_ID.trim(),
76
+ producer: "claude-code-hook",
77
+ observed_via: "claude-code-hook",
78
+ data: {
79
+ details,
80
+ raw
81
+ }
82
+ };
83
+ }
84
+ function buildConductorEmitEventArgs(event) {
85
+ const args = ["--type", event.type, "--source", event.source];
86
+ if (nonEmpty(event.subject ?? void 0)) args.push("--subject", event.subject);
87
+ if (nonEmpty(event.run_id ?? void 0)) args.push("--run-id", event.run_id);
88
+ if (nonEmpty(event.worker_id ?? void 0)) args.push("--worker-id", event.worker_id);
89
+ if (nonEmpty(event.producer ?? void 0)) args.push("--producer", event.producer);
90
+ if (nonEmpty(event.observed_via ?? void 0)) {
91
+ args.push("--observed-via", event.observed_via);
92
+ }
93
+ args.push("--data-json-stdin", "--json");
94
+ return args;
16
95
  }
17
- catch {
18
- // Defensive: never let a hook failure propagate into the Claude tool loop.
19
- exitCode = 0;
96
+ function resolveConductorEmitCommand(env, emitArgs) {
97
+ const cliFile = env.BAPI_CONDUCTOR_CLI_FILE;
98
+ if (nonEmpty(cliFile)) {
99
+ return {
100
+ command: process.execPath,
101
+ args: [cliFile.trim(), "emit-event", ...emitArgs]
102
+ };
103
+ }
104
+ const bin = nonEmpty(env.BAPI_CONDUCTOR_BIN) ? env.BAPI_CONDUCTOR_BIN.trim() : "conductor";
105
+ return { command: bin, args: ["emit-event", ...emitArgs] };
106
+ }
107
+ var defaultConductorSpawn = (command, args, input) => {
108
+ const result = spawnSync(command, args, {
109
+ input,
110
+ encoding: "utf-8",
111
+ // Never use a shell: list args + stdin keep raw payloads/secrets out of any
112
+ // shell-interpreted command string.
113
+ shell: false
114
+ });
115
+ return { status: result.status, error: result.error };
116
+ };
117
+ function emitClaudeHookEventWithConductorCli(event, deps = {}) {
118
+ const env = deps.env ?? process.env;
119
+ const spawn = deps.spawn ?? defaultConductorSpawn;
120
+ const emitArgs = buildConductorEmitEventArgs(event);
121
+ const { command, args } = resolveConductorEmitCommand(env, emitArgs);
122
+ const stdinPayload = JSON.stringify(event.data ?? {});
123
+ try {
124
+ const result = spawn(command, args, stdinPayload);
125
+ if (result.error) return false;
126
+ return result.status === 0;
127
+ } catch {
128
+ return false;
129
+ }
130
+ }
131
+ function runClaudeConductorHookCli(deps = {}) {
132
+ const env = deps.env ?? process.env;
133
+ const readStdin = deps.readStdin ?? (() => readStdinSync());
134
+ const warn = deps.warn ?? ((m) => process.stderr.write(`${m}
135
+ `));
136
+ let payload;
137
+ try {
138
+ const raw = readStdin();
139
+ payload = JSON.parse(raw);
140
+ } catch {
141
+ warn(CONDUCTOR_HOOK_EMIT_FAILED_WARNING);
142
+ return 0;
143
+ }
144
+ let event;
145
+ try {
146
+ event = buildClaudeHookConductorEvent(payload, env);
147
+ } catch {
148
+ warn(CONDUCTOR_HOOK_EMIT_FAILED_WARNING);
149
+ return 0;
150
+ }
151
+ if (event === null) {
152
+ return 0;
153
+ }
154
+ const ok = emitClaudeHookEventWithConductorCli(event, { env, spawn: deps.spawn });
155
+ if (!ok) {
156
+ warn(CONDUCTOR_HOOK_EMIT_FAILED_WARNING);
157
+ }
158
+ return 0;
159
+ }
160
+ function readStdinSync() {
161
+ return readFileSync(0, "utf-8");
162
+ }
163
+
164
+ // src/conductor-claude-hook-bin.ts
165
+ var exitCode = 0;
166
+ try {
167
+ exitCode = runClaudeConductorHookCli();
168
+ } catch {
169
+ exitCode = 0;
20
170
  }
21
171
  process.exit(exitCode);
@@ -192,3 +192,29 @@ export const DecisionPageInputShape = {
192
192
  .describe("Confirmed improvements displayed as informational list, not submitted."),
193
193
  };
194
194
  export const DecisionPageInputSchema = z.object(DecisionPageInputShape);
195
+ // Lean schema for tool registration: keeps strict routing/output fields and
196
+ // defers the heavy nested arrays to a single `content` object. This removes
197
+ // ~1,800 tokens from the always-on core tools/list payload (BAPI-444).
198
+ // The handler reconstructs the flat payload and validates with DecisionPageInputSchema.
199
+ export const DecisionPageLeanInputShape = {
200
+ ticket_key: z.string().describe("Jira ticket key, e.g. BAPI-123"),
201
+ artifact_type: z
202
+ .enum(["review_decisions", "pre_ticket_planning"])
203
+ .optional()
204
+ .default("review_decisions")
205
+ .describe('Which flavor of page to render. "review_decisions" (default) or "pre_ticket_planning" (adds system_goals and implementation_order sections).'),
206
+ output_subdir: z
207
+ .string()
208
+ .optional()
209
+ .default("review")
210
+ .describe('Optional docs-relative subdirectory to write the page under (default "review"). No absolute paths, backslashes, ".." segments, null bytes, or encoded path tokens.'),
211
+ output_filename: z
212
+ .string()
213
+ .optional()
214
+ .describe('Optional output filename (default "${ticket_key}-decisions.html"). Must end with .html; no path separators.'),
215
+ labels: DecisionPageLabelsSchema.optional().describe("Optional presentation-label overrides (title, intro, section_heading, improvements_heading)."),
216
+ content: z
217
+ .record(z.string(), z.unknown())
218
+ .optional()
219
+ .describe("Contains deferred heavy payloads like actionable_items or system_goals."),
220
+ };
package/build/doctor.js CHANGED
@@ -15,8 +15,11 @@
15
15
  * (`which`/`where`, `bash --version`, `git rev-parse`) through the injected deps.
16
16
  */
17
17
  import { readFile, stat } from "fs/promises";
18
+ import { spawn } from "child_process";
18
19
  import os from "os";
20
+ import path from "path";
19
21
  import { createDefaultStartTicketsDeps } from "./start-tickets.js";
22
+ import { VERSION } from "./version.generated.js";
20
23
  import { DEFAULT_AGENT_NAME, resolveAgentSpec, isAgentName, formatValidAgentNames, } from "./agent-registry.js";
21
24
  import { getDoctorPrereqDescriptors, probePrerequisite, } from "./start-tickets-prereqs.js";
22
25
  /** User-facing usage text for the read-only `doctor` subcommand. */
@@ -161,6 +164,188 @@ export function formatDoctorReport(platform, agent, collection) {
161
164
  : "All required prerequisites are present.");
162
165
  return lines.join("\n");
163
166
  }
167
+ // ---------------------------------------------------------------------------
168
+ // Launcher-cache diagnostics (BAPI-451 W3, E-4/E-9) — strictly read-only.
169
+ //
170
+ // install-bridge writes a launcher pinned to `@${VERSION}` (a specific `_npx`
171
+ // bucket). The FIRST launch of a not-yet-cached pin pays a cold install inline,
172
+ // which can exceed an MCP client's connect deadline. doctor inspects each
173
+ // project-local config's actual `bridge-api` launcher spec and reports:
174
+ // - `unpinned` — `@latest` / no version suffix (cache bucket is unstable).
175
+ // - `stale-pinned` — pinned to a version other than the running `VERSION`.
176
+ // - `warmed` — pinned to `VERSION` and the `_npx` bucket is present.
177
+ // - `indeterminate`— pinned to `VERSION` but the bucket probe was not a clean
178
+ // hit (a.k.a. `pinned-but-unwarmed`), or the config/spec was
179
+ // ambiguous. Never silently reported as "warm".
180
+ // The bucket probe is `npx --no-install <spec> --version`: `--no-install` means
181
+ // the probe can never mutate (warm) the cache, honoring the read-only contract.
182
+ // ---------------------------------------------------------------------------
183
+ /** The npm package whose pinned launcher spec doctor inspects. */
184
+ const BRIDGE_PACKAGE_NAME = "@bridge_gpt/mcp-server";
185
+ /** Project-local MCP configs doctor inspects (mirrors install-bridge's targets). */
186
+ const LAUNCHER_CONFIG_TARGETS = [
187
+ { relPath: ".mcp.json", topLevelKey: "mcpServers" },
188
+ { relPath: ".cursor/mcp.json", topLevelKey: "mcpServers" },
189
+ { relPath: ".vscode/mcp.json", topLevelKey: "servers" },
190
+ ];
191
+ /**
192
+ * Extract the `@bridge_gpt/mcp-server[@version]` launcher spec from an args array.
193
+ * The package name is itself scoped (`@scope/name`), so the version delimiter is
194
+ * the LAST `@`: `@bridge_gpt/mcp-server` is unpinned (no version), while
195
+ * `@bridge_gpt/mcp-server@0.2.11` pins `0.2.11`. Returns null when no spec matches.
196
+ */
197
+ export function parseLauncherPin(args) {
198
+ if (!Array.isArray(args))
199
+ return null;
200
+ for (const arg of args) {
201
+ if (typeof arg !== "string")
202
+ continue;
203
+ if (arg === BRIDGE_PACKAGE_NAME)
204
+ return { spec: arg, version: null };
205
+ if (arg.startsWith(`${BRIDGE_PACKAGE_NAME}@`)) {
206
+ const version = arg.slice(BRIDGE_PACKAGE_NAME.length + 1).trim();
207
+ return { spec: arg, version: version.length > 0 ? version : null };
208
+ }
209
+ }
210
+ return null;
211
+ }
212
+ /**
213
+ * Default read-only cache probe. Spawns `npx --no-install <spec> --version`
214
+ * array-based (`shell: false`), ignores child stdio, and bounds the run with a
215
+ * timeout. A clean exit-0 means the pinned bucket is warm; any non-zero exit,
216
+ * spawn error, or timeout is classified `indeterminate` (never silently "warm").
217
+ * `--no-install` guarantees the probe never downloads/warms the bucket.
218
+ */
219
+ function probeNpxNoInstallDefault(spec) {
220
+ return new Promise((resolve) => {
221
+ try {
222
+ const child = spawn("npx", ["--no-install", spec, "--version"], {
223
+ shell: false,
224
+ stdio: "ignore",
225
+ timeout: 30_000,
226
+ });
227
+ child.on("error", () => resolve({ warmed: false, indeterminate: true }));
228
+ child.on("close", (code, signal) => {
229
+ if (signal)
230
+ resolve({ warmed: false, indeterminate: true });
231
+ else if (code === 0)
232
+ resolve({ warmed: true, indeterminate: false });
233
+ else
234
+ resolve({ warmed: false, indeterminate: true });
235
+ });
236
+ }
237
+ catch {
238
+ resolve({ warmed: false, indeterminate: true });
239
+ }
240
+ });
241
+ }
242
+ /**
243
+ * Inspect every project-local MCP config's `bridge-api` launcher pin (read-only).
244
+ * Configs that are absent, unparseable, or carry no `bridge-api` entry are skipped
245
+ * (an absent config is not a finding). Each present launcher is classified and,
246
+ * when pinned to the running `VERSION`, probed read-only with `--no-install`.
247
+ */
248
+ export async function inspectLauncherCache(deps) {
249
+ const inspections = [];
250
+ for (const { relPath, topLevelKey } of LAUNCHER_CONFIG_TARGETS) {
251
+ const fullPath = path.join(deps.cwd, relPath);
252
+ let raw;
253
+ try {
254
+ raw = await deps.readFile(fullPath);
255
+ }
256
+ catch {
257
+ continue; // config absent — not a finding.
258
+ }
259
+ let parsed;
260
+ try {
261
+ parsed = JSON.parse(raw);
262
+ }
263
+ catch {
264
+ inspections.push({
265
+ relPath,
266
+ spec: null,
267
+ pinnedVersion: null,
268
+ state: "indeterminate",
269
+ remediation: "config is not valid JSON — cannot determine the launcher pin.",
270
+ });
271
+ continue;
272
+ }
273
+ const entry = parsed && typeof parsed === "object"
274
+ ? parsed[topLevelKey]?.["bridge-api"]
275
+ : undefined;
276
+ if (!entry)
277
+ continue; // no bridge-api launcher in this config — not a finding.
278
+ const pin = parseLauncherPin(entry.args);
279
+ if (!pin) {
280
+ inspections.push({
281
+ relPath,
282
+ spec: null,
283
+ pinnedVersion: null,
284
+ state: "indeterminate",
285
+ remediation: `no ${BRIDGE_PACKAGE_NAME} spec found in the launcher args.`,
286
+ });
287
+ continue;
288
+ }
289
+ if (pin.version === null || pin.version === "latest") {
290
+ inspections.push({
291
+ relPath,
292
+ spec: pin.spec,
293
+ pinnedVersion: pin.version,
294
+ state: "unpinned",
295
+ remediation: `pin the launcher to ${BRIDGE_PACKAGE_NAME}@${VERSION} (run /install-bridge or the install-bridge subcommand to rewrite this config).`,
296
+ });
297
+ continue;
298
+ }
299
+ if (pin.version !== VERSION) {
300
+ inspections.push({
301
+ relPath,
302
+ spec: pin.spec,
303
+ pinnedVersion: pin.version,
304
+ state: "stale-pinned",
305
+ remediation: `config pins ${pin.version} but this package is ${VERSION}; run upgrade-bridge / install-bridge to repin and re-warm.`,
306
+ });
307
+ continue;
308
+ }
309
+ // Pinned to the running VERSION — probe the bucket read-only.
310
+ const probe = await deps.probeNpxNoInstall(pin.spec);
311
+ if (probe.warmed) {
312
+ inspections.push({ relPath, spec: pin.spec, pinnedVersion: pin.version, state: "warmed" });
313
+ }
314
+ else {
315
+ inspections.push({
316
+ relPath,
317
+ spec: pin.spec,
318
+ pinnedVersion: pin.version,
319
+ state: "indeterminate",
320
+ remediation: "pinned-but-unwarmed: the pinned _npx bucket is not a confirmed cache hit, so the first MCP launch may pay a one-time cold install. " +
321
+ `Warm it with: npx ${pin.spec} --version, or raise MCP_TIMEOUT for the first launch.`,
322
+ });
323
+ }
324
+ }
325
+ return inspections;
326
+ }
327
+ /** Render the launcher-cache section of the doctor report (pure formatting). */
328
+ export function formatLauncherCacheReport(inspections) {
329
+ const lines = ["", "Launcher cache (MCP cold-start readiness)", ""];
330
+ if (inspections.length === 0) {
331
+ lines.push("No project-local bridge-api launcher configs found to inspect.");
332
+ return lines.join("\n");
333
+ }
334
+ const labels = {
335
+ warmed: "WARMED ",
336
+ unpinned: "UNPINNED",
337
+ "stale-pinned": "STALE-PINNED",
338
+ indeterminate: "INDETERMINATE",
339
+ };
340
+ for (const i of inspections) {
341
+ const specText = i.spec ? ` (${i.spec})` : "";
342
+ lines.push(`${labels[i.state]} ${i.relPath}${specText}`);
343
+ if (i.remediation) {
344
+ lines.push(` ${i.remediation}`);
345
+ }
346
+ }
347
+ return lines.join("\n");
348
+ }
164
349
  /**
165
350
  * CLI entry for the read-only `doctor` subcommand. Returns a process exit code.
166
351
  * Help returns 0; parser errors return 1; otherwise it prints the report and
@@ -186,6 +371,21 @@ export async function runDoctorCli(argv, overrides = {}) {
186
371
  const agent = resolveAgentSpec(parsed.options.agentName) ?? resolveAgentSpec(DEFAULT_AGENT_NAME);
187
372
  const collection = await collectDoctorResults(deps, parsed.options.agentName);
188
373
  log(formatDoctorReport(deps.platform, agent, collection));
374
+ // Strictly read-only launcher-cache diagnostics (BAPI-451). Best-effort: a probe
375
+ // failure never changes the doctor exit code (cold-start readiness is advisory,
376
+ // not a hard prerequisite). The exit code remains driven by required prereqs.
377
+ try {
378
+ const launcherDeps = {
379
+ cwd: overrides.launcherProbe?.cwd ?? deps.cwd,
380
+ readFile: overrides.launcherProbe?.readFile ?? ((p) => readFile(p, "utf-8")),
381
+ probeNpxNoInstall: overrides.launcherProbe?.probeNpxNoInstall ?? probeNpxNoInstallDefault,
382
+ };
383
+ const launcherInspections = await inspectLauncherCache(launcherDeps);
384
+ log(formatLauncherCacheReport(launcherInspections));
385
+ }
386
+ catch {
387
+ /* launcher-cache diagnostics are advisory; never block the doctor report */
388
+ }
189
389
  if (!collection.ok)
190
390
  return 1;
191
391
  return collection.results.some((r) => !r.found) ? 1 : 0;