@bridge_gpt/mcp-server 0.2.41 → 0.2.43

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/README.md +330 -191
  2. package/build/agent-capabilities/cli.js +2 -1
  3. package/build/agent-launchers/claude-executor-adapter.js +17 -4
  4. package/build/agents.generated.js +2 -2
  5. package/build/claude-review-workflow.js +510 -45
  6. package/build/claude-user-config-doctor.js +42 -11
  7. package/build/cli-release.js +2 -1
  8. package/build/commands.generated.js +6 -5
  9. package/build/conduct-epic/bridge-client.js +354 -113
  10. package/build/conduct-epic/checkpoint-store.js +17 -0
  11. package/build/conduct-epic/cli.js +947 -99
  12. package/build/conduct-epic/cut-protocol.js +327 -0
  13. package/build/conduct-epic/spawn.js +14 -2
  14. package/build/conductor/bridge-api-client.js +148 -1
  15. package/build/conductor/cli.js +109 -1
  16. package/build/conductor/doctor.js +101 -16
  17. package/build/conductor/epic-reconcile.js +72 -19
  18. package/build/conductor/epic-runtime.js +15 -3
  19. package/build/conductor/errors.js +47 -0
  20. package/build/conductor/git-hooks.js +205 -11
  21. package/build/conductor/install-doctor.js +230 -1
  22. package/build/conductor/local-merge.js +130 -28
  23. package/build/conductor/recovery-cli.js +313 -0
  24. package/build/conductor/recovery-operations.js +219 -0
  25. package/build/conductor/tools.js +32 -3
  26. package/build/conductor/worker-ledger-cli.js +27 -1
  27. package/build/conductor-bin.js +20 -16
  28. package/build/credentials-cli.js +3 -2
  29. package/build/docs.generated.js +2 -1
  30. package/build/doctor.js +120 -44
  31. package/build/drive-epic.js +375 -0
  32. package/build/executor/cli.js +48 -1
  33. package/build/executor/env.js +21 -0
  34. package/build/executor/http-client.js +71 -3
  35. package/build/executor/index-scope.js +39 -0
  36. package/build/executor/job-errors.js +9 -0
  37. package/build/executor/job-log-registry.js +69 -0
  38. package/build/executor/job-runner.js +198 -29
  39. package/build/executor/live-worker-registry.js +83 -0
  40. package/build/executor/observation.js +259 -6
  41. package/build/executor/platform.js +147 -3
  42. package/build/executor/process.js +58 -14
  43. package/build/executor/runner.js +454 -48
  44. package/build/executor/test-clock.js +3 -2
  45. package/build/executor/worker-finalization.js +233 -56
  46. package/build/executor/worktree.js +8 -1
  47. package/build/index-scope-contract.js +96 -0
  48. package/build/index.js +2277 -270
  49. package/build/init.js +83 -22
  50. package/build/install-bridge-conductor.js +323 -14
  51. package/build/install-bridge.js +225 -47
  52. package/build/install-doctor.js +23 -9
  53. package/build/install-reexec.js +2 -1
  54. package/build/launcher-config-inspection.js +83 -22
  55. package/build/mcp-host-config.js +331 -67
  56. package/build/mcp-host-targets.js +45 -21
  57. package/build/mcp-identity.js +92 -0
  58. package/build/mcp-install-state.js +94 -1
  59. package/build/mcp-invoke.js +2 -1
  60. package/build/mcp-provisioning.js +45 -12
  61. package/build/mcp-registration-doctor.js +35 -13
  62. package/build/mcp-server-invocation.js +4 -2
  63. package/build/merge-pull-request.js +208 -9
  64. package/build/pipelines.generated.js +305 -15
  65. package/build/plane/cli.js +73 -7
  66. package/build/plane/defaults.js +18 -5
  67. package/build/plane/manifest.js +90 -0
  68. package/build/plane/preflight.js +100 -10
  69. package/build/plane/shutdown.js +71 -3
  70. package/build/plane/test-fakes.js +9 -1
  71. package/build/readme.generated.js +1 -1
  72. package/build/regression-check.js +3 -2
  73. package/build/review-tickets.js +8 -7
  74. package/build/run-unit-tests-launcher.js +149 -6
  75. package/build/schedule-run.js +3 -2
  76. package/build/setup-epic.js +531 -82
  77. package/build/sfcc/tool-wrapper.js +15 -0
  78. package/build/start-tickets-prereqs.js +11 -6
  79. package/build/start-tickets.js +91 -85
  80. package/build/update-check.js +3 -2
  81. package/build/upgrade-advice.js +2 -1
  82. package/build/upgrade-cli.js +50 -18
  83. package/build/version.generated.js +2 -1
  84. package/build/worktree-core.js +31 -17
  85. package/docs/CONDUCTOR.md +22 -0
  86. package/docs/install/mcp-tool-integrations.md +19 -3
  87. package/package.json +2 -2
  88. package/pipelines/greenfield-setup.json +286 -0
@@ -368,6 +368,21 @@ function notConfigured(failureClass, message) {
368
368
  */
369
369
  export function withSfccGate(deps, handler) {
370
370
  return async (args, target) => {
371
+ // (0) Refuse a handler that was registered WITHOUT the target boundary.
372
+ //
373
+ // `withSfccGate` returns a two-arity handler and `register.ts` is the only
374
+ // thing that supplies the second argument, via `withSfccTargetBoundary`. A
375
+ // caller that skips that wrapper reaches (c) below with `target` undefined
376
+ // and throws a bare `TypeError`, which the read-path error formatter then
377
+ // masks into an opaque, deliberately detail-free INTERNAL_ERROR 500 — a
378
+ // registration defect wearing the costume of an upstream failure. Refusing
379
+ // here names it instead, and does so BEFORE the version fetch so a
380
+ // misregistered tool can never cause a network call.
381
+ if (target === undefined || target === null) {
382
+ return notConfigured("target-not-supplied", "This SFCC tool was invoked without a resolved target, which means it was " +
383
+ "registered without withSfccTargetBoundary. Refusing rather than treating " +
384
+ "an unresolved target as approved.");
385
+ }
371
386
  // (a) Read /config-field/version
372
387
  const version = await getSfccVersionConfig(deps.buildGetUrl, deps.getGetHeaders, deps.repoName);
373
388
  if (version === null) {
@@ -18,7 +18,8 @@ import { resolveBapiCredentials, getPrimaryCredentialStorePath, } from "./creden
18
18
  import { resolveStartTicketsRepoName } from "./start-tickets-repo.js";
19
19
  import { probeWorktreeMcpRegistration } from "./mcp-registration-doctor.js";
20
20
  import { probeWorktreeCommandAssets } from "./command-assets-doctor.js";
21
- import { CLAUDE_MCP_SHADOWING_REMEDIATION_COMMAND, formatClaudeMcpShadowFinding, formatClaudeUserConfigDiagnostic, inspectClaudeUserConfigForMcpShadowing, resolveClaudeUserConfigPath, } from "./claude-user-config-doctor.js";
21
+ import { MCP_SERVER_NAME, MCP_PACKAGE_NAME } from "./mcp-identity.js";
22
+ import { CLAUDE_MCP_SHADOWING_REMEDIATION_COMMAND, claudeMcpShadowingRemediationCommand, formatClaudeMcpShadowFinding, formatClaudeUserConfigDiagnostic, inspectClaudeUserConfigForMcpShadowing, resolveClaudeUserConfigPath, } from "./claude-user-config-doctor.js";
22
23
  // ---------------------------------------------------------------------------
23
24
  // Constants (moved here from start-tickets.ts so both consumers share them)
24
25
  // ---------------------------------------------------------------------------
@@ -42,7 +43,7 @@ export const TMUX_COMMAND = "tmux";
42
43
  /** Actionable hint emitted when Git Bash is missing on Windows. */
43
44
  export const GIT_FOR_WINDOWS_BASH_HINT = "Install Git for Windows / Git Bash — Worktrunk runs its pre-start / post-start hooks via Git Bash.";
44
45
  /** The read-only doctor invocation surfaced from preflight failures and elsewhere. */
45
- export const START_TICKETS_DOCTOR_COMMAND = "npx -y @bridge_gpt/mcp-server doctor";
46
+ export const START_TICKETS_DOCTOR_COMMAND = `npx -y ${MCP_PACKAGE_NAME} doctor`;
46
47
  /**
47
48
  * BAPI-527 live-source checkout guard (conductor durable execution).
48
49
  *
@@ -352,7 +353,7 @@ function ripgrepDescriptor() {
352
353
  const CREDENTIAL_RESOLUTION_HINT = "Rerun /install-bridge to persist the routing credential, set BAPI_API_KEY in the " +
353
354
  'environment, or add it under "bapi:<repo_name>" in ~/.config/bridge/credentials.json. ' +
354
355
  "To migrate a key that only lives in .mcp.json / .cursor/mcp.json, run: " +
355
- "npx -y @bridge_gpt/mcp-server credentials migrate-agent-config --write-credentials.";
356
+ `npx -y ${MCP_PACKAGE_NAME} credentials migrate-agent-config --write-credentials.`;
356
357
  const CREDENTIAL_RESOLUTION_INSTALL_HINTS = {
357
358
  darwin: CREDENTIAL_RESOLUTION_HINT,
358
359
  linux: CREDENTIAL_RESOLUTION_HINT,
@@ -531,7 +532,7 @@ export async function resolveMainWorktreePathForDoctor(deps) {
531
532
  * which is why the advice below is phrased as "delete, or move aside". Keep every
532
533
  * such identifier out of this file, even inside a comment like this one.
533
534
  */
534
- const CLAUDE_MCP_SHADOWING_HINT = `Run \`${CLAUDE_MCP_SHADOWING_REMEDIATION_COMMAND}\` to clear the 'bridge-api' MCP ` +
535
+ const CLAUDE_MCP_SHADOWING_HINT = `Run \`${CLAUDE_MCP_SHADOWING_REMEDIATION_COMMAND}\` to clear the '${MCP_SERVER_NAME}' MCP ` +
535
536
  "registration in ~/.claude.json (use the matching scope if it is registered at user scope, " +
536
537
  "or under the projects entry for this repository/worktree). This is ADVISORY: workers load " +
537
538
  "MCP servers with --strict-mcp-config from their own worktree registration, so this entry " +
@@ -598,15 +599,19 @@ export function claudeMcpShadowingDescriptor() {
598
599
  if (inspection.findings.length > 0) {
599
600
  // Advisory (BAPI-790): reported in full, with the remediation command,
600
601
  // but `found: true` so it never fails the prerequisite or the exit code.
602
+ // BAPI-807: the remediation names the key this finding was ACTUALLY
603
+ // found under. A fixed canonical command suggested against a legacy
604
+ // `bridge-api` registration silently succeeds as a no-op, which reads to
605
+ // the operator as "the advisory is wrong".
601
606
  const findings = inspection.findings.map((finding) => `advisory: ${formatClaudeMcpShadowFinding(finding)} — not loaded by a worker under ` +
602
- `strict MCP loading; clear it with \`${CLAUDE_MCP_SHADOWING_REMEDIATION_COMMAND}\``);
607
+ `strict MCP loading; clear it with \`${claudeMcpShadowingRemediationCommand(finding.registrationKey)}\``);
603
608
  return { found: true, detail: [...findings, ...degraded].join("; ") };
604
609
  }
605
610
  return {
606
611
  found: true,
607
612
  detail: degraded.length > 0
608
613
  ? degraded.join("; ")
609
- : "no foreign bridge-api registration in the Claude user config",
614
+ : `no foreign ${MCP_SERVER_NAME} registration in the Claude user config`,
610
615
  };
611
616
  },
612
617
  };
@@ -59,10 +59,7 @@ import os from "node:os";
59
59
  import path from "path";
60
60
  import { VERSION } from "./version.generated.js";
61
61
  import { resolveBapiCredentials, getPrimaryCredentialStorePath, } from "./credential-store.js";
62
- // BAPI-814/E1b: the read-only index-branch override probe. `getIndexBranch` is
63
- // reused from the conduct-epic client rather than re-wrapped, so both conductor
64
- // paths read the one repository-wide override through the same client.
65
- import { getIndexBranch } from "./conduct-epic/bridge-client.js";
62
+ import { MCP_PACKAGE_NAME } from "./mcp-identity.js";
66
63
  import { resolveStartTicketsRepoName as resolveSharedStartTicketsRepoName, resolveRequiredStartTicketsRepoName, } from "./start-tickets-repo.js";
67
64
  import { provisionMcpRegistrationsForCreatedWorktrees, } from "./mcp-provisioning.js";
68
65
  import { provisionCommandsForCreatedWorktrees, } from "./command-provisioning.js";
@@ -78,6 +75,7 @@ import { DEFAULT_AGENT_NAME, resolveAgentSpec, isAgentName, formatValidAgentName
78
75
  import { createStartTicketsConductorContext, provisionConductorHooksForRows, emitStartTicketsRunStarted, injectConductorEnvIntoShellCommand, buildSupervisorTabCommand, isSupervisorLaunchEnabled, supervisorSpawnKey, } from "./start-tickets-conductor.js";
79
76
  import { transitionEpicDispatch, resolveConductorBridgeApiAccess, } from "./conductor/bridge-api-client.js";
80
77
  import { PR_BASE_BRANCH_ENV_VAR, buildPrBaseContractLaunchInstruction, } from "./pr-base-contract.js";
78
+ import { INDEX_SCOPE_CONFIGURATION_ERROR, INDEX_SCOPE_ENV_VAR, validateOptionalIndexScope, } from "./index-scope-contract.js";
81
79
  // Re-export the shared prereq surface (constants, platform helpers, command
82
80
  // probes) so existing import sites that read them from "./start-tickets.js"
83
81
  // keep working unchanged.
@@ -121,7 +119,7 @@ export function appendSummaryRowWarning(row, warning) {
121
119
  export function getStartTicketsUsage() {
122
120
  return [
123
121
  "Usage:",
124
- " npx -y @bridge_gpt/mcp-server start-tickets [flags] KEY [KEY ...]",
122
+ ` npx -y ${MCP_PACKAGE_NAME} start-tickets [flags] KEY [KEY ...]`,
125
123
  "",
126
124
  "Flags:",
127
125
  " --agent claude|cursor-agent Agent command to launch in each worktree (default: claude)",
@@ -177,7 +175,6 @@ export function parseStartTicketsArgs(argv) {
177
175
  let maxParallelRaw;
178
176
  let agentName = DEFAULT_AGENT_NAME;
179
177
  let baseBranch = "main";
180
- let baseBranchExplicit = false;
181
178
  let conductorEnabled = false;
182
179
  let workflow = "implement";
183
180
  let reviewRoundsRaw;
@@ -347,7 +344,6 @@ export function parseStartTicketsArgs(argv) {
347
344
  return { status: "error", message: `Invalid --base-branch value: ${error}` };
348
345
  }
349
346
  baseBranch = trimmed;
350
- baseBranchExplicit = true;
351
347
  continue;
352
348
  }
353
349
  if (arg === "--dry-run") {
@@ -455,7 +451,6 @@ export function parseStartTicketsArgs(argv) {
455
451
  branchOverrides,
456
452
  agentName,
457
453
  baseBranch,
458
- ...(baseBranchExplicit ? { baseBranchExplicit: true } : {}),
459
454
  conductorEnabled,
460
455
  workflow,
461
456
  reviewRounds,
@@ -514,17 +509,19 @@ export function resolveStartTicketsPlatformConfig(deps, agent, autoApprove = fal
514
509
  resumeMode = false, workflow = "implement", reviewRounds,
515
510
  // BAPI-586: the effective run base branch (incl. epic.base_branch override).
516
511
  // Injected as BAPI_BASE_BRANCH so the spawned worker's PR targets it — for
517
- // conductor runs always, and (BAPI-801) for plain runs whose base is a
518
- // non-default branch — and threaded (BAPI-593) into the spawned workflow command.
512
+ // conductor runs always, and (BAPI-801/BAPI-847) for every plain run whose base
513
+ // resolved, `main` included — and threaded (BAPI-593) into the spawned workflow
514
+ // command.
519
515
  baseBranch) {
520
516
  if (!isSupportedStartTicketsPlatform(deps.platform)) {
521
517
  return { ok: false, error: unsupportedPlatformMessage(deps.platform) };
522
518
  }
523
519
  const platform = deps.platform;
524
- // BAPI-801: both conductor and plain dispatch carry the PR-base ENVIRONMENT
525
- // contract (see resolvePrBaseBranchEnvValue). Only the conductor launch
526
- // INSTRUCTION remains conductor-only a plain worker's prompt stays bare and
527
- // learns the base from the env plus the create-PR instruction files.
520
+ // BAPI-801/BAPI-847: both conductor and plain dispatch carry the PR-base
521
+ // ENVIRONMENT contract for EVERY resolved base, `main` included (see
522
+ // resolvePrBaseBranchEnvValue). Only the conductor launch INSTRUCTION remains
523
+ // conductor-only a plain worker's prompt stays bare and learns the base from
524
+ // the env plus the create-PR instruction files.
528
525
  const prBaseBranch = resolvePrBaseBranchEnvValue(conductorEnabled, baseBranch);
529
526
  return {
530
527
  ok: true,
@@ -566,26 +563,33 @@ export function prependRepoNameEnvAssignment(command, repoName, platform = "darw
566
563
  return `export BAPI_REPO_NAME='${shSquoteInner(repoName)}' && ${command}`;
567
564
  }
568
565
  /**
569
- * BAPI-801: decide the `BAPI_BASE_BRANCH` value a spawned worker's shell should
570
- * carry — the single rule shared by the real spawn builder
566
+ * BAPI-801/BAPI-847: decide the `BAPI_BASE_BRANCH` value a spawned worker's shell
567
+ * should carry — the single rule shared by the real spawn builder
571
568
  * ({@link resolveStartTicketsPlatformConfig}) and the dry-run preview builder
572
569
  * ({@link getDryRunPlatformDetails}), so the preview can never drift from what
573
570
  * is actually spawned.
574
571
  *
575
572
  * - Conductor runs export whatever base was resolved, `main` included — BAPI-586
576
573
  * behavior, unchanged.
577
- * - Plain runs export only a non-empty base other than the CLI default `main`.
578
- * This mirrors the `--base-branch` threading rule in {@link buildAgentPrompt},
579
- * keeps today's plain spawn strings byte-stable on the default base, and costs
580
- * nothing: the create-PR fallback tier is `main` anyway.
574
+ * - Plain runs export EVERY resolved base, `main` included (BAPI-847).
581
575
  *
582
- * Returns `null` when nothing should be exported;
576
+ * The plain rule used to omit `main` on the reasoning that "the create-PR
577
+ * fallback tier is `main` anyway". That reasoning was false, and it is the defect
578
+ * BAPI-847 removes: create-PR falls back to the `base_branch` CONFIG FIELD before
579
+ * it falls back to `main`, so a run that exported nothing resolved through
580
+ * whatever that repository-wide field happened to hold — `epic/<EPIC>` while an
581
+ * epic held the repository's index. An unrelated ticket's PR was opened against
582
+ * an epic branch exactly that way. Exporting the real base always makes a
583
+ * worker's PR target independent of repository-wide configuration.
584
+ *
585
+ * Returns `null` only when no base was resolved at all (unset, or blank after
586
+ * normalization) — a blank value is never converted into an exported branch name.
583
587
  * {@link prependBaseBranchEnvAssignment} is fail-open on that value.
584
588
  */
585
589
  function resolvePrBaseBranchEnvValue(conductorEnabled, baseBranch) {
586
590
  if (conductorEnabled)
587
591
  return baseBranch ?? null;
588
- if (!baseBranch || baseBranch === "main")
592
+ if (!baseBranch || !baseBranch.trim())
589
593
  return null;
590
594
  return baseBranch;
591
595
  }
@@ -1698,6 +1702,41 @@ export async function materializeWorkerLaunchCommand(deps, key, fullCommand) {
1698
1702
  // ---------------------------------------------------------------------------
1699
1703
  // Tab spawning across created worktrees
1700
1704
  // ---------------------------------------------------------------------------
1705
+ /**
1706
+ * Build the per-row environment injected into ONE worker's launch command
1707
+ * (BAPI-394 conductor identity + BAPI-844 index scope).
1708
+ *
1709
+ * Returns `undefined` when the row carries neither, so an ordinary worker's
1710
+ * command passes through {@link injectConductorEnvIntoShellCommand} byte-for-byte
1711
+ * unchanged — the pre-feature bare command, with no empty assignment and no
1712
+ * altered ordering.
1713
+ *
1714
+ * The scope is applied LAST so it is emitted from the row's own value and can
1715
+ * never be supplied by a shared conductor env object. Reuses the existing
1716
+ * injection path rather than adding a second shell-prefix mechanism: one escaper,
1717
+ * one platform rule, one place a quoting bug could live.
1718
+ */
1719
+ export function buildRowSpawnEnv(row) {
1720
+ if (!row.conductorEnv && !row.indexScope)
1721
+ return undefined;
1722
+ const env = { ...row.conductorEnv };
1723
+ if (row.indexScope)
1724
+ env[INDEX_SCOPE_ENV_VAR] = row.indexScope;
1725
+ return env;
1726
+ }
1727
+ /**
1728
+ * Stamp the run's validated index scope onto every SPAWNABLE row (BAPI-844).
1729
+ *
1730
+ * A no-op that returns the input array unchanged when the run declared no scope,
1731
+ * so an ordinary run's rows are the exact objects they were. Non-spawnable rows
1732
+ * (`create-failed`, `dry-run`, `skipped`) are never stamped: nothing launches for
1733
+ * them, and stamping would put a routing token into a summary row.
1734
+ */
1735
+ export function stampIndexScopeOnRows(rows, indexScope) {
1736
+ if (!indexScope)
1737
+ return rows;
1738
+ return rows.map((row) => row.status === "created" && row.path ? { ...row, indexScope } : row);
1739
+ }
1701
1740
  /**
1702
1741
  * Open one tab/session per successfully-created worktree, building each shell
1703
1742
  * command with the platform-correct `buildShellCommand` and passing per-ticket
@@ -1716,7 +1755,7 @@ export async function spawnTabsForCreatedWorktrees(deps, rows, terminal, buildSh
1716
1755
  // Scope the row's conductor identity env to this terminal/tab/session only
1717
1756
  // (BAPI-394). No-op when the row carries no conductorEnv (dry-run, non-Claude
1718
1757
  // agents, or conductor disabled). Never mutates process/global env.
1719
- const shellCommand = injectConductorEnvIntoShellCommand(deps.platform, baseShellCommand, row.conductorEnv);
1758
+ const shellCommand = injectConductorEnvIntoShellCommand(deps.platform, baseShellCommand, buildRowSpawnEnv(row));
1720
1759
  // Deliver the (potentially multi-KB) command via a launch-script file so the
1721
1760
  // terminal spawn payload stays tiny and escaping-immune. No-op when no writer
1722
1761
  // seam is configured; a failed write falls back inline only while the command
@@ -1764,10 +1803,10 @@ export function getDryRunPlatformDetails(agent, platform = process.platform, env
1764
1803
  // The builder accepts an optional resolved modelAlias; the dry-run caller
1765
1804
  // now passes the previewed tier's alias so `--model` shows in the preview.
1766
1805
  // The resolved repo name (when known) is injected as a BAPI_REPO_NAME prefix,
1767
- // and (BAPI-801) the PR base as a BAPI_BASE_BRANCH prefix through the SAME
1768
- // resolvePrBaseBranchEnvValue rule and the same outermost ordering the real
1769
- // spawn builder uses dry-run shares both environment renderings with real
1770
- // spawning. Reuses the same buildAgentShellCommand/buildAgentPrompt path as a
1806
+ // and (BAPI-801/BAPI-847) the PR base as a BAPI_BASE_BRANCH prefix through the
1807
+ // SAME resolvePrBaseBranchEnvValue rule including the ordinary `main` case
1808
+ // and the same outermost ordering the real spawn builder uses; dry-run shares
1809
+ // both environment renderings with real spawning. Reuses the same buildAgentShellCommand/buildAgentPrompt path as a
1771
1810
  // real spawn — dry-run is never special-cased — so the preview is exact.
1772
1811
  buildAgentShellCommand: (key, worktreePath, modelAlias) => prependBaseBranchEnvAssignment(prependRepoNameEnvAssignment(buildAgentShellCommand(agent, key, worktreePath, platform, autoApprove, modelAlias, conductorEnabled, false, workflow, reviewRounds, baseBranch), repoName, platform), resolvePrBaseBranchEnvValue(conductorEnabled, baseBranch), platform),
1773
1812
  };
@@ -1788,7 +1827,7 @@ export function buildDryRunMcpProvisioningLines(worktreePath, platform = process
1788
1827
  const invocation = mcpServerInvocation ?? {
1789
1828
  form: "npm-channel",
1790
1829
  command: "npx",
1791
- packageSpec: "@bridge_gpt/mcp-server@latest",
1830
+ packageSpec: `${MCP_PACKAGE_NAME}@latest`,
1792
1831
  };
1793
1832
  const built = buildMcpShimCommand(invocation, "<target>", worktreePath);
1794
1833
  const shim = `${built.command} ${built.args.join(" ")}`;
@@ -1811,11 +1850,12 @@ export function buildDryRunDetailLines(agent, key, branch, platform = process.pl
1811
1850
  const { worktrunkBinary, buildAgentShellCommand: build } = getDryRunPlatformDetails(agent, platform, env, autoApprove, conductorEnabled, repoName, workflow, reviewRounds, baseBranch);
1812
1851
  const wtArgs = buildWtSwitchArgs(branch, false, baseBranch);
1813
1852
  const agentInvocation = build(key, "<worktree-path>", modelAlias);
1814
- // BAPI-801: one concise secondary detail naming the PR base, emitted only when
1815
- // the shared rule actually exports BAPI_BASE_BRANCH. It sits AFTER the rendered
1816
- // spawn command so the copyable command stays the primary scan target, and it
1817
- // is ordinary routing detail never warning-like. An ordinary plain run on
1818
- // `main` exports nothing and so keeps its existing output density verbatim.
1853
+ // BAPI-801/BAPI-847: one concise secondary detail naming the PR base, emitted
1854
+ // whenever the shared rule exports BAPI_BASE_BRANCH which is now every run
1855
+ // with a resolved base, the ordinary `main` case included, because that is what
1856
+ // the real spawn exports. It sits AFTER the rendered spawn command so the
1857
+ // copyable command stays the primary scan target, and it is ordinary routing
1858
+ // detail — never warning-like.
1819
1859
  const prBaseBranch = resolvePrBaseBranchEnvValue(conductorEnabled, baseBranch);
1820
1860
  return [
1821
1861
  `DRY-RUN: ${key} -> branch=${branch}`,
@@ -2800,6 +2840,19 @@ async function defaultClaimEpicDispatch(dispatchKey, runId, deps) {
2800
2840
  });
2801
2841
  }
2802
2842
  export async function orchestrateStartTickets(deps, options, overrides = {}) {
2843
+ // BAPI-844: validate the declared index scope FIRST, before any side effect at
2844
+ // all — including the temp-dir prune below. A malformed declaration is a launch
2845
+ // configuration failure, not something to spawn canonical workers through: the
2846
+ // whole point of declaring a scope is that these workers must not research the
2847
+ // canonical index. The message is the shared fixed text, so the submitted value
2848
+ // never reaches the summary, a warning, or the operator's terminal.
2849
+ let indexScope;
2850
+ try {
2851
+ indexScope = validateOptionalIndexScope(options.epic?.index_scope_id);
2852
+ }
2853
+ catch {
2854
+ return { ok: false, error: INDEX_SCOPE_CONFIGURATION_ERROR };
2855
+ }
2803
2856
  // IH-2 (PR #552 review): best-effort prune of stale launch-script temp dirs
2804
2857
  // before doing anything else. Awaited (to avoid concurrent cleanup races) but
2805
2858
  // fully fail-open — it never throws, so a dry-run also benefits from cleanup
@@ -2988,57 +3041,13 @@ export async function orchestrateStartTickets(deps, options, overrides = {}) {
2988
3041
  if (overrides.modelRoutingWarningLog) {
2989
3042
  emitInvocationModelRoutingDiagnosticForRows(routed, overrides.modelRoutingWarningLog);
2990
3043
  }
2991
- const rows = await spawnTabsFn(deps, routed, terminal, platformConfig.config.buildAgentShellCommand);
3044
+ // BAPI-844: stamp the validated scope on the spawnable rows immediately before
3045
+ // the spawn, so the value the tab is launched with is the one this run
3046
+ // declared. A run with no scope gets the identical `routed` array back.
3047
+ const scoped = stampIndexScopeOnRows(routed, indexScope);
3048
+ const rows = await spawnTabsFn(deps, scoped, terminal, platformConfig.config.buildAgentShellCommand);
2992
3049
  return { ok: true, rows };
2993
3050
  }
2994
- /**
2995
- * Warn when an implicit base branch could be resolved through an active
2996
- * repository-wide index-branch override (BAPI-814/E1b).
2997
- *
2998
- * Informational only, and deliberately narrow:
2999
- *
3000
- * - **Only for an IMPLICIT base.** An explicit `--base-branch` is the caller's
3001
- * own decision and is unaffected by the override, so the probe is skipped
3002
- * entirely — no request is made and no warning is printed.
3003
- * - **Only with resolvable credentials.** `start-tickets` creates worktrees and
3004
- * opens tabs; it does not require Bridge access to do its job. "No credential"
3005
- * is therefore an ordinary state for this command, not a diagnostic, and
3006
- * staying silent keeps an unrelated command from growing a credential
3007
- * complaint. A lookup that FAILS after credentials resolved is different, and
3008
- * does produce one bounded, sanitized line.
3009
- * - **Never a refusal.** Nothing here changes the exit code, the base branch, or
3010
- * any spawn behavior.
3011
- *
3012
- * Warnings go to `errorLog` (stderr), so machine-readable stdout stays clean.
3013
- */
3014
- async function warnOnIndexOverrideForImplicitBase(deps, options, errorLog, fetchImpl) {
3015
- if (options.baseBranchExplicit)
3016
- return;
3017
- const access = await resolveConductorBridgeApiAccess({
3018
- env: deps.env,
3019
- cwd: deps.cwd,
3020
- homedir: os.homedir,
3021
- platform: deps.platform,
3022
- readFile: deps.readFile ?? ((filePath) => readFile(filePath, "utf-8")),
3023
- stat: (filePath) => stat(filePath).then((info) => ({ mode: info.mode })),
3024
- });
3025
- if (!access.ok)
3026
- return;
3027
- const result = await getIndexBranch(access.access, fetchImpl);
3028
- if (!result.ok) {
3029
- errorLog(`Warning: the repository's indexed-branch override could not be read: ${result.error}. ` +
3030
- "Continuing — ticket startup is unaffected.");
3031
- return;
3032
- }
3033
- const override = result.value.override;
3034
- if (override === null)
3035
- return;
3036
- errorLog(`Warning: ${access.access.repoName} has an ACTIVE index-branch override: ` +
3037
- `'${override.override_branch}'. No --base-branch was given, so the implicit base — ` +
3038
- "the base-branch config field and feature-branch provisioning — may resolve through " +
3039
- "that override rather than the repository's usual base. Ticket startup is ALLOWED and " +
3040
- "continues; pass --base-branch to choose explicitly.");
3041
- }
3042
3051
  /** Platform-specific guidance printed when one or more tabs fail to spawn. */
3043
3052
  function spawnFailureHintForPlatform(platform) {
3044
3053
  if (platform === "darwin") {
@@ -3094,9 +3103,6 @@ export async function runStartTicketsCli(argv, overrides = {}) {
3094
3103
  errorLog("Warning: --tier value was not one of cheap|basic|premium; ignoring the override and " +
3095
3104
  "using premium model routing for every ticket.");
3096
3105
  }
3097
- // BAPI-814/E1b: informational only, ahead of the dry-run branch so both a
3098
- // preview and a real run report the same repository-wide condition.
3099
- await warnOnIndexOverrideForImplicitBase(deps, options, errorLog, overrides.fetchImpl ?? globalThis.fetch);
3100
3106
  if (options.dryRun) {
3101
3107
  // Resolve the repo identity for the preview so the dry-run command matches
3102
3108
  // what the real spawn injects (see prependRepoNameEnvAssignment). The real
@@ -6,11 +6,12 @@ import { writeFile, mkdir, readFile } from "fs/promises";
6
6
  import path from "path";
7
7
  import os from "os";
8
8
  import { VERSION } from "./version.generated.js";
9
+ import { MCP_PACKAGE_NAME } from "./mcp-identity.js";
9
10
  const CACHE_TTL = 86400000; // 24 hours
10
11
  const FETCH_TIMEOUT = 3000;
11
- const REGISTRY_URL = "https://registry.npmjs.org/@bridge_gpt/mcp-server/latest";
12
+ const REGISTRY_URL = `https://registry.npmjs.org/${MCP_PACKAGE_NAME}/latest`;
12
13
  export function getCachePath() {
13
- return path.join(os.homedir(), ".config", "@bridge_gpt", "mcp-server", "update-check.json");
14
+ return path.join(os.homedir(), ".config", ...MCP_PACKAGE_NAME.split("/"), "update-check.json");
14
15
  }
15
16
  /**
16
17
  * Compare two semver strings. Returns true if `latest` is strictly greater.
@@ -12,6 +12,7 @@
12
12
  * caller can interpolate arbitrary (possibly secret-bearing) text into advice
13
13
  * that is surfaced to users and models.
14
14
  */
15
+ import { MCP_PACKAGE_NAME } from "./mcp-identity.js";
15
16
  /**
16
17
  * The canonical, always-fresh upgrade invocation.
17
18
  *
@@ -20,7 +21,7 @@
20
21
  * pin that upgrader then writes into each host config: the upgrader must be the
21
22
  * newest published code, while a project launcher must be reproducible.
22
23
  */
23
- export const CANONICAL_UPGRADE_COMMAND = "npx -y @bridge_gpt/mcp-server@latest --upgrade";
24
+ export const CANONICAL_UPGRADE_COMMAND = `npx -y ${MCP_PACKAGE_NAME}@latest --upgrade`;
24
25
  /** Accept only plain release strings into user-facing advice. */
25
26
  const SAFE_VERSION_RE = /^[0-9A-Za-z.\-+]{1,64}$/;
26
27
  /** Replace anything that is not a plausible version string with a fixed marker. */
@@ -43,6 +43,7 @@ import { CANONICAL_UPGRADE_COMMAND } from "./upgrade-advice.js";
43
43
  import { describeLauncherReason, inspectLauncherConfigs, isApplicableStandardLauncher, isPrerequisiteFailure, LAUNCHER_CONFIG_TARGETS, } from "./launcher-config-inspection.js";
44
44
  import { buildGenericAgentShellCommand, detectTerminal, getDefaultSpawnTerminalTabForPlatform, createDefaultStartTicketsDeps, } from "./start-tickets.js";
45
45
  import { AGENT_REGISTRY } from "./agent-registry.js";
46
+ import { MCP_PACKAGE_NAME } from "./mcp-identity.js";
46
47
  /** The sentinel this CLI passes to the child it re-execs from `@latest`. */
47
48
  const INTERNAL_REEXEC_FLAG = "--internal-reexec";
48
49
  // ---------------------------------------------------------------------------
@@ -53,7 +54,7 @@ function defaultReexec(cwd, oldVersion) {
53
54
  return new Promise((resolve) => {
54
55
  const child = spawn(npxCmd, [
55
56
  "-y",
56
- "@bridge_gpt/mcp-server@latest",
57
+ `${MCP_PACKAGE_NAME}@latest`,
57
58
  "upgrade",
58
59
  INTERNAL_REEXEC_FLAG,
59
60
  "--old-version",
@@ -67,7 +68,7 @@ function defaultReexec(cwd, oldVersion) {
67
68
  });
68
69
  }
69
70
  function localInstallPath(cwd) {
70
- return path.join(cwd, "node_modules", "@bridge_gpt", "mcp-server");
71
+ return path.join(cwd, "node_modules", ...MCP_PACKAGE_NAME.split("/"));
71
72
  }
72
73
  async function defaultHasCompetingLocalInstall(cwd) {
73
74
  try {
@@ -81,7 +82,7 @@ async function defaultHasCompetingLocalInstall(cwd) {
81
82
  function defaultRemoveCompetingLocalInstall(cwd) {
82
83
  const npmCmd = process.platform === "win32" ? "npm.cmd" : "npm";
83
84
  return new Promise((resolve) => {
84
- const child = spawn(npmCmd, ["uninstall", "@bridge_gpt/mcp-server"], {
85
+ const child = spawn(npmCmd, ["uninstall", MCP_PACKAGE_NAME], {
85
86
  stdio: "inherit",
86
87
  cwd,
87
88
  });
@@ -153,20 +154,33 @@ export async function resolveUpgradeTarget(argv, deps) {
153
154
  // ---------------------------------------------------------------------------
154
155
  // Reporting helpers (pure)
155
156
  // ---------------------------------------------------------------------------
156
- /** Render the dry-run preview line for one inspected target. */
157
+ /**
158
+ * Render the dry-run preview line for one inspected target.
159
+ *
160
+ * BAPI-807: an update against a legacy registration is described AS an update to
161
+ * that key — never as a rename. `upgrade` moves the package pin; it has no
162
+ * opinion about the registration name, and a preview implying otherwise would
163
+ * advertise a migration this command deliberately does not perform.
164
+ */
157
165
  export function formatDryRunLine(found) {
158
166
  const { relPath, targetVersion } = found;
167
+ // Suffix, not a rewrite of the line: the existing pin transitions are pinned
168
+ // by tests and the key is additional context, not a replacement for them.
169
+ const keyNote = found.legacyRegistration ? ` [\`${found.registrationKey}\` entry]` : "";
159
170
  switch (found.action) {
160
171
  case "repin":
161
- return ` ${relPath}: ${found.currentVersion} -> ${targetVersion}`;
172
+ return ` ${relPath}: ${found.currentVersion} -> ${targetVersion}${keyNote}`;
162
173
  case "already-current":
163
- return ` ${relPath}: already ${targetVersion}`;
174
+ return ` ${relPath}: already ${targetVersion}${keyNote}`;
164
175
  case "normalize-unpinned":
165
- return ` ${relPath}: would normalize ${found.spec} -> @bridge_gpt/mcp-server@${targetVersion}`;
176
+ return ` ${relPath}: would normalize ${found.spec} -> ${MCP_PACKAGE_NAME}@${targetVersion}${keyNote}`;
166
177
  case "add":
167
- return ` ${relPath}: would add @bridge_gpt/mcp-server@${targetVersion}`;
178
+ return ` ${relPath}: would add ${MCP_PACKAGE_NAME}@${targetVersion}`;
179
+ case "duplicate-registration":
180
+ // Reachable only in the dry-run preview: a real run refuses in preflight.
181
+ return ` ${relPath}: blocked — ${describeLauncherReason("duplicate-registration")}`;
168
182
  case "create":
169
- return ` ${relPath}: would create with @bridge_gpt/mcp-server@${targetVersion}`;
183
+ return ` ${relPath}: would create with ${MCP_PACKAGE_NAME}@${targetVersion}`;
170
184
  case "skip-worktree-shim":
171
185
  return ` ${relPath}: skipped — worktree mcp-invoke shim (no published-package pin applies)`;
172
186
  case "skip-inactive":
@@ -184,11 +198,11 @@ export function formatResultLine(action) {
184
198
  case "already-current":
185
199
  return ` ${relPath}: already ${action.toVersion}`;
186
200
  case "normalized":
187
- return ` ${relPath}: normalized ${action.fromSpec} -> @bridge_gpt/mcp-server@${action.toVersion}`;
201
+ return ` ${relPath}: normalized ${action.fromSpec} -> ${MCP_PACKAGE_NAME}@${action.toVersion}`;
188
202
  case "added":
189
- return ` ${relPath}: added @bridge_gpt/mcp-server@${action.toVersion}`;
203
+ return ` ${relPath}: added ${MCP_PACKAGE_NAME}@${action.toVersion}`;
190
204
  case "created":
191
- return ` ${relPath}: created with @bridge_gpt/mcp-server@${action.toVersion}`;
205
+ return ` ${relPath}: created with ${MCP_PACKAGE_NAME}@${action.toVersion}`;
192
206
  case "skipped-worktree-shim":
193
207
  return ` ${relPath}: skipped — worktree mcp-invoke shim preserved`;
194
208
  case "skipped-ahead":
@@ -278,8 +292,8 @@ export async function runUpgradeCli(argv, injected) {
278
292
  !isInternalReexec) {
279
293
  stdout(`- Would re-exec from @latest to upgrade the CLI ${VERSION} -> ${target.version}`);
280
294
  }
281
- stdout(`- Would detect and remove competing local install in node_modules/@bridge_gpt/mcp-server if present.`);
282
- stdout(`- Launcher pins (target @bridge_gpt/mcp-server@${target.version}):`);
295
+ stdout(`- Would detect and remove competing local install in node_modules/${MCP_PACKAGE_NAME} if present.`);
296
+ stdout(`- Launcher pins (target ${MCP_PACKAGE_NAME}@${target.version}):`);
283
297
  const lines = inspections
284
298
  .map(formatDryRunLine)
285
299
  .filter((l) => l !== null);
@@ -304,8 +318,8 @@ export async function runUpgradeCli(argv, injected) {
304
318
  if (!removed) {
305
319
  // Leaving it in place means npx keeps preferring the stale local copy, so
306
320
  // the repin below would be cosmetic. That is not a completed upgrade.
307
- stderr("Bridge API upgrade failed: could not remove node_modules/@bridge_gpt/mcp-server.");
308
- stderr(" Remove it manually (rm -rf node_modules/@bridge_gpt/mcp-server), then re-run the upgrade.");
321
+ stderr(`Bridge API upgrade failed: could not remove node_modules/${MCP_PACKAGE_NAME}.`);
322
+ stderr(` Remove it manually (rm -rf node_modules/${MCP_PACKAGE_NAME}), then re-run the upgrade.`);
309
323
  return 1;
310
324
  }
311
325
  stdout(" Removed competing local install.");
@@ -313,7 +327,7 @@ export async function runUpgradeCli(argv, injected) {
313
327
  // ---- Phase 5: reconcile ----
314
328
  let initResult;
315
329
  try {
316
- stdout(`\nReconciling launcher pins to @bridge_gpt/mcp-server@${target.version}...\n`);
330
+ stdout(`\nReconciling launcher pins to ${MCP_PACKAGE_NAME}@${target.version}...\n`);
317
331
  initResult = await deps.runInit(deps.cwd, {
318
332
  targetVersion: target.version,
319
333
  // Upgrade renders its own truthful per-config report below; the generic
@@ -367,10 +381,28 @@ export async function runUpgradeCli(argv, injected) {
367
381
  verificationFailures.push(`${action.relPath}: config disappeared after the write`);
368
382
  continue;
369
383
  }
384
+ // BAPI-807: a reconciliation that ADDED a canonical entry beside an existing
385
+ // legacy one leaves both keys present, which the re-inspection reports as
386
+ // `duplicate-registration`. Check it before the pin comparison — the pin
387
+ // question is meaningless once there are two entries, and this is the exact
388
+ // regression the resolve-then-write rule exists to prevent.
389
+ if (after.action === "duplicate-registration") {
390
+ verificationFailures.push(`${action.relPath}: reconciliation left TWO Bridge registrations in this config ` +
391
+ "(a canonical entry was written beside a legacy one)");
392
+ continue;
393
+ }
370
394
  if (after.action !== "already-current" || after.currentVersion !== target.version) {
371
- verificationFailures.push(`${action.relPath}: expected @bridge_gpt/mcp-server@${target.version} after the write, ` +
395
+ verificationFailures.push(`${action.relPath}: expected ${MCP_PACKAGE_NAME}@${target.version} after the write, ` +
372
396
  `found ${after.spec ?? "no recognizable package token"}`);
373
397
  }
398
+ // The pin landing is not sufficient: a legacy registration must still be
399
+ // under the SAME key it started under. `runInit` reports the key it actually
400
+ // wrote, so this compares two independent observations rather than trusting
401
+ // either one alone.
402
+ if (action.registrationKey && after.registrationKey !== action.registrationKey) {
403
+ verificationFailures.push(`${action.relPath}: expected the Bridge entry to remain under \`${action.registrationKey}\` ` +
404
+ `after the write, found ${after.registrationKey ? `\`${after.registrationKey}\`` : "no recognized registration"}`);
405
+ }
374
406
  }
375
407
  // ---- Phase 7: report what actually happened ----
376
408
  const resultLines = initResult.configActions
@@ -1,2 +1,3 @@
1
1
  // AUTO-GENERATED — do not edit manually. Regenerate with: npm run build
2
- export const VERSION = "0.2.41";
2
+ export const VERSION = "0.2.43";
3
+ export const BUILD_COMMIT = "1840a67a5a0d-dirty";