@bridge_gpt/mcp-server 0.2.16 → 0.2.19

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 (103) hide show
  1. package/CONDUCTOR.md +75 -0
  2. package/README.md +2 -2
  3. package/build/agent-capabilities/probe-context.js +13 -3
  4. package/build/agent-capabilities/probes.js +262 -11
  5. package/build/agent-capabilities/reporter.js +1 -0
  6. package/build/agents.generated.js +3 -3
  7. package/build/backend-warnings.js +44 -0
  8. package/build/claude-settings.js +129 -0
  9. package/build/commands.generated.js +7 -6
  10. package/build/conductor/bridge-api-client.js +198 -18
  11. package/build/conductor/claude-hook.js +22 -4
  12. package/build/conductor/cli.js +76 -25
  13. package/build/conductor/deny-enforcement-preflight.js +96 -0
  14. package/build/conductor/doctor.js +183 -2
  15. package/build/conductor/done-gate.js +5 -0
  16. package/build/conductor/epic-reconcile.js +71 -14
  17. package/build/conductor/epic-runtime.js +839 -67
  18. package/build/conductor/epic-state.js +524 -63
  19. package/build/conductor/errors.js +156 -3
  20. package/build/conductor/event-accessors.js +252 -0
  21. package/build/conductor/file-scope-guard.js +201 -0
  22. package/build/conductor/github-mergeability.js +85 -0
  23. package/build/conductor/local-merge.js +47 -1
  24. package/build/conductor/merge-identity.js +41 -0
  25. package/build/conductor/merge-ledger.js +19 -72
  26. package/build/conductor/plan.js +12 -2
  27. package/build/conductor/pr-ci-producer.js +17 -2
  28. package/build/conductor/pr-discovery.js +11 -1
  29. package/build/conductor/producer-ledger.js +1 -1
  30. package/build/conductor/store.js +161 -18
  31. package/build/conductor/supervisor-config.js +4 -39
  32. package/build/conductor/supervisor-escalation.js +10 -26
  33. package/build/conductor/supervisor-ledger.js +5 -12
  34. package/build/conductor/supervisor-merge.js +32 -5
  35. package/build/conductor/supervisor-message-relay.js +2 -5
  36. package/build/conductor/supervisor-notification.js +1 -1
  37. package/build/conductor/supervisor-runtime.js +12 -54
  38. package/build/conductor/supervisor-state.js +4 -18
  39. package/build/conductor/supervisor-types.js +2 -2
  40. package/build/conductor/taxonomy.js +12 -0
  41. package/build/conductor/tools.js +28 -6
  42. package/build/conductor/worker-ledger-cli.js +244 -0
  43. package/build/conductor-bin.js +1800 -5166
  44. package/build/conductor-claude-hook-bin.js +4 -2
  45. package/build/doctor.js +40 -0
  46. package/build/executor/cli.js +229 -0
  47. package/build/executor/credentials.js +65 -0
  48. package/build/executor/deps.js +117 -0
  49. package/build/executor/env.js +79 -0
  50. package/build/executor/heartbeat.js +59 -0
  51. package/build/executor/http-client.js +131 -0
  52. package/build/executor/index.js +10 -0
  53. package/build/executor/job-errors.js +55 -0
  54. package/build/executor/job-log-registry.js +110 -0
  55. package/build/executor/job-runner.js +688 -0
  56. package/build/executor/job-types.js +60 -0
  57. package/build/executor/merge-job.js +155 -0
  58. package/build/executor/observation.js +123 -0
  59. package/build/executor/permissions.js +79 -0
  60. package/build/executor/preflight.js +144 -0
  61. package/build/executor/process.js +81 -0
  62. package/build/executor/prompt-spec.js +235 -0
  63. package/build/executor/results.js +134 -0
  64. package/build/executor/resume-pre-spawn.js +179 -0
  65. package/build/executor/runner.js +98 -0
  66. package/build/executor/terminal-mutation.js +34 -0
  67. package/build/executor/test-clock.js +109 -0
  68. package/build/executor/types.js +18 -0
  69. package/build/executor/verdict-artifact.js +53 -0
  70. package/build/executor/viewer-tabs.js +78 -0
  71. package/build/executor/watch-cli.js +113 -0
  72. package/build/executor/worker-command.js +106 -0
  73. package/build/executor/worker-finalization.js +97 -0
  74. package/build/executor/worker-log.js +92 -0
  75. package/build/executor/worktree-gc.js +134 -0
  76. package/build/executor/worktree-inspection.js +86 -0
  77. package/build/executor/worktree.js +103 -0
  78. package/build/index.js +13950 -9669
  79. package/build/install-bridge.js +25 -8
  80. package/build/install-doctor.js +387 -0
  81. package/build/mcp-invoke.js +19 -3
  82. package/build/mcp-provisioning.js +31 -25
  83. package/build/mcp-registration-doctor.js +27 -7
  84. package/build/mcp-server-invocation.js +152 -0
  85. package/build/pipelines.generated.js +31 -6
  86. package/build/readme.generated.js +1 -1
  87. package/build/regression-check.js +53 -1
  88. package/build/review-tickets.js +175 -21
  89. package/build/sfcc/reads-site-preference.js +52 -19
  90. package/build/start-tickets-conductor.js +47 -99
  91. package/build/start-tickets-prereqs.js +185 -4
  92. package/build/start-tickets.js +218 -180
  93. package/build/version.generated.js +1 -1
  94. package/build/visual-diff-worker.js +313 -0
  95. package/build/visual-diff.js +632 -0
  96. package/build/worktree-core.js +202 -0
  97. package/package.json +10 -6
  98. package/pipelines/review-ticket.json +24 -2
  99. package/public/css/main.min.css +3311 -1
  100. package/public/css/main.min.css.map +1 -1
  101. package/public/js/main.min.js +7924 -1
  102. package/public/js/main.min.js.map +1 -1
  103. package/smoke-test/SMOKE-TEST.md +5 -2
@@ -65,7 +65,14 @@ export function buildPrewarmCommandPreview() {
65
65
  return `npx ${buildPrewarmArgs().join(" ")}`;
66
66
  }
67
67
  /** The natural-language sequential prompt handed to the spawned agent session. */
68
- export const INSTALL_BRIDGE_AGENT_PROMPT = "Please execute the /install-bridge command. Once it completes successfully, execute the /learn-repository command.";
68
+ export const INSTALL_BRIDGE_AGENT_PROMPT = "Please execute the /install-bridge command. Once it completes successfully, execute the " +
69
+ "/learn-repository command. After /learn-repository completes, ask the user for consent to start " +
70
+ "repository indexing; with consent, call the parse_repository MCP tool once and report that the " +
71
+ "job was queued (progress is checked with get_parse_status — do not poll it to completion). " +
72
+ "When you are done, end with an explicit summary line stating how " +
73
+ "many config fields the apply_install_manifest call applied (e.g. 'Applied 8 of 9 derived " +
74
+ "fields') and whether indexing was queued — if 0 fields were applied, say so loudly and explain " +
75
+ "what is still pending.";
69
76
  /** Default base URL when `BAPI_BASE_URL` is unset (mirrors index.ts). */
70
77
  export const DEFAULT_BAPI_BASE_URL = "https://bridgegpt-api.com";
71
78
  /** Default docs dir when `BAPI_DOCS_DIR` is unset (mirrors index.ts). */
@@ -739,15 +746,19 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
739
746
  }
740
747
  else {
741
748
  log(` warning: could not persist the routing credential (${result.kind}). ` +
742
- `start-tickets model routing may not resolve the key for ${plan.credentialTarget}; ` +
743
- "set BAPI_API_KEY in the shell or re-run install-bridge.");
749
+ `start-tickets model routing may not resolve the key for ${plan.credentialTarget} ` +
750
+ "and will fail open to the premium/Opus tier (the most expensive) — " +
751
+ "set BAPI_API_KEY in the shell or re-run install-bridge, then verify with " +
752
+ "'npx -y @bridge_gpt/mcp-server doctor'.");
744
753
  }
745
754
  }
746
755
  catch {
747
756
  // Fail-open: persistence is best-effort (mirrors Stage 6). The secret is never
748
757
  // included in the warning.
749
758
  log(" warning: could not persist the routing credential (unexpected error). " +
750
- "start-tickets model routing may need BAPI_API_KEY in the shell.");
759
+ "start-tickets model routing may need BAPI_API_KEY in the shell and will fail open " +
760
+ "to the premium/Opus tier (the most expensive) until fixed — verify with " +
761
+ "'npx -y @bridge_gpt/mcp-server doctor'.");
751
762
  }
752
763
  // ---- Step 5 — spawn a fresh agent session for the agentic remainder ----
753
764
  log(`Step 5/5 — opening a ${agent.name} session for /install-bridge + /learn-repository…`);
@@ -759,14 +770,20 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
759
770
  if (!spawnResult.ok) {
760
771
  // Steps 1–4 (scaffold, config, connectivity-verified, credential persist) all
761
772
  // succeeded and are durable; only the best-effort Step 5 tab spawn failed. The
762
- // install itself is complete, so this is a non-fatal warning (exit 0) — the
773
+ // deterministic setup is complete, so this is a non-fatal warning (exit 0) — the
763
774
  // user can run the agentic remainder by hand. Mirrors start-tickets treating a
764
775
  // spawn failure as non-fatal to the run.
765
- errorLog(`Warning: setup completed, but the agent session could not be opened (${spawnResult.error}). ` +
766
- "Run /install-bridge then /learn-repository manually in this project.");
776
+ errorLog(`Warning: setup steps completed, but the agent session could not be opened (${spawnResult.error}). ` +
777
+ "The project is NOT configured yet — run /install-bridge then /learn-repository manually " +
778
+ "in this project to derive and apply the config fields.");
767
779
  return 0;
768
780
  }
769
781
  log("");
770
- log("install-bridge complete. A fresh agent session is running /install-bridge then /learn-repository.");
782
+ log(`install-bridge setup steps complete. A fresh ${agent.name} session is now running ` +
783
+ "/install-bridge then /learn-repository.");
784
+ log("NOTE: the install is not finished until that session's apply reports applied fields — " +
785
+ "it will pause to ask you to approve the project description. Verify afterwards on the " +
786
+ "project's Get Started page (install status panel) or via the session's " +
787
+ "'Applied N of M' summary.");
771
788
  return 0;
772
789
  }
@@ -0,0 +1,387 @@
1
+ /**
2
+ * install-doctor — an advisory, strictly read-only "Install status" section for
3
+ * the `doctor` subcommand, turning the install manifest's prose done-criteria
4
+ * into an executable checklist.
5
+ *
6
+ * Checks (all best-effort, all read-only GETs, none affect the doctor exit code):
7
+ * - identity — can a repo name / base URL be resolved (env, then the
8
+ * project-local MCP config env blocks)?
9
+ * - credential — does the standard resolver (env → user-scoped store) find
10
+ * a Bridge API key? (never reads or prints the key value)
11
+ * - connectivity — GET /jira/ping (distinguishes rejected key vs unknown repo)
12
+ * - bootstrap — GET /jira/config/install-manifest → N/M bootstrap fields set
13
+ * - integrations — the same manifest's presence-only integrations checklist
14
+ * (unconfigured credentials are configured by a human in the
15
+ * setup UI — never through an agent)
16
+ * - indexing — GET /jira/parse-status → parse job running / idle
17
+ *
18
+ * The section is advisory by contract: any failure inside it degrades to a
19
+ * WARN/SKIP line, never a throw, and the doctor exit code stays driven by the
20
+ * required prerequisites only.
21
+ */
22
+ import path from "path";
23
+ import { resolveBapiCredentials } from "./credential-store.js";
24
+ /** Default production base URL (mirrors install-bridge's DEFAULT_BAPI_BASE_URL). */
25
+ const DEFAULT_BASE_URL = "https://bridgegpt-api.com";
26
+ /** Project-local MCP configs whose env blocks may carry repo/base-URL values. */
27
+ const MCP_CONFIG_ENV_TARGETS = [
28
+ { relPath: ".mcp.json", topLevelKey: "mcpServers" },
29
+ { relPath: ".cursor/mcp.json", topLevelKey: "mcpServers" },
30
+ { relPath: ".vscode/mcp.json", topLevelKey: "servers" },
31
+ ];
32
+ /** Timeout for each read-only probe request. */
33
+ const PROBE_TIMEOUT_MS = 5_000;
34
+ /**
35
+ * Resolve the repo name and base URL: environment first, then the NON-SECRET
36
+ * env-block values of the project-local MCP configs (`BAPI_REPO_NAME` /
37
+ * `BAPI_BASE_URL`). The API key is deliberately NEVER read from config files —
38
+ * secret resolution goes through the standard credential resolver only.
39
+ */
40
+ export async function resolveInstallDoctorTarget(deps) {
41
+ const envRepo = deps.env.BAPI_REPO_NAME?.trim();
42
+ const envBase = deps.env.BAPI_BASE_URL?.trim();
43
+ let repoName = envRepo && envRepo.length > 0 ? envRepo : null;
44
+ let repoSource = repoName ? "env" : null;
45
+ let baseUrl = envBase && envBase.length > 0 ? envBase : null;
46
+ if (!repoName || !baseUrl) {
47
+ for (const { relPath, topLevelKey } of MCP_CONFIG_ENV_TARGETS) {
48
+ let raw;
49
+ try {
50
+ raw = await deps.readFile(path.join(deps.cwd, relPath));
51
+ }
52
+ catch {
53
+ continue; // absent config is not a finding
54
+ }
55
+ let parsed;
56
+ try {
57
+ parsed = JSON.parse(raw);
58
+ }
59
+ catch {
60
+ continue;
61
+ }
62
+ const envBlock = parsed && typeof parsed === "object"
63
+ ? // eslint-disable-next-line @typescript-eslint/no-explicit-any
64
+ parsed[topLevelKey]?.["bridge-api"]?.env
65
+ : undefined;
66
+ if (!envBlock)
67
+ continue;
68
+ if (!repoName && typeof envBlock.BAPI_REPO_NAME === "string" && envBlock.BAPI_REPO_NAME.trim()) {
69
+ repoName = envBlock.BAPI_REPO_NAME.trim();
70
+ repoSource = "config";
71
+ }
72
+ if (!baseUrl && typeof envBlock.BAPI_BASE_URL === "string" && envBlock.BAPI_BASE_URL.trim()) {
73
+ baseUrl = envBlock.BAPI_BASE_URL.trim();
74
+ }
75
+ if (repoName && baseUrl)
76
+ break;
77
+ }
78
+ }
79
+ return { repoName, repoSource, baseUrl: baseUrl ?? DEFAULT_BASE_URL };
80
+ }
81
+ /** Read-only GET with a bounded timeout. Never throws. */
82
+ async function probeGet(deps, url, apiKey) {
83
+ try {
84
+ const resp = await deps.fetch(url, {
85
+ headers: { "X-API-Key": apiKey },
86
+ signal: AbortSignal.timeout(PROBE_TIMEOUT_MS),
87
+ });
88
+ let body = null;
89
+ try {
90
+ body = await resp.json();
91
+ }
92
+ catch {
93
+ /* non-JSON body — status alone is still useful */
94
+ }
95
+ return { ok: true, status: resp.status, body };
96
+ }
97
+ catch (e) {
98
+ return { ok: false, error: e instanceof Error ? e.message : String(e) };
99
+ }
100
+ }
101
+ /** Count set/total bootstrap fields from an install-manifest response body. */
102
+ function summarizeManifestGroups(body) {
103
+ if (!body || typeof body !== "object")
104
+ return null;
105
+ const groups = body.groups;
106
+ if (!Array.isArray(groups))
107
+ return null;
108
+ let total = 0;
109
+ const unset = [];
110
+ for (const group of groups) {
111
+ const fields = group?.fields;
112
+ if (!Array.isArray(fields))
113
+ continue;
114
+ for (const field of fields) {
115
+ const name = field?.field_name;
116
+ if (typeof name !== "string")
117
+ continue;
118
+ total += 1;
119
+ if (!field.is_set)
120
+ unset.push(name);
121
+ }
122
+ }
123
+ return { total, unset };
124
+ }
125
+ /** Summarize the manifest's integrations checklist (presence booleans only). */
126
+ function summarizeIntegrations(body) {
127
+ if (!body || typeof body !== "object")
128
+ return null;
129
+ const integrations = body.integrations;
130
+ if (!Array.isArray(integrations))
131
+ return null;
132
+ let total = 0;
133
+ const unconfigured = [];
134
+ for (const item of integrations) {
135
+ const label = item?.label;
136
+ if (typeof label !== "string")
137
+ continue;
138
+ total += 1;
139
+ if (!item.is_configured)
140
+ unconfigured.push(label);
141
+ }
142
+ return { total, unconfigured };
143
+ }
144
+ /**
145
+ * Collect the install-status checklist. Never throws; every failure path becomes
146
+ * a WARN or SKIP check line. Performs at most three read-only GETs, and only
147
+ * when both a repo name and a credential resolve.
148
+ */
149
+ export async function collectInstallStatusChecks(deps) {
150
+ const checks = [];
151
+ const target = await resolveInstallDoctorTarget(deps);
152
+ if (!target.repoName) {
153
+ checks.push({
154
+ id: "identity",
155
+ label: "Repository identity",
156
+ status: "SKIP",
157
+ detail: "no BAPI_REPO_NAME in the environment or project-local MCP configs",
158
+ remediation: "run install-bridge (or set BAPI_REPO_NAME) to configure this project.",
159
+ });
160
+ return checks;
161
+ }
162
+ checks.push({
163
+ id: "identity",
164
+ label: "Repository identity",
165
+ status: "PASS",
166
+ detail: `${target.repoName} (from ${target.repoSource}), base URL ${target.baseUrl}`,
167
+ });
168
+ const credDeps = {
169
+ env: deps.env,
170
+ homedir: deps.homedir,
171
+ platform: deps.platform,
172
+ readFile: deps.readFile,
173
+ stat: deps.stat,
174
+ stderr: () => { },
175
+ };
176
+ const cred = await resolveBapiCredentials(target.repoName, credDeps);
177
+ if (!cred.ok) {
178
+ checks.push({
179
+ id: "credential",
180
+ label: "Bridge API credential",
181
+ status: "WARN",
182
+ detail: `not resolved (${cred.kind})`,
183
+ remediation: "set BAPI_API_KEY or persist it via /install-bridge / the credentials subcommand; " +
184
+ "until then the remaining install checks are skipped.",
185
+ });
186
+ for (const [id, label] of [
187
+ ["connectivity", "Server connectivity"],
188
+ ["bootstrap", "Bootstrap config fields"],
189
+ ["indexing", "Repository indexing"],
190
+ ]) {
191
+ checks.push({ id, label, status: "SKIP", detail: "no credential resolved" });
192
+ }
193
+ return checks;
194
+ }
195
+ checks.push({
196
+ id: "credential",
197
+ label: "Bridge API credential",
198
+ status: "PASS",
199
+ detail: `resolved from ${cred.credentials.source} (value never read into the report)`,
200
+ });
201
+ const apiKey = cred.credentials.apiKey;
202
+ const repoQuery = `repo_name=${encodeURIComponent(target.repoName)}`;
203
+ // --- connectivity ---
204
+ const ping = await probeGet(deps, `${target.baseUrl}/jira/ping?${repoQuery}`, apiKey);
205
+ if (!ping.ok) {
206
+ checks.push({
207
+ id: "connectivity",
208
+ label: "Server connectivity",
209
+ status: "WARN",
210
+ detail: `unreachable (${ping.error})`,
211
+ remediation: `check ${target.baseUrl} and your network, then re-run doctor.`,
212
+ });
213
+ }
214
+ else if (ping.status === 200) {
215
+ checks.push({ id: "connectivity", label: "Server connectivity", status: "PASS", detail: "ping OK" });
216
+ }
217
+ else if (ping.status === 401 || ping.status === 403) {
218
+ checks.push({
219
+ id: "connectivity",
220
+ label: "Server connectivity",
221
+ status: "WARN",
222
+ detail: `the server rejected the resolved key (HTTP ${ping.status})`,
223
+ remediation: "the key may have been rotated — re-run install-bridge with a current key.",
224
+ });
225
+ }
226
+ else if (ping.status === 404) {
227
+ checks.push({
228
+ id: "connectivity",
229
+ label: "Server connectivity",
230
+ status: "WARN",
231
+ detail: `the server does not know repo '${target.repoName}' (HTTP 404)`,
232
+ remediation: "create the project in the setup UI (or check BAPI_REPO_NAME spelling).",
233
+ });
234
+ }
235
+ else {
236
+ checks.push({
237
+ id: "connectivity",
238
+ label: "Server connectivity",
239
+ status: "WARN",
240
+ detail: `unexpected HTTP ${ping.status} from /jira/ping`,
241
+ });
242
+ }
243
+ // --- bootstrap fields ---
244
+ const manifest = await probeGet(deps, `${target.baseUrl}/jira/config/install-manifest?${repoQuery}`, apiKey);
245
+ if (!manifest.ok) {
246
+ checks.push({
247
+ id: "bootstrap",
248
+ label: "Bootstrap config fields",
249
+ status: "WARN",
250
+ detail: `manifest unreachable (${manifest.error})`,
251
+ });
252
+ }
253
+ else if (manifest.status === 404) {
254
+ checks.push({
255
+ id: "bootstrap",
256
+ label: "Bootstrap config fields",
257
+ status: "WARN",
258
+ detail: "no configuration row found for this repository",
259
+ remediation: "create the project in the setup UI, then run /install-bridge.",
260
+ });
261
+ }
262
+ else if (manifest.status === 200) {
263
+ const summary = summarizeManifestGroups(manifest.body);
264
+ if (!summary) {
265
+ checks.push({
266
+ id: "bootstrap",
267
+ label: "Bootstrap config fields",
268
+ status: "WARN",
269
+ detail: "manifest response had an unexpected shape",
270
+ });
271
+ }
272
+ else if (summary.unset.length === 0) {
273
+ checks.push({
274
+ id: "bootstrap",
275
+ label: "Bootstrap config fields",
276
+ status: "PASS",
277
+ detail: `${summary.total}/${summary.total} bootstrap fields set`,
278
+ });
279
+ }
280
+ else {
281
+ checks.push({
282
+ id: "bootstrap",
283
+ label: "Bootstrap config fields",
284
+ status: "WARN",
285
+ detail: `${summary.total - summary.unset.length}/${summary.total} set; unset: ${summary.unset.join(", ")}`,
286
+ remediation: "run /install-bridge to derive the unset fields (intentionally-unset fields are fine to leave).",
287
+ });
288
+ }
289
+ }
290
+ else {
291
+ checks.push({
292
+ id: "bootstrap",
293
+ label: "Bootstrap config fields",
294
+ status: "WARN",
295
+ detail: `unexpected HTTP ${manifest.status} from the install manifest`,
296
+ });
297
+ }
298
+ // --- integrations (from the same manifest response; presence booleans only) ---
299
+ if (manifest.ok && manifest.status === 200) {
300
+ const integrations = summarizeIntegrations(manifest.body);
301
+ if (integrations === null) {
302
+ checks.push({
303
+ id: "integrations",
304
+ label: "Integration credentials",
305
+ status: "SKIP",
306
+ detail: "the manifest response carried no integrations checklist",
307
+ });
308
+ }
309
+ else if (integrations.unconfigured.length === 0) {
310
+ checks.push({
311
+ id: "integrations",
312
+ label: "Integration credentials",
313
+ status: "PASS",
314
+ detail: `${integrations.total}/${integrations.total} configured`,
315
+ });
316
+ }
317
+ else {
318
+ checks.push({
319
+ id: "integrations",
320
+ label: "Integration credentials",
321
+ status: "WARN",
322
+ detail: `not configured: ${integrations.unconfigured.join(", ")}`,
323
+ remediation: "a human configures these in the setup UI (project settings) — Bridge API never accepts " +
324
+ "integration secrets through an agent or MCP tool.",
325
+ });
326
+ }
327
+ }
328
+ else {
329
+ checks.push({
330
+ id: "integrations",
331
+ label: "Integration credentials",
332
+ status: "SKIP",
333
+ detail: "manifest unavailable",
334
+ });
335
+ }
336
+ // --- indexing ---
337
+ const parse = await probeGet(deps, `${target.baseUrl}/jira/parse-status?${repoQuery}`, apiKey);
338
+ if (!parse.ok || parse.status !== 200) {
339
+ checks.push({
340
+ id: "indexing",
341
+ label: "Repository indexing",
342
+ status: "SKIP",
343
+ detail: parse.ok ? `HTTP ${parse.status} from /jira/parse-status` : `unreachable (${parse.error})`,
344
+ });
345
+ }
346
+ else {
347
+ const status = parse.body?.status;
348
+ if (status === "in_progress") {
349
+ const startedAt = parse.body.started_at;
350
+ checks.push({
351
+ id: "indexing",
352
+ label: "Repository indexing",
353
+ status: "INFO",
354
+ detail: `parse job in progress${typeof startedAt === "string" ? ` (started ${startedAt})` : ""}`,
355
+ });
356
+ }
357
+ else {
358
+ checks.push({
359
+ id: "indexing",
360
+ label: "Repository indexing",
361
+ status: "INFO",
362
+ detail: "no parse job currently running — if this repository has never been indexed, " +
363
+ "queue one with /parse-repository (monitor with get_parse_status).",
364
+ });
365
+ }
366
+ }
367
+ return checks;
368
+ }
369
+ /** Render the install-status section of the doctor report (pure formatting). */
370
+ export function formatInstallStatusReport(checks) {
371
+ const lines = ["", "Install status (easy-install done criteria — advisory)", ""];
372
+ const pad = {
373
+ PASS: "PASS ",
374
+ WARN: "WARN ",
375
+ INFO: "INFO ",
376
+ SKIP: "SKIPPED",
377
+ };
378
+ for (const check of checks) {
379
+ lines.push(`${pad[check.status]} ${check.label}${check.detail ? ` — ${check.detail}` : ""}`);
380
+ if (check.remediation) {
381
+ lines.push(` ${check.remediation}`);
382
+ }
383
+ }
384
+ lines.push("");
385
+ lines.push("This section is advisory and never changes the doctor exit code. It performs read-only GETs only.");
386
+ return lines.join("\n");
387
+ }
@@ -32,10 +32,15 @@ import { getThirdPartyTargetDefinition, resolveThirdPartyTargetEnv, validateThir
32
32
  export function getMcpInvokeUsage() {
33
33
  return [
34
34
  "Usage:",
35
- " npx -y @bridge_gpt/mcp-server mcp-invoke --target <target> --project-root <ABS_WORKTREE_PATH>",
35
+ " node <abs>/mcp_server/build/index.js mcp-invoke --target <target> --project-root <ABS_WORKTREE_PATH>",
36
+ "",
37
+ " (npm-channel fallback may invoke the same shim through a package spec, e.g.",
38
+ " npx -y @bridge_gpt/mcp-server@latest mcp-invoke --target <target> --project-root <ABS_WORKTREE_PATH>)",
36
39
  "",
37
40
  "Internal worktree shim: resolves the target's launch command and credentials",
38
41
  "from the given project root, then spawns the real MCP server over stdio.",
42
+ "Argument parsing is identical regardless of how this process was launched",
43
+ "(absolute-path node invocation vs npm-channel npx).",
39
44
  "",
40
45
  "Flags:",
41
46
  " --target <target> MCP target to launch. 'bapi' launches the Bridge",
@@ -264,11 +269,22 @@ export async function resolveBapiInvocation(projectRoot, deps) {
264
269
  if (!credResult.ok) {
265
270
  return { ok: false, error: `failed to resolve credentials: ${credResult.error}` };
266
271
  }
267
- const env = buildChildEnv(deps.env, {
272
+ const overlay = {
268
273
  BAPI_API_KEY: credResult.credentials.apiKey,
269
274
  BAPI_PROJECT_ROOT: projectRoot,
270
275
  BAPI_REPO_NAME: repoName,
271
- });
276
+ };
277
+ // BAPI-527: `CONDUCTOR_NODE_PATH` is NON-SECRET operational metadata (the
278
+ // captured conductor Node path used by the worker-ledger CLI subprocess).
279
+ // `buildChildEnv` already inherits the full parent env, so this re-affirms the
280
+ // pass-through explicitly and ONLY when present — it is a non-secret allowlist
281
+ // entry, never a credential. It is emphatically NOT the MCP server's re-exec
282
+ // command: the spawned command stays `deps.execPath` + `[scriptPath]` below.
283
+ const conductorNodePath = deps.env.CONDUCTOR_NODE_PATH;
284
+ if (typeof conductorNodePath === "string" && conductorNodePath.trim().length > 0) {
285
+ overlay.CONDUCTOR_NODE_PATH = conductorNodePath;
286
+ }
287
+ const env = buildChildEnv(deps.env, overlay);
272
288
  return {
273
289
  ok: true,
274
290
  invocation: { command: deps.execPath, args: [deps.scriptPath], env },
@@ -18,9 +18,9 @@
18
18
  * to avoid a runtime import cycle.
19
19
  */
20
20
  import path from "path";
21
- import { VERSION } from "./version.generated.js";
22
21
  import { readBridgeConfig } from "./bridge-config.js";
23
22
  import { getThirdPartyTargetDefinition, validateThirdPartyTargetManifestEntry, } from "./third-party-mcp-targets.js";
23
+ import { buildMcpShimCommand, } from "./mcp-server-invocation.js";
24
24
  // ---------------------------------------------------------------------------
25
25
  // Pure helpers
26
26
  // ---------------------------------------------------------------------------
@@ -55,27 +55,27 @@ export function serverNameForMcpTarget(target) {
55
55
  return target === "bapi" ? "bridge-api" : target;
56
56
  }
57
57
  /**
58
- * Build a secret-free shim entry for any target. `args` are version-pinned and
59
- * carry the target name plus the absolute project root; there is intentionally
60
- * no `env` block.
58
+ * Build a secret-free shim entry for any target from a structured invocation.
59
+ * The command/args carry the target name plus the absolute project root; there is
60
+ * intentionally no `env` block. The absolute-build-path form serializes as
61
+ * `{ command: "node", args: ["<abs>/build/index.js", "mcp-invoke", …] }` (the
62
+ * node executable is a single substitutable field), and the npm-channel fallback
63
+ * as `{ command: "npx", args: ["-y", "<spec>", "mcp-invoke", …] }`.
64
+ *
65
+ * BAPI-527: `.mcp.json` remains intentionally ENV-FREE. `CONDUCTOR_NODE_PATH`
66
+ * (the captured conductor Node path for the worker-ledger CLI subprocess) is NOT
67
+ * written here — it is inherited at runtime from the worker SHELL env that
68
+ * `start-tickets` injects at the spawn boundary, never stored as a registration
69
+ * value. Keeping the shim env-free preserves the credential-safety invariant
70
+ * (secrets are resolved by the `mcp-invoke` shim at launch, not persisted in the
71
+ * worktree) and equally avoids persisting non-secret operational metadata here.
61
72
  */
62
- export function buildShimMcpServerEntry(target, absoluteWorktreePath) {
63
- return {
64
- command: "npx",
65
- args: [
66
- "-y",
67
- `@bridge_gpt/mcp-server@${VERSION}`,
68
- "mcp-invoke",
69
- "--target",
70
- target,
71
- "--project-root",
72
- absoluteWorktreePath,
73
- ],
74
- };
73
+ export function buildShimMcpServerEntry(target, absoluteWorktreePath, invocation) {
74
+ return buildMcpShimCommand(invocation, target, absoluteWorktreePath);
75
75
  }
76
76
  /** Back-compat wrapper around `buildShimMcpServerEntry("bapi", ...)`. */
77
- export function buildBridgeApiShimMcpServerEntry(absoluteWorktreePath) {
78
- return buildShimMcpServerEntry("bapi", absoluteWorktreePath);
77
+ export function buildBridgeApiShimMcpServerEntry(absoluteWorktreePath, invocation) {
78
+ return buildShimMcpServerEntry("bapi", absoluteWorktreePath, invocation);
79
79
  }
80
80
  /**
81
81
  * Convert all supported manifest `mcp` entries into a map of server name -> shim
@@ -84,12 +84,12 @@ export function buildBridgeApiShimMcpServerEntry(absoluteWorktreePath) {
84
84
  * or incomplete non-`bapi` targets are skipped with a secret-free warning (they
85
85
  * never appear in the returned entries).
86
86
  */
87
- export function buildMcpServerEntriesForManifest(manifest, absoluteWorktreePath) {
87
+ export function buildMcpServerEntriesForManifest(manifest, absoluteWorktreePath, invocation) {
88
88
  const entries = {};
89
89
  const warnings = [];
90
90
  for (const mcp of manifest.mcp) {
91
91
  if (mcp.target === "bapi") {
92
- entries[serverNameForMcpTarget("bapi")] = buildShimMcpServerEntry("bapi", absoluteWorktreePath);
92
+ entries[serverNameForMcpTarget("bapi")] = buildShimMcpServerEntry("bapi", absoluteWorktreePath, invocation);
93
93
  continue;
94
94
  }
95
95
  const definition = getThirdPartyTargetDefinition(mcp.target);
@@ -102,9 +102,9 @@ export function buildMcpServerEntriesForManifest(manifest, absoluteWorktreePath)
102
102
  warnings.push(`MCP target '${mcp.target}' registration skipped: ${validation.error}.`);
103
103
  continue;
104
104
  }
105
- entries[serverNameForMcpTarget(mcp.target)] = buildShimMcpServerEntry(mcp.target, absoluteWorktreePath);
105
+ entries[serverNameForMcpTarget(mcp.target)] = buildShimMcpServerEntry(mcp.target, absoluteWorktreePath, invocation);
106
106
  }
107
- return { entries, warnings };
107
+ return { entries, warnings, registrationForm: invocation.form };
108
108
  }
109
109
  /** Both registration files written for every provisioned worktree. */
110
110
  export function getWorktreeMcpRegistrationTargets(worktreePath, platform) {
@@ -305,7 +305,7 @@ export async function provisionMcpRegistrationForWorktree(row, deps) {
305
305
  if (!normalized.ok) {
306
306
  return { ...row, status: "spawn-failed", error: `MCP provisioning failed: ${normalized.error}` };
307
307
  }
308
- const built = buildMcpServerEntriesForManifest(read.manifest, normalized.path);
308
+ const built = buildMcpServerEntriesForManifest(read.manifest, normalized.path, deps.mcpServerInvocation);
309
309
  if (Object.keys(built.entries).length === 0) {
310
310
  // Nothing supported to write (e.g. a manifest with no bapi target and no
311
311
  // supported Tier-2 targets). Surface any warnings but leave status unchanged.
@@ -318,10 +318,16 @@ export async function provisionMcpRegistrationForWorktree(row, deps) {
318
318
  return { ...row, status: "spawn-failed", error: `MCP provisioning failed: ${result.error}` };
319
319
  }
320
320
  }
321
+ // Record the resolved registration form for audit (path-free — the form label
322
+ // only, never the absolute server entry path, which could leak a username).
323
+ let result = { ...row, mcpRegistrationForm: built.registrationForm };
324
+ result = withWarnings(result, built.warnings);
325
+ if (built.registrationForm !== "absolute-build-path") {
326
+ result = withWarning(result, "MCP registration used npm-channel fallback because an on-disk build entry was not resolvable.");
327
+ }
321
328
  // Pre-approve the just-registered servers so Claude Code does not prompt to
322
329
  // trust them in this fresh worktree path. Best-effort: a failure here only
323
330
  // loses the convenience of skipping the prompt, so warn and still spawn.
324
- let result = withWarnings(row, built.warnings);
325
331
  const serverNames = Object.keys(built.entries);
326
332
  const trust = await writeClaudeServerTrustSettings(normalized.path, serverNames, deps);
327
333
  if (!trust.ok) {
@@ -35,22 +35,42 @@ function flagValue(args, flag) {
35
35
  return undefined;
36
36
  }
37
37
  /**
38
- * True when `entry` is a secret-free Bridge API shim registration that launches
39
- * `npx -y @bridge_gpt/mcp-server@<version> mcp-invoke --target bapi
40
- * --project-root <worktreeRoot>`. The `--target` must be `bapi` and the
41
- * `--project-root` must match the given worktree exactly.
38
+ * True when `entry` is a secret-free Bridge API shim registration pointing at
39
+ * THIS worktree. Two forms are accepted (BAPI-526):
40
+ *
41
+ * - Absolute-build-path (PRIMARY): an absolute `node` (or absolute Node-path)
42
+ * invocation of the conductor's own compiled server, e.g.
43
+ * `{ command: "node", args: ["<abs>/mcp_server/build/index.js", "mcp-invoke",
44
+ * "--target", "bapi", "--project-root", "<worktreeRoot>"] }`. `command` may be
45
+ * any non-empty string ("node" or a future absolute Node path); `args[0]` is a
46
+ * non-empty server entry path.
47
+ * - npm-channel (LEGACY/FALLBACK): `{ command: "npx", args: ["-y",
48
+ * "@bridge_gpt/mcp-server@<spec>", "mcp-invoke", "--target", "bapi",
49
+ * "--project-root", "<worktreeRoot>"] }`.
50
+ *
51
+ * In both forms `--target` must be `bapi` and `--project-root` must match the
52
+ * given worktree exactly. The server entry path is NOT required to sit under any
53
+ * particular username/path prefix.
42
54
  */
43
55
  export function isBridgeApiShimEntry(entry, worktreeRoot) {
44
56
  if (!entry || typeof entry !== "object" || Array.isArray(entry))
45
57
  return false;
46
58
  const candidate = entry;
47
- if (candidate.command !== "npx")
59
+ if (typeof candidate.command !== "string" || candidate.command.length === 0)
48
60
  return false;
49
61
  if (!Array.isArray(candidate.args))
50
62
  return false;
51
63
  const args = candidate.args.filter((a) => typeof a === "string");
52
- if (!args.some((a) => a.startsWith("@bridge_gpt/mcp-server")))
53
- return false;
64
+ if (candidate.command === "npx") {
65
+ // npm-channel fallback: must reference the package spec.
66
+ if (!args.some((a) => a.startsWith("@bridge_gpt/mcp-server")))
67
+ return false;
68
+ }
69
+ else {
70
+ // Absolute-build-path: first arg must be a non-empty server entry path.
71
+ if (typeof args[0] !== "string" || args[0].length === 0)
72
+ return false;
73
+ }
54
74
  if (!args.includes("mcp-invoke"))
55
75
  return false;
56
76
  if (flagValue(args, "--target") !== "bapi")