@bridge_gpt/mcp-server 0.2.41 → 0.2.42

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/README.md +10 -10
  2. package/build/agent-capabilities/cli.js +2 -1
  3. package/build/agent-launchers/claude-executor-adapter.js +17 -4
  4. package/build/claude-user-config-doctor.js +42 -11
  5. package/build/cli-release.js +2 -1
  6. package/build/commands.generated.js +4 -4
  7. package/build/conduct-epic/bridge-client.js +354 -113
  8. package/build/conduct-epic/checkpoint-store.js +17 -0
  9. package/build/conduct-epic/cli.js +752 -99
  10. package/build/conduct-epic/cut-protocol.js +327 -0
  11. package/build/conduct-epic/spawn.js +14 -2
  12. package/build/conductor/bridge-api-client.js +27 -1
  13. package/build/conductor/cli.js +46 -1
  14. package/build/conductor/doctor.js +101 -16
  15. package/build/conductor/epic-reconcile.js +72 -19
  16. package/build/conductor/epic-runtime.js +15 -3
  17. package/build/conductor/errors.js +47 -0
  18. package/build/conductor/git-hooks.js +205 -11
  19. package/build/conductor/install-doctor.js +230 -1
  20. package/build/conductor/local-merge.js +130 -28
  21. package/build/conductor/tools.js +32 -3
  22. package/build/conductor/worker-ledger-cli.js +27 -1
  23. package/build/conductor-bin.js +15 -15
  24. package/build/credentials-cli.js +3 -2
  25. package/build/doctor.js +107 -41
  26. package/build/executor/cli.js +48 -1
  27. package/build/executor/env.js +21 -0
  28. package/build/executor/index-scope.js +39 -0
  29. package/build/executor/job-log-registry.js +69 -0
  30. package/build/executor/job-runner.js +148 -26
  31. package/build/executor/live-worker-registry.js +83 -0
  32. package/build/executor/observation.js +167 -6
  33. package/build/executor/platform.js +147 -3
  34. package/build/executor/process.js +58 -14
  35. package/build/executor/runner.js +235 -48
  36. package/build/executor/test-clock.js +3 -2
  37. package/build/index-scope-contract.js +96 -0
  38. package/build/index.js +153 -204
  39. package/build/init.js +83 -22
  40. package/build/install-bridge-conductor.js +323 -14
  41. package/build/install-bridge.js +202 -38
  42. package/build/install-doctor.js +23 -9
  43. package/build/install-reexec.js +2 -1
  44. package/build/launcher-config-inspection.js +83 -22
  45. package/build/mcp-host-config.js +331 -67
  46. package/build/mcp-host-targets.js +45 -21
  47. package/build/mcp-identity.js +92 -0
  48. package/build/mcp-install-state.js +94 -1
  49. package/build/mcp-invoke.js +2 -1
  50. package/build/mcp-provisioning.js +45 -12
  51. package/build/mcp-registration-doctor.js +35 -13
  52. package/build/mcp-server-invocation.js +4 -2
  53. package/build/merge-pull-request.js +208 -9
  54. package/build/pipelines.generated.js +3 -3
  55. package/build/plane/defaults.js +4 -1
  56. package/build/plane/preflight.js +81 -10
  57. package/build/plane/test-fakes.js +9 -1
  58. package/build/readme.generated.js +1 -1
  59. package/build/regression-check.js +3 -2
  60. package/build/review-tickets.js +8 -7
  61. package/build/run-unit-tests-launcher.js +74 -1
  62. package/build/schedule-run.js +3 -2
  63. package/build/setup-epic.js +453 -78
  64. package/build/sfcc/tool-wrapper.js +15 -0
  65. package/build/start-tickets-prereqs.js +11 -6
  66. package/build/start-tickets.js +91 -85
  67. package/build/update-check.js +3 -2
  68. package/build/upgrade-advice.js +2 -1
  69. package/build/upgrade-cli.js +50 -18
  70. package/build/version.generated.js +1 -1
  71. package/docs/CONDUCTOR.md +22 -0
  72. package/docs/install/mcp-tool-integrations.md +19 -3
  73. package/package.json +2 -2
@@ -143,13 +143,15 @@ import { randomBytes as cryptoRandomBytes, createHash } from "crypto";
143
143
  import os from "os";
144
144
  import path from "path";
145
145
  import readline from "readline";
146
- import { runInit, buildBridgeApiEntry, resolveInitScaffoldAssets, refreshBridgeApiPackageSpec, currentBridgePackageSpec, } from "./init.js";
146
+ import { runInit, InitPreflightError, buildBridgeApiEntry, mergeBridgeApiProfileToken, resolveInitScaffoldAssets, refreshBridgeApiPackageSpec, currentBridgePackageSpec, } from "./init.js";
147
147
  import { hasProjectRootMarker as sharedHasProjectRootMarker } from "./project-root.js";
148
148
  import { VERSION } from "./version.generated.js";
149
149
  import { validateRepoName } from "./bridge-config.js";
150
- import { MCP_HOST_TARGETS, HOST_PLATFORM_ORDER, allHostTargets, agentForPlatform, isHostPlatformId, detectDefaultPlatforms, } from "./mcp-host-targets.js";
151
- import { provisionHostTarget, createDefaultVendorProcessDeps, } from "./mcp-host-config.js";
152
- import { recordInstalledExecutorServiceUnit, recordInstalledProjectArtifact, writeMcpInstallState, } from "./mcp-install-state.js";
150
+ import { MCP_HOST_TARGETS, HOST_PLATFORM_ORDER, allHostTargets, agentForPlatform, isHostPlatformId, detectDefaultPlatforms, hostAdapterForTarget, } from "./mcp-host-targets.js";
151
+ import { provisionHostTarget, createDefaultVendorProcessDeps, inspectBridgeApiProfileToken, } from "./mcp-host-config.js";
152
+ import { MCP_SERVER_NAME, MCP_PACKAGE_NAME, resolveRegistrationKey, } from "./mcp-identity.js";
153
+ import { DUPLICATE_REGISTRATION_GUIDANCE } from "./launcher-config-inspection.js";
154
+ import { recordInstalledConductorCapability, recordInstalledExecutorServiceUnit, recordInstalledProjectArtifact, writeMcpInstallState, } from "./mcp-install-state.js";
153
155
  // Executor provisioning (BAPI-779). The generator and the lifecycle module are
154
156
  // imported DIRECTLY and driven programmatically — never by spawning
155
157
  // `executor install-service` as a subprocess, which would lose the typed plan,
@@ -157,8 +159,10 @@ import { recordInstalledExecutorServiceUnit, recordInstalledProjectArtifact, wri
157
159
  import { collectExecutorInstallPreflight, } from "./executor/install-preflight.js";
158
160
  import { inspectExecutorServiceState, startExecutorService, } from "./executor/service-lifecycle.js";
159
161
  import { executorLaunchdLabelForId, executorSystemdUnitNameForId, executorLaunchdPlistPathForId, executorSystemdUnitPathForId, resolvePackagedExecutorInvocation, writeExecutorServicePlan, } from "./executor/service-unit.js";
162
+ import { collectConductorNativeLedgerSafe, } from "./conductor/doctor.js";
163
+ import { installConductorGitHooks, } from "./conductor/git-hooks.js";
160
164
  import { runInstallBridgeConductorCli, } from "./install-bridge-conductor.js";
161
- import { runConductorInstallDoctor, } from "./conductor/install-doctor.js";
165
+ import { runConductorInstallDoctor, CONDUCTOR_PROFILE_TOKEN, } from "./conductor/install-doctor.js";
162
166
  import { resolveConductorBridgeApiAccess, } from "./conductor/bridge-api-client.js";
163
167
  import { claudeReviewWorkflowPath, writeClaudeReviewWorkflow, } from "./claude-review-workflow.js";
164
168
  import { runSetupEpicCli } from "./setup-epic.js";
@@ -194,7 +198,7 @@ export function buildPrewarmArgs() {
194
198
  // cache with the exact tarball this `@${VERSION}`-pinned warm needs, so resolve
195
199
  // from cache and skip the redundant registry-metadata round-trip (falls back to
196
200
  // the network on a genuine miss).
197
- return ["-y", "--prefer-offline", `@bridge_gpt/mcp-server@${VERSION}`, "--version"];
201
+ return ["-y", "--prefer-offline", `${MCP_PACKAGE_NAME}@${VERSION}`, "--version"];
198
202
  }
199
203
  /** Secret-free preview of the pre-warm command (no env, no key). */
200
204
  export function buildPrewarmCommandPreview() {
@@ -232,7 +236,7 @@ export const INSTALL_BRIDGE_AGENT_PROMPT = "Execute the /install-bridge command
232
236
  "contract metadata only — render both kinds identically, with no label, icon, or separate list, " +
233
237
  "and never branch on it. Keep an empty section visible with a short neutral line rather than " +
234
238
  "dropping it, and after both sections close with a pointer to the Bridge MCP server README at " +
235
- "https://www.npmjs.com/package/@bridge_gpt/mcp-server for the complete tool documentation, " +
239
+ `https://www.npmjs.com/package/${MCP_PACKAGE_NAME} for the complete tool documentation, ` +
236
240
  "including when either or both sections are empty. Do not render the obsolete five-section report, " +
237
241
  "and do not locally filter, count, sort, regroup, infer availability, fabricate an item, or fall " +
238
242
  "back to the complete tool_capabilities catalog or the workflows collection. If " +
@@ -302,7 +306,7 @@ export function getInstallBridgeUsage(baseUrl = DEFAULT_BAPI_BASE_URL) {
302
306
  const setupUrl = buildInstallBridgeSetupUrl(baseUrl);
303
307
  return [
304
308
  "Usage:",
305
- " npx -y @bridge_gpt/mcp-server install [flags]",
309
+ ` npx -y ${MCP_PACKAGE_NAME} install [flags]`,
306
310
  "",
307
311
  "One-command Bridge API project bootstrap. Scaffolds the project, writes the",
308
312
  "per-host MCP config with your credentials, verifies connectivity, persists the",
@@ -502,8 +506,17 @@ export function getInstallBridgeUsage(baseUrl = DEFAULT_BAPI_BASE_URL) {
502
506
  " which writes the safe project-default supervisor",
503
507
  " posture in one transaction; it optionally",
504
508
  " scaffolds a parameterized claude-review workflow",
505
- " (its own separate consent), prints the capability",
506
- " matrix, and re-runs the doctor. `--dry-run`",
509
+ " (its own separate consent), then closes the local",
510
+ " out-of-the-box gap in two more phases, each behind",
511
+ " its OWN consent: tool visibility adds the",
512
+ " `conductor` token to existing .mcp.json /",
513
+ " .vscode/mcp.json / .cursor/mcp.json entries (never",
514
+ " Codex or Copilot, and never creating a file) and",
515
+ " prints the MCP-client restart advisory; local",
516
+ " observability installs the managed conductor Git",
517
+ " hooks and then reports native ledger loadability",
518
+ " read-only. It finally prints the capability",
519
+ " matrix and re-runs the doctor. `--dry-run`",
507
520
  " performs NO write of any kind and no write-consent",
508
521
  " prompt (the base-URL confirmation still applies).",
509
522
  " Executor service units are generated by",
@@ -770,7 +783,7 @@ export function promptSecretViaReadline(promptText, input = process.stdin, outpu
770
783
  * The deferral command printed with the GitHub offer (BAPI-669, U6). Declining is
771
784
  * cheap precisely because this exists — the connection is a standalone command.
772
785
  */
773
- export const INSTALL_BRIDGE_CONNECT_GITHUB_COMMAND = "npx -y @bridge_gpt/mcp-server connect-github";
786
+ export const INSTALL_BRIDGE_CONNECT_GITHUB_COMMAND = `npx -y ${MCP_PACKAGE_NAME} connect-github`;
774
787
  /**
775
788
  * The `Step 4b` label + purpose + deferral lines printed immediately before the
776
789
  * GitHub prompt (BAPI-669, U6). Shared with {@link buildLaunchStepPreview} so the
@@ -845,13 +858,13 @@ export async function offerGithubConnection(repoName, baseUrl, deps, log) {
845
858
  const code = await runGithubConnectionFlow(connectDeps, api, repoName);
846
859
  if (code !== 0) {
847
860
  log(" note: GitHub was not connected. Your install is complete — connect GitHub later with " +
848
- `'npx -y @bridge_gpt/mcp-server@latest connect-github --repo ${repoName}'.`);
861
+ `'npx -y ${MCP_PACKAGE_NAME}@latest connect-github --repo ${repoName}'.`);
849
862
  }
850
863
  }
851
864
  catch {
852
865
  // The install is already durable; a failure here is never fatal to it.
853
866
  log(" note: the GitHub connection offer could not run. Your install is complete — connect " +
854
- `GitHub later with 'npx -y @bridge_gpt/mcp-server@latest connect-github --repo ${repoName}'.`);
867
+ `GitHub later with 'npx -y ${MCP_PACKAGE_NAME}@latest connect-github --repo ${repoName}'.`);
855
868
  }
856
869
  }
857
870
  /**
@@ -995,6 +1008,12 @@ export function createDefaultInstallBridgeDeps() {
995
1008
  fetch: params.fetch,
996
1009
  reviewPolicySource: params.reviewPolicySource,
997
1010
  readWorkflowFile: () => params.readFile(claudeReviewWorkflowPath(params.cwd)),
1011
+ // BAPI-775: the SAME project root the rest of installation resolves
1012
+ // against, and the SAME read-only host-config inspector the
1013
+ // tool-visibility phase reports from — so the doctor's profile-token
1014
+ // section and the phase can never disagree about what is on disk.
1015
+ projectRoot: params.cwd,
1016
+ inspectProfileToken: (projectRoot, token) => inspectBridgeApiProfileToken(projectRoot, token, { readFile: params.readFile }),
998
1017
  // Read-only service-state probe (BAPI-779). Typed to return a state,
999
1018
  // never a runner, so the doctor cannot start/enable/reload anything.
1000
1019
  inspectExecutorServiceState: params.inspectExecutorServiceState,
@@ -1141,6 +1160,46 @@ function buildConductorExecutorDeps(deps, baseUrl) {
1141
1160
  sleep: deps.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms))),
1142
1161
  };
1143
1162
  }
1163
+ /**
1164
+ * Compose the local out-of-the-box provisioning bundle the two BAPI-775 phases
1165
+ * run on, from the FINAL merged deps.
1166
+ *
1167
+ * Same shape and same rationale as {@link buildConductorExecutorDeps}: every
1168
+ * entry is a thin adapter over an existing implementation, and nothing here
1169
+ * decides anything. Consent, ordering, degradation, and reporting live in
1170
+ * `install-bridge-conductor.ts`.
1171
+ *
1172
+ * `mergeBridgeApiProfileToken` is called with the resolved project root and the
1173
+ * conductor token, reusing the helper byte-for-byte: its idempotent token-set
1174
+ * merge and its refusal to create a missing host config are load-bearing, and
1175
+ * reimplementing either here would fork the only writer of
1176
+ * `BRIDGE_MCP_PROFILE`.
1177
+ */
1178
+ function buildConductorLocalDeps(deps) {
1179
+ return {
1180
+ mergeProfileToken: () => mergeBridgeApiProfileToken(deps.cwd, CONDUCTOR_PROFILE_TOKEN),
1181
+ inspectProfileToken: () => inspectBridgeApiProfileToken(deps.cwd, CONDUCTOR_PROFILE_TOKEN, {
1182
+ readFile: deps.readFile,
1183
+ }),
1184
+ // The repository root is this command's cwd, which is what the hook
1185
+ // installer resolves its worktree and hooks directory from.
1186
+ installGitHooks: async () => installConductorGitHooks({ cwd: deps.cwd }),
1187
+ // The SHARED collector (BAPI-775), so the phase's ledger verdict and the
1188
+ // conductor doctor's cannot diverge. Its safe variant contains any
1189
+ // collection failure as a generic degraded inspection.
1190
+ probeNativeLedger: () => collectConductorNativeLedgerSafe(),
1191
+ recordCapability: async (capability) => {
1192
+ const result = await recordInstalledConductorCapability(deps.cwd, capability, {
1193
+ readFile: deps.readFile,
1194
+ writeFile: (p, data) => deps.writeFile(p, data),
1195
+ rename: deps.rename,
1196
+ mkdir: (p, o) => deps.mkdir(p, o),
1197
+ unlink: deps.unlink,
1198
+ });
1199
+ return result.ok ? { ok: true } : { ok: false, error: result.error };
1200
+ },
1201
+ };
1202
+ }
1144
1203
  /**
1145
1204
  * Read-only local service-state inspector for the unified doctor (BAPI-779).
1146
1205
  *
@@ -1928,8 +1987,8 @@ function hostConfigTargetsForPlatforms(platforms) {
1928
1987
  const set = new Set(platforms);
1929
1988
  return HOST_PLATFORM_ORDER.filter((id) => set.has(id))
1930
1989
  .map((id) => MCP_HOST_TARGETS[id])
1931
- .filter((t) => t.scope === "project" && t.format === "json")
1932
- .map((t) => ({ relPath: t.relPath, topLevelKey: t.topLevelKey }));
1990
+ .filter((t) => t.scope === "project" && hostAdapterForTarget(t).format === "json")
1991
+ .map((t) => ({ relPath: t.relPath, topLevelKey: hostAdapterForTarget(t).topLevelKey }));
1933
1992
  }
1934
1993
  /**
1935
1994
  * Derive the human-facing labels for a set of selected platforms, in registry
@@ -1942,11 +2001,13 @@ export function labelsForPlatforms(platforms) {
1942
2001
  const set = new Set(platforms);
1943
2002
  return HOST_PLATFORM_ORDER.filter((id) => set.has(id)).map((id) => MCP_HOST_TARGETS[id].label);
1944
2003
  }
2004
+ /** Project-local host-config metadata sourced from the shared registry/adapter. */
2005
+ function toHostConfigTarget(target) {
2006
+ return { relPath: target.relPath, topLevelKey: hostAdapterForTarget(target).topLevelKey };
2007
+ }
1945
2008
  /** Resolve which project-local host configs to write, mirroring runInit detection. */
1946
2009
  async function resolveHostConfigTargets(deps) {
1947
- const targets = [
1948
- { relPath: ".mcp.json", topLevelKey: "mcpServers" },
1949
- ];
2010
+ const targets = [toHostConfigTarget(MCP_HOST_TARGETS["claude-code"])];
1950
2011
  const dirExists = async (rel) => {
1951
2012
  try {
1952
2013
  await deps.stat(path.join(deps.cwd, rel));
@@ -1957,10 +2018,10 @@ async function resolveHostConfigTargets(deps) {
1957
2018
  }
1958
2019
  };
1959
2020
  if (await dirExists(".vscode")) {
1960
- targets.push({ relPath: ".vscode/mcp.json", topLevelKey: "servers" });
2021
+ targets.push(toHostConfigTarget(MCP_HOST_TARGETS["copilot-vscode"]));
1961
2022
  }
1962
2023
  if ((await dirExists(".cursor")) || deps.env.CURSOR_TRACE_DIR) {
1963
- targets.push({ relPath: ".cursor/mcp.json", topLevelKey: "mcpServers" });
2024
+ targets.push(toHostConfigTarget(MCP_HOST_TARGETS.cursor));
1964
2025
  }
1965
2026
  return targets;
1966
2027
  }
@@ -2021,7 +2082,7 @@ export function buildInstallBridgeSecretFreeServerEntry(cwd, repoName, baseUrl,
2021
2082
  */
2022
2083
  function formatCredentialStoreGuidance(repoName, credentialStorePath) {
2023
2084
  return [
2024
- " The bridge-api server resolves your key at runtime from the BAPI_API_KEY environment",
2085
+ ` The ${MCP_SERVER_NAME} server resolves your key at runtime from the BAPI_API_KEY environment`,
2025
2086
  ` variable or the user-scoped credential store (${credentialStorePath}, target`,
2026
2087
  ` bapi:${repoName}), so it stays out of this file entirely.`,
2027
2088
  ];
@@ -2034,7 +2095,7 @@ function formatCredentialStoreGuidance(repoName, credentialStorePath) {
2034
2095
  * existing file — never for replacing it.
2035
2096
  */
2036
2097
  function formatSecretFreeManualMerge(relPath, topLevelKey, secretFreeEntry) {
2037
- const snippet = JSON.stringify({ [topLevelKey]: { "bridge-api": secretFreeEntry } }, null, 2);
2098
+ const snippet = JSON.stringify({ [topLevelKey]: { [MCP_SERVER_NAME]: secretFreeEntry } }, null, 2);
2038
2099
  return [
2039
2100
  ` To configure ${relPath} by hand, MERGE this secret-free entry into the existing`,
2040
2101
  " file (do not replace the file):",
@@ -2102,7 +2163,16 @@ async function detectExistingRealKey(deps, targets) {
2102
2163
  if (result.state === "absent")
2103
2164
  continue;
2104
2165
  const topLevel = asRecord(result.config[target.topLevelKey]);
2105
- const entry = asRecord(topLevel?.["bridge-api"]);
2166
+ // BAPI-807: a real key under a legacy `bridge-api` entry is just as much a
2167
+ // reason to ask for overwrite consent as one under `bridge`. A both-key
2168
+ // config is conservatively treated as possibly key-bearing for the same
2169
+ // reason `invalid` is.
2170
+ const resolution = resolveRegistrationKey(topLevel);
2171
+ if (resolution.state === "conflict")
2172
+ return true;
2173
+ if (resolution.state === "absent")
2174
+ continue;
2175
+ const entry = asRecord(topLevel?.[resolution.key]);
2106
2176
  const env = asRecord(entry?.["env"]);
2107
2177
  if (env && !isPlaceholderApiKey(env["BAPI_API_KEY"])) {
2108
2178
  return true;
@@ -2175,6 +2245,28 @@ export function preserveExistingLauncherArgs(existing, entry) {
2175
2245
  const preserved = refreshBridgeApiPackageSpec(existingArgs, currentBridgePackageSpec());
2176
2246
  return preserved ? { ...entry, args: preserved } : entry;
2177
2247
  }
2248
+ /**
2249
+ * Read-only pre-scan for both-key configs across EVERY project target (BAPI-807).
2250
+ *
2251
+ * Run before the registration phase begins writing, so a conflict discovered in
2252
+ * `.cursor/mcp.json` cannot leave `.mcp.json` and `.vscode/mcp.json` already
2253
+ * rewritten. A target that is absent or unparseable contributes no conflict —
2254
+ * those are handled by the existing per-target branches, which have their own
2255
+ * non-fatal semantics.
2256
+ */
2257
+ async function detectDuplicateRegistrations(deps, targets) {
2258
+ const conflicts = [];
2259
+ for (const target of targets) {
2260
+ const read = await readHostConfig(deps, path.join(deps.cwd, target.relPath));
2261
+ if (read.state !== "parsed")
2262
+ continue;
2263
+ const topLevel = asRecord(read.config[target.topLevelKey]);
2264
+ if (resolveRegistrationKey(topLevel).state === "conflict") {
2265
+ conflicts.push(target.relPath);
2266
+ }
2267
+ }
2268
+ return conflicts;
2269
+ }
2178
2270
  /**
2179
2271
  * Write the `bridge-api` entry into each host config via read-merge-write,
2180
2272
  * preserving unrelated servers and top-level keys (BAPI-666). Per target:
@@ -2192,6 +2284,13 @@ export function preserveExistingLauncherArgs(existing, entry) {
2192
2284
  async function writeHostConfigs(deps, targets, entries, trackedState, ctx) {
2193
2285
  const written = [];
2194
2286
  const skipped = [];
2287
+ // BAPI-807: detect both-key configs across ALL targets BEFORE the first write.
2288
+ // A multi-target registration phase that discovered the conflict on its third
2289
+ // target would already have rewritten the first two.
2290
+ const duplicateRegistrations = await detectDuplicateRegistrations(deps, targets);
2291
+ if (duplicateRegistrations.length > 0) {
2292
+ return { written, skipped, duplicateRegistrations };
2293
+ }
2195
2294
  for (const target of targets) {
2196
2295
  const fullPath = path.join(deps.cwd, target.relPath);
2197
2296
  const read = await readHostConfig(deps, fullPath);
@@ -2235,21 +2334,33 @@ async function writeHostConfigs(deps, targets, entries, trackedState, ctx) {
2235
2334
  // never initialized before its tracked-state consent is resolved.
2236
2335
  const config = read.state === "parsed" ? read.config : {};
2237
2336
  const topLevel = asRecord(config[target.topLevelKey]) ?? {};
2337
+ // BAPI-807: resolve WHICH key this config's registration lives under before
2338
+ // writing. A legacy-only entry is replaced under `bridge-api`, a canonical-only
2339
+ // entry under `bridge`, and an absent one is created under MCP_SERVER_NAME.
2340
+ const resolution = resolveRegistrationKey(topLevel);
2341
+ if (resolution.state === "conflict") {
2342
+ // Unreachable: the pre-scan above returned before any write when it found a
2343
+ // conflict. Re-checked rather than asserted, so a future caller that skips
2344
+ // the pre-scan still cannot write over an ambiguous registration.
2345
+ skipped.push({ relPath: target.relPath, reason: "invalid" });
2346
+ continue;
2347
+ }
2348
+ const registrationKey = resolution.state === "absent" ? resolution.writeKey : resolution.key;
2238
2349
  // BAPI-714 (Group C): a PRE-EXISTING Bridge entry keeps its own launcher args
2239
2350
  // composition — only the package-spec token is refreshed — so the installer
2240
2351
  // never migrates a legacy bare launcher to `serve`. A newly created entry gets
2241
2352
  // the full template args, `serve` included. The rule is identical for the
2242
2353
  // real-key and secret-free variants: credential safety must not decide whether
2243
2354
  // a launcher is migrated.
2244
- topLevel["bridge-api"] = preserveExistingLauncherArgs(topLevel["bridge-api"], entry);
2355
+ topLevel[registrationKey] = preserveExistingLauncherArgs(topLevel[registrationKey], entry);
2245
2356
  config[target.topLevelKey] = topLevel;
2246
2357
  await deps.mkdir(path.dirname(fullPath), { recursive: true });
2247
2358
  await deps.writeFile(fullPath, JSON.stringify(config, null, 2) + "\n", {
2248
2359
  encoding: "utf-8",
2249
2360
  });
2250
- written.push({ relPath: target.relPath, mode });
2361
+ written.push({ relPath: target.relPath, mode, registrationKey });
2251
2362
  }
2252
- return { written, skipped };
2363
+ return { written, skipped, duplicateRegistrations: [] };
2253
2364
  }
2254
2365
  /**
2255
2366
  * Provision the selected GLOBAL (Codex, Copilot CLI) and MANUAL (Windsurf)
@@ -2266,6 +2377,7 @@ async function writeHostConfigs(deps, targets, entries, trackedState, ctx) {
2266
2377
  */
2267
2378
  async function provisionSelectedGlobalTargets(deps, platforms, entry, needKey) {
2268
2379
  const logLines = [];
2380
+ const duplicateRegistrations = [];
2269
2381
  let anyManualRequired = false;
2270
2382
  const provisionDeps = {
2271
2383
  fs: {
@@ -2286,7 +2398,7 @@ async function provisionSelectedGlobalTargets(deps, platforms, entry, needKey) {
2286
2398
  continue;
2287
2399
  const target = MCP_HOST_TARGETS[id];
2288
2400
  // Skip project JSON targets — those are handled by writeHostConfigs.
2289
- if (target.scope === "project" && target.format === "json")
2401
+ if (target.scope === "project" && hostAdapterForTarget(target).format === "json")
2290
2402
  continue;
2291
2403
  const outcome = await provisionHostTarget(target, entry, provisionDeps);
2292
2404
  switch (outcome.status) {
@@ -2297,12 +2409,19 @@ async function provisionSelectedGlobalTargets(deps, platforms, entry, needKey) {
2297
2409
  break;
2298
2410
  case "manual-required":
2299
2411
  anyManualRequired = true;
2300
- logLines.push(` ${target.label}: add the bridge-api MCP server manually to ${outcome.displayPath} ` +
2412
+ logLines.push(` ${target.label}: add the ${MCP_SERVER_NAME} MCP server manually to ${outcome.displayPath} ` +
2301
2413
  "(the API key is redacted in printed instructions).");
2302
2414
  break;
2303
2415
  case "skipped-invalid":
2304
2416
  logLines.push(` ${target.label}: skipped ${outcome.displayPath} — existing config is not valid; left untouched.`);
2305
2417
  break;
2418
+ case "skipped-duplicate-registration":
2419
+ // BAPI-807: NOT an ordinary vendor-fallback condition. The target was
2420
+ // left byte-identical and the caller must fail loudly rather than report
2421
+ // a configured host.
2422
+ duplicateRegistrations.push(target.displayPath);
2423
+ logLines.push(` ${target.label}: skipped ${outcome.displayPath} — ${outcome.detail}`);
2424
+ break;
2306
2425
  case "failed":
2307
2426
  logLines.push(` ${target.label}: could not be configured automatically; configure it manually.`);
2308
2427
  break;
@@ -2311,7 +2430,7 @@ async function provisionSelectedGlobalTargets(deps, platforms, entry, needKey) {
2311
2430
  if (needKey && anyManualRequired) {
2312
2431
  logLines.push(` ${formatNeedKeyCredentialStoreLine(needKey.repoName, needKey.credentialStorePath)}`);
2313
2432
  }
2314
- return logLines;
2433
+ return { logLines, duplicateRegistrations };
2315
2434
  }
2316
2435
  /** Build the `/jira/ping` URL exactly like the MCP `ping` tool / buildGetUrl. */
2317
2436
  export function buildPingUrl(baseUrl, repoName) {
@@ -2842,7 +2961,7 @@ export const INSTALL_BRIDGE_SIGNUP_UPGRADE_REQUIRED = [
2842
2961
  "Signup now verifies your email address with a code before creating a workspace. Upgrade",
2843
2962
  "and re-run:",
2844
2963
  "",
2845
- " npx -y @bridge_gpt/mcp-server@latest install --email <addr>",
2964
+ ` npx -y ${MCP_PACKAGE_NAME}@latest install --email <addr>`,
2846
2965
  ].join("\n");
2847
2966
  /**
2848
2967
  * Mask an address for display: first character, then the domain.
@@ -3619,11 +3738,11 @@ function buildManualHostInstructions(entry, editors, needKey) {
3619
3738
  "Add the server manually (replace <REDACTED> with your key):",
3620
3739
  ];
3621
3740
  if (editors.windsurf) {
3622
- const windsurfSnippet = JSON.stringify({ mcpServers: { "bridge-api": { command: entry.command, args: entry.args, env: redactedEnv } } }, null, 2);
3741
+ const windsurfSnippet = JSON.stringify({ mcpServers: { [MCP_SERVER_NAME]: { command: entry.command, args: entry.args, env: redactedEnv } } }, null, 2);
3623
3742
  lines.push("", " Windsurf → ~/.codeium/windsurf/mcp_config.json:", windsurfSnippet);
3624
3743
  }
3625
3744
  if (editors.codex) {
3626
- lines.push("", " Codex → ~/.codex/config.toml (add an [mcp_servers.bridge-api] table with the", " same command/args/env shown above, BAPI_API_KEY set to your key).");
3745
+ lines.push("", ` Codex → ~/.codex/config.toml (add an [mcp_servers.${MCP_SERVER_NAME}] table with the`, " same command/args/env shown above, BAPI_API_KEY set to your key).");
3627
3746
  }
3628
3747
  if (needKey) {
3629
3748
  lines.push("", ` ${formatNeedKeyCredentialStoreLine(needKey.repoName, needKey.credentialStorePath)}`);
@@ -3638,7 +3757,7 @@ function buildManualHostInstructions(entry, editors, needKey) {
3638
3757
  * ONCE and reused by the Step 1–5 catch, the `index.ts` dispatch catch, the
3639
3758
  * tests, and the feature documentation, so the command text cannot drift.
3640
3759
  */
3641
- export const INSTALL_BRIDGE_DOCTOR_COMMAND = "npx -y @bridge_gpt/mcp-server doctor";
3760
+ export const INSTALL_BRIDGE_DOCTOR_COMMAND = `npx -y ${MCP_PACKAGE_NAME} doctor`;
3642
3761
  /**
3643
3762
  * The canonical pointer LINE (BAPI-669, U9a). One syntax — no quotes, no alternate
3644
3763
  * punctuation — shared by the Step 1–5 catch and by every operational fatal in the
@@ -3980,6 +4099,9 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
3980
4099
  // seams above are: a test that overrides only `fetch` or only `homedir`
3981
4100
  // must see its override here, not a stale default captured earlier.
3982
4101
  executorProvisioning: buildConductorExecutorDeps(deps, conductorBaseUrl),
4102
+ // Built from the FINAL merged deps for the same reason (BAPI-775): a test
4103
+ // that overrides only `readFile` or only `cwd` must see its override here.
4104
+ localProvisioning: buildConductorLocalDeps(deps),
3983
4105
  };
3984
4106
  return runInstallBridgeConductorCli(argv.slice(1), conductorDeps);
3985
4107
  }
@@ -5072,6 +5194,18 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
5072
5194
  trackedState.set(target.relPath, await isTrackedProjectConfig(deps, target.relPath));
5073
5195
  }
5074
5196
  const writeResult = await writeHostConfigs(deps, targets, { real: entry, secretFree: secretFreeEntry }, trackedState, { repoName, credentialStorePath });
5197
+ // BAPI-807: a both-key project config is a FAIL-LOUD condition, not an
5198
+ // ordinary skip. Nothing was written (the pre-write scan returned before the
5199
+ // first target), and continuing would report a successful install over a
5200
+ // registration Bridge deliberately refused to touch.
5201
+ if (writeResult.duplicateRegistrations.length > 0) {
5202
+ errorLog(`Error: ${DUPLICATE_REGISTRATION_GUIDANCE}`);
5203
+ for (const relPath of writeResult.duplicateRegistrations) {
5204
+ errorLog(` ${relPath}`);
5205
+ }
5206
+ errorLog(" Nothing was written. Re-run install-bridge once one entry is removed.");
5207
+ return 1;
5208
+ }
5075
5209
  // BAPI-708 (B-a): these files are the MCP SERVER REGISTRATION (BAPI_REPO_NAME,
5076
5210
  // BAPI_BASE_URL, BAPI_DOCS_DIR, BAPI_PROJECT_ROOT, and the version-pinned npx
5077
5211
  // launcher) — calling them a credentials file was simply inaccurate. Where the
@@ -5094,9 +5228,20 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
5094
5228
  const needKeyManual = bootstrapInviteMode
5095
5229
  ? { repoName, credentialStorePath }
5096
5230
  : undefined;
5097
- const globalLogLines = await provisionSelectedGlobalTargets(deps, selectedPlatforms, entry, needKeyManual);
5098
- for (const line of globalLogLines)
5231
+ const globalProvisioning = await provisionSelectedGlobalTargets(deps, selectedPlatforms, entry, needKeyManual);
5232
+ for (const line of globalProvisioning.logLines)
5099
5233
  log(line);
5234
+ // BAPI-807: a global target that refused because it already carries both
5235
+ // registrations is fail-loud, exactly like the project-config case above —
5236
+ // never an ordinary vendor fallback the run can shrug off.
5237
+ if (globalProvisioning.duplicateRegistrations.length > 0) {
5238
+ errorLog(`Error: ${DUPLICATE_REGISTRATION_GUIDANCE}`);
5239
+ for (const displayPath of globalProvisioning.duplicateRegistrations) {
5240
+ errorLog(` ${displayPath}`);
5241
+ }
5242
+ errorLog(" That host was left untouched. Re-run install-bridge once one entry is removed.");
5243
+ return 1;
5244
+ }
5100
5245
  // Legacy manual-editor instructions cover editors that are DETECTED but were
5101
5246
  // NOT part of the selection (so the emitter above did not handle them). The two
5102
5247
  // editors are suppressed INDEPENDENTLY: Codex is dropped when it was selected
@@ -5187,7 +5332,7 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
5187
5332
  `start-tickets model routing may not resolve the key for bapi:${repoName} ` +
5188
5333
  "and will fail open to the premium/Opus tier (the most expensive) — " +
5189
5334
  "set BAPI_API_KEY in the shell or re-run install, then verify with " +
5190
- "'npx -y @bridge_gpt/mcp-server doctor'.");
5335
+ `'npx -y ${MCP_PACKAGE_NAME} doctor'.`);
5191
5336
  }
5192
5337
  }
5193
5338
  catch {
@@ -5196,7 +5341,7 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
5196
5341
  log(" warning: could not persist the routing credential (unexpected error). " +
5197
5342
  "start-tickets model routing may need BAPI_API_KEY in the shell and will fail open " +
5198
5343
  "to the premium/Opus tier (the most expensive) until fixed — verify with " +
5199
- "'npx -y @bridge_gpt/mcp-server doctor'.");
5344
+ `'npx -y ${MCP_PACKAGE_NAME} doctor'.`);
5200
5345
  }
5201
5346
  }
5202
5347
  // ---- commit-your-assets notice (BAPI-708, Part C) ----
@@ -5280,7 +5425,7 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
5280
5425
  "server provides.");
5281
5426
  // The trust dialog is the single most common place a spawned session stalls:
5282
5427
  // the tab opens, nothing is approved, and the install looks hung.
5283
- log(`In the new tab: approve the workspace and the 'bridge-api' MCP server if ${handoffToolLabel} ` +
5428
+ log(`In the new tab: approve the workspace and the '${MCP_SERVER_NAME}' MCP server if ${handoffToolLabel} ` +
5284
5429
  "asks — configuration can't proceed until you do.");
5285
5430
  // Recovery was previously printed ONLY on no-spawn paths, so a tab that opened
5286
5431
  // and then died left the user with no instruction at all.
@@ -5304,6 +5449,25 @@ export async function runInstallBridgeCli(argv, overrides = {}) {
5304
5449
  return 0;
5305
5450
  }
5306
5451
  catch (error) {
5452
+ // BAPI-807: a launcher-config PREFLIGHT refusal is actionable, and the
5453
+ // operator must be told what to do — collapsing it into the generic
5454
+ // "unexpected error" cause is how a duplicate registration would look like
5455
+ // a bug in Bridge instead of two entries in their own config.
5456
+ //
5457
+ // This is safe under the module's secret-suppression discipline because
5458
+ // `InitPreflightError` is a TYPE this codebase owns, and its payload is a
5459
+ // closed set: a repo-relative config path plus a reason string drawn from
5460
+ // the fixed `REASON_TEXT` table. No caught free-form message, no config
5461
+ // body, and no env value is read here.
5462
+ if (error instanceof InitPreflightError) {
5463
+ errorLog("Error: these MCP launcher configs cannot be safely reconciled:");
5464
+ for (const failure of error.failures) {
5465
+ errorLog(` ${failure.relPath}: ${failure.reason}`);
5466
+ }
5467
+ errorLog(" Nothing was changed. Resolve these by hand, then re-run install-bridge.");
5468
+ errorLog(INSTALL_BRIDGE_DOCTOR_POINTER);
5469
+ return 1;
5470
+ }
5307
5471
  // Resume guidance ONLY after the exchange actually succeeded (R5-2): before
5308
5472
  // that the invite is unspent and there is nothing to resume. The wording comes
5309
5473
  // from the mode-aware helper, never a fresh literal, so a self-serve user is
@@ -22,6 +22,8 @@
22
22
  import path from "path";
23
23
  import { resolveBapiCredentials } from "./credential-store.js";
24
24
  import { detectClaudeLogin, formatClaudeLoginAdvisory } from "./claude-login.js";
25
+ import { getProjectJsonTargets, hostAdapterForTarget } from "./mcp-host-targets.js";
26
+ import { existingRegistrationKey, MCP_PACKAGE_NAME, } from "./mcp-identity.js";
25
27
  /** Default production base URL (mirrors install-bridge's DEFAULT_BAPI_BASE_URL). */
26
28
  const DEFAULT_BASE_URL = "https://bridgegpt-api.com";
27
29
  /**
@@ -36,12 +38,15 @@ const DEFAULT_BASE_URL = "https://bridgegpt-api.com";
36
38
  function buildSetupUrl(baseUrl) {
37
39
  return `${baseUrl.replace(/\/+$/, "")}/setup`;
38
40
  }
39
- /** Project-local MCP configs whose env blocks may carry repo/base-URL values. */
40
- const MCP_CONFIG_ENV_TARGETS = [
41
- { relPath: ".mcp.json", topLevelKey: "mcpServers" },
42
- { relPath: ".cursor/mcp.json", topLevelKey: "mcpServers" },
43
- { relPath: ".vscode/mcp.json", topLevelKey: "servers" },
44
- ];
41
+ /**
42
+ * Project-local MCP configs whose env blocks may carry repo/base-URL values.
43
+ * Derived from the shared host-target registry's project-scoped JSON targets
44
+ * rather than duplicating each path/root-key pair here.
45
+ */
46
+ const MCP_CONFIG_ENV_TARGETS = getProjectJsonTargets().map((target) => ({
47
+ relPath: target.relPath,
48
+ topLevelKey: hostAdapterForTarget(target).topLevelKey,
49
+ }));
45
50
  /** Timeout for each read-only probe request. */
46
51
  const PROBE_TIMEOUT_MS = 5_000;
47
52
  /**
@@ -72,9 +77,18 @@ export async function resolveInstallDoctorTarget(deps) {
72
77
  catch {
73
78
  continue;
74
79
  }
75
- const envBlock = parsed && typeof parsed === "object"
80
+ // BAPI-807: read the env block through the RESOLVED registration key. A
81
+ // config written before the rename registers `bridge-api`, and looking
82
+ // only under the canonical name would report a perfectly working install
83
+ // as having no repo/base-URL configured at all.
84
+ const servers = parsed && typeof parsed === "object"
85
+ ? // eslint-disable-next-line @typescript-eslint/no-explicit-any
86
+ parsed[topLevelKey]
87
+ : undefined;
88
+ const registrationKey = existingRegistrationKey(servers);
89
+ const envBlock = registrationKey !== null
76
90
  ? // eslint-disable-next-line @typescript-eslint/no-explicit-any
77
- parsed[topLevelKey]?.["bridge-api"]?.env
91
+ servers[registrationKey]?.env
78
92
  : undefined;
79
93
  if (!envBlock)
80
94
  continue;
@@ -410,7 +424,7 @@ export async function collectInstallStatusChecks(deps) {
410
424
  label: "GitHub connection",
411
425
  status: "WARN",
412
426
  detail: "no GitHub repository is connected to this project",
413
- remediation: `run 'npx -y @bridge_gpt/mcp-server@latest connect-github --repo ${target.repoName}'.`,
427
+ remediation: `run 'npx -y ${MCP_PACKAGE_NAME}@latest connect-github --repo ${target.repoName}'.`,
414
428
  });
415
429
  }
416
430
  }
@@ -40,6 +40,7 @@ import { VERSION } from "./version.generated.js";
40
40
  import { isNewerVersion } from "./update-check.js";
41
41
  import { fetchLatestVersion } from "./cli-release.js";
42
42
  import { runInstallBridgeCli } from "./install-bridge.js";
43
+ import { MCP_PACKAGE_NAME } from "./mcp-identity.js";
43
44
  /** The sentinel that marks an already-re-exec'd child. Matches `upgrade-cli.ts`. */
44
45
  export const INSTALL_REEXEC_SENTINEL = "--internal-reexec";
45
46
  /**
@@ -189,7 +190,7 @@ export async function runInstallBridgeWithLatestCli(argv, deps = {}) {
189
190
  const npxCmd = platform === "win32" ? "npx.cmd" : "npx";
190
191
  const childArgs = [
191
192
  "-y",
192
- "@bridge_gpt/mcp-server@latest",
193
+ `${MCP_PACKAGE_NAME}@latest`,
193
194
  "install",
194
195
  INSTALL_REEXEC_SENTINEL,
195
196
  ...forwardedArgs,