@bridge_gpt/mcp-server 0.2.67 → 0.2.69

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 (127) hide show
  1. package/README.md +272 -51
  2. package/build/agent-launchers/claude-executor-adapter.js +27 -0
  3. package/build/agents.generated.js +22 -2
  4. package/build/bitbucket-workspace-remote.js +136 -0
  5. package/build/bootstrap-project-identity.js +24 -0
  6. package/build/bridge-permissions.js +1362 -0
  7. package/build/check-update-cli.js +74 -0
  8. package/build/claude-settings.js +102 -2
  9. package/build/cli-help.js +27 -0
  10. package/build/codex-home.js +9 -1
  11. package/build/codex-launcher-pin.js +598 -0
  12. package/build/command-assets-doctor.js +24 -70
  13. package/build/command-provisioning.js +29 -5
  14. package/build/commands.generated.js +9 -9
  15. package/build/conductor/bridge-api-client.js +714 -0
  16. package/build/conductor/command-drift-production.js +92 -0
  17. package/build/conductor/command-drift.js +89 -0
  18. package/build/conductor/doctor.js +69 -26
  19. package/build/conductor/epic-branch-ci-inspection.js +1375 -0
  20. package/build/conductor/epic-implementer-cli.js +76 -22
  21. package/build/conductor/gate-discovery.js +890 -0
  22. package/build/conductor/install-doctor.js +15 -3
  23. package/build/conductor/json-output.js +63 -0
  24. package/build/conductor/readiness-cli.js +95 -10
  25. package/build/conductor/readiness-sections.js +369 -3
  26. package/build/conductor/readiness.js +211 -8
  27. package/build/conductor/recovery-cli.js +110 -4
  28. package/build/conductor/recovery-operations.js +99 -1
  29. package/build/connect-bitbucket-api.js +41 -6
  30. package/build/connect-bitbucket.js +106 -33
  31. package/build/connect-github-api.js +33 -14
  32. package/build/credential-store.js +207 -47
  33. package/build/docs.generated.js +57 -3
  34. package/build/doctor.js +198 -4
  35. package/build/drive-epic.js +45 -8
  36. package/build/epic-coherence-input.test-fixtures.js +4 -3
  37. package/build/epic-implementer-bin.js +1369 -25
  38. package/build/epic-workarounds-path.js +39 -0
  39. package/build/executor/cli.js +30 -0
  40. package/build/executor/completion-contract.js +20 -5
  41. package/build/executor/deps.js +4 -1
  42. package/build/executor/gate-qualification-job.js +499 -0
  43. package/build/executor/guarded-fast-forward-push.js +129 -0
  44. package/build/executor/http-client.js +43 -4
  45. package/build/executor/integration-job.js +122 -8
  46. package/build/executor/job-errors.js +8 -1
  47. package/build/executor/job-runner.js +589 -47
  48. package/build/executor/likely-tests.js +290 -0
  49. package/build/executor/live-worker-registry.js +1 -1
  50. package/build/executor/observation.js +31 -2
  51. package/build/executor/packaged-recipe.js +106 -0
  52. package/build/executor/permissions.js +115 -28
  53. package/build/executor/platform.js +1 -1
  54. package/build/executor/preflight.js +61 -12
  55. package/build/executor/process-containment.js +36 -12
  56. package/build/executor/process.js +15 -7
  57. package/build/executor/prompt-spec.js +33 -1
  58. package/build/executor/recently-failed-tests-file.js +71 -0
  59. package/build/executor/reconcile-overlap-job.js +41 -23
  60. package/build/executor/required-verification.js +1 -1
  61. package/build/executor/required-work-ledger.js +105 -17
  62. package/build/executor/retirement.js +69 -0
  63. package/build/executor/runner.js +114 -11
  64. package/build/executor/test-clock.js +15 -0
  65. package/build/executor/types.js +32 -0
  66. package/build/executor/viewer-tabs.js +58 -8
  67. package/build/executor/worker-command.js +23 -7
  68. package/build/executor/worker-diagnostics.js +37 -0
  69. package/build/executor/worker-finalization.js +31 -5
  70. package/build/executor/worker-guard-hook.js +179 -25
  71. package/build/executor/worker-log.js +4 -3
  72. package/build/executor/worker-verification-guidance.js +37 -0
  73. package/build/executor/worktree.js +88 -11
  74. package/build/first-build-advisory.js +97 -0
  75. package/build/generated-assets-advisory.js +14 -0
  76. package/build/generated-assets-catalog.js +66 -0
  77. package/build/generated-assets-context.js +46 -0
  78. package/build/generated-assets-dependencies.js +26 -0
  79. package/build/generated-assets-doctor.js +50 -0
  80. package/build/generated-assets-health.js +65 -0
  81. package/build/generated-assets-host.js +17 -0
  82. package/build/generated-assets-inventory.js +17 -0
  83. package/build/generated-assets-paths.js +60 -0
  84. package/build/generated-assets-publisher.js +133 -0
  85. package/build/generated-assets-remedy.js +25 -0
  86. package/build/generated-assets-repair.js +72 -0
  87. package/build/generated-assets-session.js +71 -0
  88. package/build/generated-assets-test-support.js +35 -0
  89. package/build/index.js +1109 -695
  90. package/build/init.js +54 -177
  91. package/build/install-bridge-conductor-gate.js +1372 -0
  92. package/build/install-bridge-conductor.js +289 -36
  93. package/build/install-bridge.js +722 -252
  94. package/build/launcher-config-inspection.js +184 -19
  95. package/build/mcp-host-targets.js +4 -0
  96. package/build/mcp-identity.js +7 -0
  97. package/build/mcp-install-state.js +50 -4
  98. package/build/merge-workflow-install.js +3 -4
  99. package/build/pipeline-utils.js +6 -2
  100. package/build/pipelines.generated.js +22 -142
  101. package/build/plan-execution-ledger.js +2 -32
  102. package/build/plane/manifest.js +16 -1
  103. package/build/plane/preflight.js +54 -1
  104. package/build/printed-remedy-commands.js +16 -1
  105. package/build/readme.generated.js +1 -1
  106. package/build/repair-assets.js +93 -0
  107. package/build/run-unit-tests-launcher.js +130 -49
  108. package/build/secret-redaction.js +33 -0
  109. package/build/session-upgrade-notice.js +86 -0
  110. package/build/setup-epic.js +108 -3
  111. package/build/start-tickets-prereqs.js +70 -16
  112. package/build/start-tickets.js +44 -5
  113. package/build/tool-call-advisory.js +162 -0
  114. package/build/tool-surface-gating.js +32 -57
  115. package/build/update-check.js +97 -16
  116. package/build/update-status.js +61 -71
  117. package/build/upgrade-advice.js +15 -11
  118. package/build/upgrade-cli.js +398 -17
  119. package/build/version.generated.js +3 -3
  120. package/build/worker-guard-hook-bin.js +3 -2
  121. package/build/worker-guard-hook-main.js +1 -1
  122. package/build/worktree-core.js +15 -2
  123. package/docs/EPIC-IMPLEMENTER-INSTALL.md +30 -12
  124. package/docs/EPIC-IMPLEMENTER.md +68 -2
  125. package/package.json +3 -3
  126. package/pipelines/greenfield-setup.json +4 -128
  127. package/pipelines/implement-ticket.json +3 -2
@@ -17,6 +17,7 @@ import path from "path";
17
17
  import { resolveBapiCredentials, getPrimaryCredentialStorePath, } from "./credential-store.js";
18
18
  import { resolveStartTicketsRepoName } from "./start-tickets-repo.js";
19
19
  import { probeWorktreeMcpRegistration } from "./mcp-registration-doctor.js";
20
+ import { VERSION } from "./version.generated.js";
20
21
  import { probeWorktreeCommandAssets } from "./command-assets-doctor.js";
21
22
  import { MCP_SERVER_NAME, MCP_PACKAGE_NAME } from "./mcp-identity.js";
22
23
  // BAPI-941: both are LEAF modules (the pure classifier and its git-backed
@@ -159,7 +160,7 @@ const WORKTRUNK_INSTALL_HINTS = {
159
160
  linux: "See the Worktrunk documentation for Linux install instructions: https://worktrunk.dev",
160
161
  };
161
162
  const GIT_INSTALL_HINTS = {
162
- darwin: "xcode-select --install (or brew install git)",
163
+ darwin: "xcode-select --install (or brew install git). Installing another Git does not ensure PATH selects it; verify with which git and git --version.",
163
164
  linux: "Install git with your distro package manager, e.g. apt install git",
164
165
  win32: "Install Git for Windows: https://git-scm.com/download/win",
165
166
  };
@@ -444,24 +445,19 @@ export function worktreeMcpReachabilityDescriptor() {
444
445
  },
445
446
  };
446
447
  }
447
- /**
448
- * Secret-free remediation hint shared by the worktree command-assets descriptor
449
- * (BAPI-664). Directs users to rerun start-tickets — the supported provisioning
450
- * workflow — rather than manually copying or committing the generated command
451
- * files, which are fill-only, never refreshed, and Git-excluded per worktree.
452
- */
453
- const WORKTREE_COMMAND_ASSETS_HINT = "Re-run start-tickets to provision the packaged Claude command assets " +
454
- "(.claude/commands/) into this worktree. Do not manually copy or commit the " +
455
- "generated command files — they are bootstrapped per worktree and Git-excluded.";
448
+ /** Version-pinned fill-only repair; permissions and unsafe paths need their own fix. */
449
+ const WORKTREE_COMMAND_ASSETS_HINT = `Run npx -y ${MCP_PACKAGE_NAME}@${VERSION} repair-assets --project-root <established-root> --host claude ` +
450
+ "to fill missing packaged files. Resolve permission/path/platform problems separately; refresh/reconnect and run doctor.";
451
+ const WORKTREE_COMMAND_ASSETS_PROVISION_HINT = "In-place asset repair is not supported on this platform. Re-run start-tickets to provision the packaged Claude command assets (.claude/commands/) into this worktree. Existing files are preserved; refresh/reconnect and run doctor.";
456
452
  const WORKTREE_COMMAND_ASSETS_INSTALL_HINTS = {
457
- darwin: WORKTREE_COMMAND_ASSETS_HINT,
453
+ darwin: WORKTREE_COMMAND_ASSETS_PROVISION_HINT,
458
454
  linux: WORKTREE_COMMAND_ASSETS_HINT,
459
- win32: WORKTREE_COMMAND_ASSETS_HINT,
455
+ win32: WORKTREE_COMMAND_ASSETS_PROVISION_HINT,
460
456
  };
461
457
  /**
462
458
  * Doctor-only, strictly read-only probe: does the current worktree contain every
463
459
  * packaged Claude slash-command asset under `.claude/commands/`? Delegates to the
464
- * read-only {@link probeWorktreeCommandAssets} using only `deps.readFile`,
460
+ * read-only {@link probeWorktreeCommandAssets} using only `deps.commandAssetMetadata`,
465
461
  * `deps.platform`, and `deps.cwd`. Reports an unavailable diagnostic (never a
466
462
  * false "present") when the optional read-only filesystem boundary is absent. The
467
463
  * probe performs NO writes, Git calls, or process spawns.
@@ -472,15 +468,14 @@ export function worktreeCommandAssetsDescriptor() {
472
468
  label: "Worktree Claude command assets",
473
469
  installHint: WORKTREE_COMMAND_ASSETS_INSTALL_HINTS,
474
470
  probe: async (deps) => {
475
- const { readFile } = deps;
476
- if (!readFile) {
471
+ if (!deps.commandAssetMetadata) {
477
472
  return {
478
473
  found: false,
479
474
  detail: "command-asset probe unavailable (no read-only filesystem access)",
480
475
  };
481
476
  }
482
477
  const result = await probeWorktreeCommandAssets(deps.cwd, {
483
- readFile,
478
+ metadata: deps.commandAssetMetadata,
484
479
  platform: deps.platform,
485
480
  });
486
481
  return { found: result.found, detail: result.detail };
@@ -817,6 +812,7 @@ export function getDoctorOnlyPrereqDescriptors(_platform, _env, agent) {
817
812
  return [
818
813
  uvDescriptor(),
819
814
  agentDescriptor(agent),
815
+ gitCompatibilityDescriptor(),
820
816
  credentialResolutionDescriptor(),
821
817
  worktreeMcpReachabilityDescriptor(),
822
818
  worktreeCommandAssetsDescriptor(),
@@ -863,6 +859,7 @@ export async function probePrerequisite(deps, descriptor) {
863
859
  label: descriptor.label,
864
860
  found: outcome.found,
865
861
  detail: outcome.detail,
862
+ gitCompatibility: outcome.gitCompatibility,
866
863
  installHint: hintForPlatform(descriptor.installHint, deps.platform),
867
864
  authNote: descriptor.authNote,
868
865
  // Deliberately NOT carried through when the probe THREW: the catch above
@@ -872,6 +869,63 @@ export async function probePrerequisite(deps, descriptor) {
872
869
  renderDetailWhenMissing: descriptor.renderDetailWhenMissing === true && !probeThrew ? true : undefined,
873
870
  };
874
871
  }
872
+ /** Release-specific advisory baseline; never a launcher admission requirement.
873
+ * Evidence: https://github.com/max-sixty/worktrunk/blob/v0.80.0/src/git/version.rs
874
+ */
875
+ export const WORKTRUNK_GIT_BASELINE = "Worktrunk v0.80.0 (Git >= 2.43.0)";
876
+ /** Read-only, advisory compatibility with the documented release, not every release. */
877
+ export async function probeGitWorktrunkCompatibility(deps) {
878
+ const basis = WORKTRUNK_GIT_BASELINE;
879
+ let executable;
880
+ let version;
881
+ const unknown = (reason) => ({ status: "unknown", executable, version, basis, reason });
882
+ try {
883
+ const locator = getCommandProbe("git", deps.platform);
884
+ const located = await deps.runCommand(locator.file, locator.args, { timeoutMs: 3000 });
885
+ if (located.unavailable)
886
+ return unknown("locator-unavailable");
887
+ if (!commandSucceeded(located))
888
+ return unknown(located.exitCode === 1 ? "git-missing" : "locator-unavailable");
889
+ const candidate = located.stdout.trim().split(/\r?\n/)[0];
890
+ // Only absolute paths from a successful locator are diagnostic metadata.
891
+ if (!candidate || candidate.length > 512 || /[\x00-\x1f\x7f]/.test(candidate) ||
892
+ !(deps.platform === "win32" ? path.win32.isAbsolute(candidate) : path.posix.isAbsolute(candidate))) {
893
+ return unknown("locator-unavailable");
894
+ }
895
+ executable = candidate;
896
+ const result = await deps.runCommand(executable, ["--version"], { timeoutMs: 3000 });
897
+ if (!commandSucceeded(result))
898
+ return unknown("version-unavailable");
899
+ const match = /^git version (\d{1,5})\.(\d{1,5})(?:\.(\d{1,5}))?(?:\.windows\.\d+| \(Apple Git-[\w.-]+\))?$/.exec(result.stdout.trim());
900
+ if (!match)
901
+ return unknown("version-unparseable");
902
+ const parts = [Number(match[1]), Number(match[2]), Number(match[3] ?? 0)];
903
+ version = parts.join(".");
904
+ if (deps.env[WORKTRUNK_BINARY_OVERRIDE_ENV]?.trim())
905
+ return unknown("override-unconfirmed");
906
+ const minimum = [2, 43, 0];
907
+ const differing = parts.findIndex((n, i) => n !== minimum[i]);
908
+ const compatible = differing < 0 || parts[differing] > minimum[differing];
909
+ return { status: compatible ? "compatible" : "incompatible", executable, version, basis };
910
+ }
911
+ catch {
912
+ return unknown(executable ? "version-unavailable" : "locator-unavailable");
913
+ }
914
+ }
915
+ /** Pure formatter shared by doctor and first-build output. No raw probe output. */
916
+ export function formatGitCompatibilityDiagnostic(result) {
917
+ const metadata = [result.executable, result.version && `Git ${result.version}`].filter(Boolean).join("; ");
918
+ return `Git compatibility advisory: ${result.status}${metadata ? ` — ${metadata}` : ""}; comparison: ${result.basis}` +
919
+ (result.reason ? `; ${result.reason}` : "") +
920
+ (result.status === "incompatible" ? ". Verify the Git executable/version selected on PATH and install a supported Git manually." : "") +
921
+ (result.reason === "override-unconfirmed" ? ". Custom Worktrunk release requirement is unconfirmed." : "");
922
+ }
923
+ export function gitCompatibilityDescriptor() {
924
+ return {
925
+ id: "git-compatibility", label: "Git compatibility advisory", installHint: GIT_INSTALL_HINTS,
926
+ probe: async (deps) => ({ found: true, gitCompatibility: await probeGitWorktrunkCompatibility(deps) }),
927
+ };
928
+ }
875
929
  /**
876
930
  * Enforce the preflight descriptors fail-fast (matching the existing
877
931
  * `runPreflight` behaviour). Iterates the shared preflight descriptors in order
@@ -1,3 +1,4 @@
1
+ import { recordProvisionedAssets } from "./generated-assets-inventory.js";
1
2
  /**
2
3
  * start-tickets — packaged CLI subcommand of the bridge-api-mcp-server bin.
3
4
  *
@@ -62,6 +63,8 @@ import path from "path";
62
63
  import { fileURLToPath } from "node:url";
63
64
  import { validateHttpBaseUrl } from "./base-url.js";
64
65
  import { VERSION } from "./version.generated.js";
66
+ import { getNoticeDecision } from "./session-upgrade-notice.js";
67
+ import { formatUpdateAdvice } from "./upgrade-advice.js";
65
68
  import { resolveBapiCredentials, getPrimaryCredentialStorePath, } from "./credential-store.js";
66
69
  import { LEGACY_SERVER_NAMES, MCP_PACKAGE_NAME, MCP_SERVER_NAME } from "./mcp-identity.js";
67
70
  // BAPI-941: the shared drift diagnostic formatter. The leaf drift module imports
@@ -836,6 +839,7 @@ export function createDefaultStartTicketsDeps() {
836
839
  encoding: "utf-8",
837
840
  // Optional bounded timeout (e.g. cursor-agent --list-models probes).
838
841
  timeout: options?.timeoutMs,
842
+ signal: options?.signal,
839
843
  }, (error, stdout, stderr) => {
840
844
  const exitCode = error && typeof error.code === "number"
841
845
  ? (error.code)
@@ -846,6 +850,7 @@ export function createDefaultStartTicketsDeps() {
846
850
  stdout: stdout ?? "",
847
851
  stderr: stderr ?? "",
848
852
  exitCode,
853
+ ...(error && typeof error.code === "string" ? { unavailable: true } : {}),
849
854
  });
850
855
  });
851
856
  });
@@ -1643,7 +1648,7 @@ export async function spawnMacOSTerminalTab(deps, terminal, shellCommand, contex
1643
1648
  : buildTerminalAppleScript(shellCommand, title);
1644
1649
  const result = await deps.runCommand("osascript", ["-e", script]);
1645
1650
  if (commandSucceeded(result))
1646
- return { ok: true };
1651
+ return { ok: true, destination: terminal === "iterm" ? "iTerm tab" : "macOS Terminal tab" };
1647
1652
  const reason = (result.stderr || result.stdout || "").trim();
1648
1653
  return {
1649
1654
  ok: false,
@@ -1731,7 +1736,7 @@ export async function spawnWindowsTerminalTab(deps, _terminal, shellCommand, con
1731
1736
  const args = buildWindowsTerminalArgs(worktreePath, shellCommand, title);
1732
1737
  const result = await deps.runCommand(WINDOWS_TERMINAL_COMMAND, args);
1733
1738
  if (commandSucceeded(result))
1734
- return { ok: true };
1739
+ return { ok: true, destination: "Windows Terminal tab" };
1735
1740
  const reason = combineCommandOutput(result);
1736
1741
  return {
1737
1742
  ok: false,
@@ -1754,7 +1759,7 @@ export async function spawnWindowsTerminalTab(deps, _terminal, shellCommand, con
1754
1759
  fallback,
1755
1760
  ]);
1756
1761
  if (commandSucceeded(result))
1757
- return { ok: true };
1762
+ return { ok: true, destination: "PowerShell fallback window" };
1758
1763
  const reason = combineCommandOutput(result);
1759
1764
  return {
1760
1765
  ok: false,
@@ -1858,7 +1863,7 @@ export async function spawnLinuxTmuxTerminalTab(deps, _terminal, shellCommand, c
1858
1863
  : buildTmuxNewSessionArgs(session, window, worktreePath, paneCommand);
1859
1864
  const result = await deps.runCommand(TMUX_COMMAND, args);
1860
1865
  if (commandSucceeded(result))
1861
- return { ok: true };
1866
+ return { ok: true, destination: "detached tmux session" };
1862
1867
  const reason = combineCommandOutput(result);
1863
1868
  return {
1864
1869
  ok: false,
@@ -2118,7 +2123,7 @@ export async function spawnTabsForCreatedWorktrees(deps, rows, terminal, buildSh
2118
2123
  worktreePath: row.path,
2119
2124
  });
2120
2125
  if (result.ok) {
2121
- out.push({ ...row, status: "spawned" });
2126
+ out.push({ ...row, status: "spawned", ...(result.destination ? { sessionDestination: result.destination } : {}) });
2122
2127
  }
2123
2128
  else {
2124
2129
  out.push({ ...row, status: "spawn-failed", error: result.error });
@@ -2220,6 +2225,23 @@ export function buildDryRunDetailLines(agent, key, branch, platform = process.pl
2220
2225
  * ` path=PATH` when available) followed by warning lines for any create/spawn
2221
2226
  * failures.
2222
2227
  */
2228
+ /** Explain only actual successful sessions; summary serialization stays stable. */
2229
+ export function formatSpawnedSessionExplanation(rows, context) {
2230
+ const spawned = rows.filter((row) => row.status === "spawned");
2231
+ if (!spawned.length)
2232
+ return "";
2233
+ const lines = ["Spawned sessions:"];
2234
+ for (const row of spawned) {
2235
+ lines.push(` ${row.key}: ${context.agent} runs in ${row.path ?? "a worktree whose path was not reported"} on branch ${row.branch}; ${row.sessionDestination ?? "terminal destination not reported"}.`);
2236
+ }
2237
+ lines.push("Work on each branch from its reported folder: it is already checked out there and normally cannot also be checked out in the original folder.");
2238
+ lines.push("Monitor the reported terminal session and its agent output; use the attach commands below on Linux. Session startup is not review or implementation completion.");
2239
+ if (context.workflow === "review-and-implement")
2240
+ lines.push("Each session reviews independently, then opens a fresh implementation session only after its review halt gate permits it.");
2241
+ if (spawned.length > 1)
2242
+ lines.push("Coordinate ownership across these independent sessions; pause affected work if overlapping changes or conflicts appear.");
2243
+ return lines.join("\n");
2244
+ }
2223
2245
  export function formatSummaryReport(rows) {
2224
2246
  const lines = ["Summary:"];
2225
2247
  // Surface the single conductor run id near the top (first line stays
@@ -2323,6 +2345,7 @@ export function buildCommandProvisioningDeps(deps) {
2323
2345
  runCommand: deps.runCommand,
2324
2346
  platform: deps.platform,
2325
2347
  cwd: deps.cwd,
2348
+ ...(!deps.readFile && !deps.writeFile && !deps.mkdir ? { recordAssets: recordProvisionedAssets } : {}),
2326
2349
  };
2327
2350
  }
2328
2351
  /**
@@ -4129,6 +4152,19 @@ export async function runStartTicketsCli(argv, overrides = {}) {
4129
4152
  const options = parsed.options;
4130
4153
  const rawDeps = overrides.deps ?? createDefaultStartTicketsDeps();
4131
4154
  const orchestrate = overrides.orchestrate ?? orchestrateStartTickets;
4155
+ // BAPI-1446: one cache-only upgrade reminder per run, on the existing stderr
4156
+ // sink. The decision is evaluated over the EFFECTIVE environment, so a worker's
4157
+ // markers (set when this CLI is itself run by an unattended process) keep it
4158
+ // silent. It reads the local cache only, never the network, and any failure is
4159
+ // contained: it never changes command execution or the exit status.
4160
+ try {
4161
+ const decision = (overrides.upgradeNotice ?? ((env) => getNoticeDecision(env, VERSION)))(rawDeps.env);
4162
+ if (decision.show)
4163
+ errorLog(formatUpdateAdvice(decision.current, decision.latest));
4164
+ }
4165
+ catch {
4166
+ // Advisory only.
4167
+ }
4132
4168
  // The parser validates `--agent`, so this resolves to a real spec; the guard
4133
4169
  // is defensive only.
4134
4170
  const agent = resolveAgentSpec(options.agentName);
@@ -4213,6 +4249,9 @@ export async function runStartTicketsCli(argv, overrides = {}) {
4213
4249
  return 1;
4214
4250
  }
4215
4251
  log(formatSummaryReport(result.rows));
4252
+ const explanation = formatSpawnedSessionExplanation(result.rows, { agent: options.agentName, workflow: options.workflow ?? "implement" });
4253
+ if (explanation)
4254
+ log(explanation);
4216
4255
  // Linux: each spawned ticket runs in a detached tmux session — tell the user
4217
4256
  // how to attach.
4218
4257
  const spawned = result.rows.filter((r) => r.status === "spawned");
@@ -0,0 +1,162 @@
1
+ /**
2
+ * First-completed-tool-result upgrade notice (BAPI-1446).
3
+ *
4
+ * Wraps the SDK's `tools/call` handler so the first eligible completed result in
5
+ * a stdio session carries one extra text content item naming both versions and
6
+ * `/upgrade-bridge`. Tool execution is untouched: the original handler runs
7
+ * outside the decoration error boundary, so a thrown tool error propagates
8
+ * exactly as it would without this wrapper, and any failure while decorating
9
+ * returns the original result.
10
+ *
11
+ * Eligibility is decided synchronously, after the original handler resolves, by
12
+ * `getNoticeDecision`. It never waits for a background lookup. The once-per-
13
+ * session flag is set only after a successful decoration, so an unknown latest
14
+ * version, a silent decision, or a result that cannot be decorated safely leaves
15
+ * the opportunity for a later call.
16
+ *
17
+ * Like `installToolSurfaceListOverride`, this reads the SDK's private
18
+ * `_requestHandlers` map through one localized compatibility adapter.
19
+ */
20
+ import { getMethodLiteral } from "@modelcontextprotocol/sdk/server/zod-json-schema-compat.js";
21
+ import { getNoticeDecision } from "./session-upgrade-notice.js";
22
+ import { formatSessionUpgradeNotice } from "./upgrade-advice.js";
23
+ /** Fixed diagnostic. It never carries exception text or private map contents. */
24
+ export const TOOL_CALL_ADVISORY_INSTALL_FAILURE = "Bridge MCP upgrade notice: tools/call advisory could not be installed (incompatible MCP SDK).";
25
+ /**
26
+ * Private provenance mark for the genuine, server-sourced `ping` advice item. A
27
+ * symbol-keyed property survives a shallow copy and is ignored by
28
+ * `JSON.stringify`, so it never reaches the wire. The symbol is not exported, so
29
+ * only this module can create or recognize the mark.
30
+ */
31
+ const SERVER_ADVICE_MARK = Symbol("bridge.serverUpgradeAdvice");
32
+ /** Mark a content item as the genuine server-sourced upgrade advice. Returns it. */
33
+ export function markServerUpgradeAdvice(item) {
34
+ Object.defineProperty(item, SERVER_ADVICE_MARK, {
35
+ value: true,
36
+ enumerable: true,
37
+ configurable: true,
38
+ writable: false,
39
+ });
40
+ return item;
41
+ }
42
+ function isServerUpgradeAdvice(item) {
43
+ return (item !== null &&
44
+ typeof item === "object" &&
45
+ item[SERVER_ADVICE_MARK] === true);
46
+ }
47
+ /** Protocol servers that already carry the wrapper, so reinstall is a no-op. */
48
+ const installedServers = new WeakSet();
49
+ function authoritativeFrom(getStatus) {
50
+ const status = getStatus?.();
51
+ if (status && status.known === true && typeof status.latestVersion === "string") {
52
+ return { known: true, latest: status.latestVersion };
53
+ }
54
+ return { known: false };
55
+ }
56
+ function emit(stderr) {
57
+ try {
58
+ stderr?.(TOOL_CALL_ADVISORY_INSTALL_FAILURE);
59
+ }
60
+ catch {
61
+ // A diagnostic sink failure must never affect startup.
62
+ }
63
+ }
64
+ /**
65
+ * Install the wrapper. Returns true when it is in place (including an earlier
66
+ * install on the same protocol server) and false after emitting the one fixed
67
+ * diagnostic. Never throws, and leaves the original handler in place on failure.
68
+ */
69
+ export function installToolCallAdvisory(options) {
70
+ const { protocolServer, callSchema, env, currentVersion, getStatus, stderr } = options;
71
+ try {
72
+ if (protocolServer && installedServers.has(protocolServer))
73
+ return true;
74
+ let method;
75
+ try {
76
+ method = getMethodLiteral(callSchema);
77
+ }
78
+ catch {
79
+ emit(stderr);
80
+ return false;
81
+ }
82
+ if (method !== "tools/call") {
83
+ emit(stderr);
84
+ return false;
85
+ }
86
+ const handlers = protocolServer?._requestHandlers;
87
+ if (!handlers || typeof handlers.get !== "function") {
88
+ emit(stderr);
89
+ return false;
90
+ }
91
+ const original = handlers.get(method);
92
+ if (typeof original !== "function") {
93
+ emit(stderr);
94
+ return false;
95
+ }
96
+ // Delivery state belongs to this installation, so it is isolated to one
97
+ // stdio session.
98
+ let delivered = false;
99
+ const decorate = (result) => {
100
+ if (delivered)
101
+ return result;
102
+ if (result === null || typeof result !== "object")
103
+ return result;
104
+ const content = result.content;
105
+ if (!Array.isArray(content))
106
+ return result;
107
+ const decision = getNoticeDecision(env, currentVersion, authoritativeFrom(getStatus));
108
+ if (!decision.show)
109
+ return result;
110
+ if (content.some(isServerUpgradeAdvice)) {
111
+ // The genuine ping advice already told the session.
112
+ delivered = true;
113
+ return result;
114
+ }
115
+ const notice = formatSessionUpgradeNotice(decision.current, decision.latest);
116
+ const decorated = {
117
+ ...result,
118
+ content: [...content, { type: "text", text: notice }],
119
+ };
120
+ delivered = true;
121
+ return decorated;
122
+ };
123
+ const wrapped = async (request, extra) => {
124
+ // Outside the boundary below: a tool-call error propagates unchanged.
125
+ const result = await original(request, extra);
126
+ try {
127
+ return decorate(result);
128
+ }
129
+ catch {
130
+ return result;
131
+ }
132
+ };
133
+ try {
134
+ // Replace in place; never remove the original first.
135
+ protocolServer.setRequestHandler(callSchema, wrapped);
136
+ }
137
+ catch {
138
+ // The replacement may have begun before the failure. Put the original back.
139
+ try {
140
+ if (handlers.get(method) !== original) {
141
+ try {
142
+ protocolServer.setRequestHandler(callSchema, original);
143
+ }
144
+ catch {
145
+ handlers.set(method, original);
146
+ }
147
+ }
148
+ }
149
+ catch {
150
+ // Nothing more can be done safely.
151
+ }
152
+ emit(stderr);
153
+ return false;
154
+ }
155
+ installedServers.add(protocolServer);
156
+ return true;
157
+ }
158
+ catch {
159
+ emit(stderr);
160
+ return false;
161
+ }
162
+ }
@@ -372,80 +372,55 @@ function setsEqual(a, b) {
372
372
  return true;
373
373
  }
374
374
  // ---------------------------------------------------------------------------
375
- // Update-staleness advisory decoration (BAPI-728)
375
+ // Asset-recovery advisory composition
376
376
  // ---------------------------------------------------------------------------
377
- /** The `_meta` key carrying the structured staleness advisory. */
378
- export const UPDATE_ADVISORY_META_KEY = "bridge-api/update-advisory";
379
- /** The tool whose description carries the model-visible advisory when stale. */
377
+ //
378
+ // BAPI-1446 removed the update-staleness decoration (BAPI-728) from `tools/list`:
379
+ // the `_meta` key and the text prepended to the `ping` description were an
380
+ // invisible channel, and the upgrade notice now reaches a session through the
381
+ // startup instructions, the first completed tool result, and the SessionStart
382
+ // hook. Only the asset-recovery advisory decorates the list now.
383
+ /** The tool whose description carries the model-visible recovery advisory. */
380
384
  const ADVISORY_HOST_TOOL = "ping";
381
- /**
382
- * Wrap a `tools/list` handler so a KNOWN-stale server tells the model so.
383
- *
384
- * Why decorate the list at all: the staleness value was already computed on every
385
- * server start and emitted to an MCP log sink nothing reads, so a session talking
386
- * to a twenty-release-old build looked completely normal — the visible symptom was
387
- * an agent that could not find `council`, with no signal explaining why. Tool
388
- * discovery is the one surface every session reads without being asked to.
389
- *
390
- * What is preserved, exactly: tool NAMES, their ORDER, their input/output SCHEMAS,
391
- * their enabled state, and every other response property. The advisory is added in
392
- * two places, both additive:
393
- *
394
- * - `_meta` — the protocol's sanctioned extension point, for clients that
395
- * surface structured metadata;
396
- * - the `ping` tool's DESCRIPTION — the surface a model actually reads. `ping`
397
- * is the topically correct host: its description already documents the
398
- * version/upgrade metadata it returns. Note this does not require anyone to
399
- * CALL `ping` — reading its description during ordinary discovery is enough,
400
- * which is the precise gap the ticket describes.
401
- *
402
- * The description text is appended at RUNTIME only, over the `ping` description
403
- * from the static tool catalog. That static catalog is untouched, so the
404
- * description prose pins that guard `mcp_server/src/index.ts` are unaffected —
405
- * but a LIVE `tools/list` capture DOES include this decoration whenever the
406
- * update status is stale, and so is NOT automatically safe for tools-budget
407
- * measurement. Budget measurements stay repository-owned only because their
408
- * child process explicitly disables the update check via
409
- * `BAPI_MCP_UPDATE_CHECK_ENABLED=false` (BAPI-917) — see
410
- * `mcp_server/src/integration/measure-tools.ts`.
411
- *
412
- * Fail-open: if the advisory cannot be produced, the original result is returned
413
- * byte-for-byte.
414
- */
415
- export function decorateListResultWithUpdateAdvisory(original, advisory) {
416
- if (!advisory)
417
- return original;
385
+ /** Compose once over undecorated SDK results, preserving the recovery metadata.
386
+ * Weak keys prevent retained results from accumulating session-wide state. */
387
+ const undecoratedResults = new WeakMap();
388
+ export function composeListAdvisories(original, recovery) {
389
+ const base = undecoratedResults.get(original) ?? original;
390
+ if (!recovery)
391
+ return base;
418
392
  try {
419
- const tools = original.tools.map((tool) => tool.name === ADVISORY_HOST_TOOL
420
- ? { ...tool, description: `${advisory}\n\n${tool.description ?? ""}`.trimEnd() }
421
- : tool);
422
- return {
423
- ...original,
424
- tools,
425
- _meta: { ...(original._meta ?? {}), [UPDATE_ADVISORY_META_KEY]: advisory },
393
+ const anchor = base.tools.find(t => t.name === ADVISORY_HOST_TOOL) ?? base.tools[0];
394
+ const result = { ...base,
395
+ tools: base.tools.map(tool => tool.name === anchor?.name
396
+ ? { ...tool, description: `${recovery.text}\n\n${anchor?.description ?? ""}`.trimEnd() } : tool),
397
+ _meta: { ...(base._meta ?? {}), "bridge-api/asset-recovery": { host: recovery.host,
398
+ missingCount: recovery.missingCount, paths: recovery.paths, capabilities: recovery.capabilities, command: recovery.command } },
426
399
  };
400
+ undecoratedResults.set(result, base);
401
+ return result;
427
402
  }
428
403
  catch {
429
- return original;
404
+ return base;
430
405
  }
431
406
  }
432
407
  /**
433
- * Compose an advisory-decorating handler over `inner`.
408
+ * Compose the recovery advisory over `inner`.
434
409
  *
435
410
  * `inner` is the capability gate's handler when dynamic gating is enabled, and
436
411
  * the SDK's own captured handler when it is disabled — so turning gating off
437
- * never turns update visibility off with it.
412
+ * never turns recovery visibility off with it. A failure to retrieve or apply the
413
+ * advisory returns the undecorated original result.
438
414
  */
439
- export function createUpdateAdvisoryListHandler(inner, getAdvisory, onListServed) {
415
+ export function createComposedAdvisoryListHandler(inner, getRecovery) {
440
416
  return async (request, extra) => {
441
- const result = await inner(request, extra);
417
+ const original = await inner(request, extra);
442
418
  try {
443
- onListServed?.();
444
- return decorateListResultWithUpdateAdvisory(result, getAdvisory());
419
+ const recovery = await getRecovery();
420
+ return composeListAdvisories(original, recovery);
445
421
  }
446
422
  catch {
447
- // Never let advisory decoration break tool discovery.
448
- return result;
423
+ return original;
449
424
  }
450
425
  };
451
426
  }