@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.
- package/README.md +272 -51
- package/build/agent-launchers/claude-executor-adapter.js +27 -0
- package/build/agents.generated.js +22 -2
- package/build/bitbucket-workspace-remote.js +136 -0
- package/build/bootstrap-project-identity.js +24 -0
- package/build/bridge-permissions.js +1362 -0
- package/build/check-update-cli.js +74 -0
- package/build/claude-settings.js +102 -2
- package/build/cli-help.js +27 -0
- package/build/codex-home.js +9 -1
- package/build/codex-launcher-pin.js +598 -0
- package/build/command-assets-doctor.js +24 -70
- package/build/command-provisioning.js +29 -5
- package/build/commands.generated.js +9 -9
- package/build/conductor/bridge-api-client.js +714 -0
- package/build/conductor/command-drift-production.js +92 -0
- package/build/conductor/command-drift.js +89 -0
- package/build/conductor/doctor.js +69 -26
- package/build/conductor/epic-branch-ci-inspection.js +1375 -0
- package/build/conductor/epic-implementer-cli.js +76 -22
- package/build/conductor/gate-discovery.js +890 -0
- package/build/conductor/install-doctor.js +15 -3
- package/build/conductor/json-output.js +63 -0
- package/build/conductor/readiness-cli.js +95 -10
- package/build/conductor/readiness-sections.js +369 -3
- package/build/conductor/readiness.js +211 -8
- package/build/conductor/recovery-cli.js +110 -4
- package/build/conductor/recovery-operations.js +99 -1
- package/build/connect-bitbucket-api.js +41 -6
- package/build/connect-bitbucket.js +106 -33
- package/build/connect-github-api.js +33 -14
- package/build/credential-store.js +207 -47
- package/build/docs.generated.js +57 -3
- package/build/doctor.js +198 -4
- package/build/drive-epic.js +45 -8
- package/build/epic-coherence-input.test-fixtures.js +4 -3
- package/build/epic-implementer-bin.js +1369 -25
- package/build/epic-workarounds-path.js +39 -0
- package/build/executor/cli.js +30 -0
- package/build/executor/completion-contract.js +20 -5
- package/build/executor/deps.js +4 -1
- package/build/executor/gate-qualification-job.js +499 -0
- package/build/executor/guarded-fast-forward-push.js +129 -0
- package/build/executor/http-client.js +43 -4
- package/build/executor/integration-job.js +122 -8
- package/build/executor/job-errors.js +8 -1
- package/build/executor/job-runner.js +589 -47
- package/build/executor/likely-tests.js +290 -0
- package/build/executor/live-worker-registry.js +1 -1
- package/build/executor/observation.js +31 -2
- package/build/executor/packaged-recipe.js +106 -0
- package/build/executor/permissions.js +115 -28
- package/build/executor/platform.js +1 -1
- package/build/executor/preflight.js +61 -12
- package/build/executor/process-containment.js +36 -12
- package/build/executor/process.js +15 -7
- package/build/executor/prompt-spec.js +33 -1
- package/build/executor/recently-failed-tests-file.js +71 -0
- package/build/executor/reconcile-overlap-job.js +41 -23
- package/build/executor/required-verification.js +1 -1
- package/build/executor/required-work-ledger.js +105 -17
- package/build/executor/retirement.js +69 -0
- package/build/executor/runner.js +114 -11
- package/build/executor/test-clock.js +15 -0
- package/build/executor/types.js +32 -0
- package/build/executor/viewer-tabs.js +58 -8
- package/build/executor/worker-command.js +23 -7
- package/build/executor/worker-diagnostics.js +37 -0
- package/build/executor/worker-finalization.js +31 -5
- package/build/executor/worker-guard-hook.js +179 -25
- package/build/executor/worker-log.js +4 -3
- package/build/executor/worker-verification-guidance.js +37 -0
- package/build/executor/worktree.js +88 -11
- package/build/first-build-advisory.js +97 -0
- package/build/generated-assets-advisory.js +14 -0
- package/build/generated-assets-catalog.js +66 -0
- package/build/generated-assets-context.js +46 -0
- package/build/generated-assets-dependencies.js +26 -0
- package/build/generated-assets-doctor.js +50 -0
- package/build/generated-assets-health.js +65 -0
- package/build/generated-assets-host.js +17 -0
- package/build/generated-assets-inventory.js +17 -0
- package/build/generated-assets-paths.js +60 -0
- package/build/generated-assets-publisher.js +133 -0
- package/build/generated-assets-remedy.js +25 -0
- package/build/generated-assets-repair.js +72 -0
- package/build/generated-assets-session.js +71 -0
- package/build/generated-assets-test-support.js +35 -0
- package/build/index.js +1109 -695
- package/build/init.js +54 -177
- package/build/install-bridge-conductor-gate.js +1372 -0
- package/build/install-bridge-conductor.js +289 -36
- package/build/install-bridge.js +722 -252
- package/build/launcher-config-inspection.js +184 -19
- package/build/mcp-host-targets.js +4 -0
- package/build/mcp-identity.js +7 -0
- package/build/mcp-install-state.js +50 -4
- package/build/merge-workflow-install.js +3 -4
- package/build/pipeline-utils.js +6 -2
- package/build/pipelines.generated.js +22 -142
- package/build/plan-execution-ledger.js +2 -32
- package/build/plane/manifest.js +16 -1
- package/build/plane/preflight.js +54 -1
- package/build/printed-remedy-commands.js +16 -1
- package/build/readme.generated.js +1 -1
- package/build/repair-assets.js +93 -0
- package/build/run-unit-tests-launcher.js +130 -49
- package/build/secret-redaction.js +33 -0
- package/build/session-upgrade-notice.js +86 -0
- package/build/setup-epic.js +108 -3
- package/build/start-tickets-prereqs.js +70 -16
- package/build/start-tickets.js +44 -5
- package/build/tool-call-advisory.js +162 -0
- package/build/tool-surface-gating.js +32 -57
- package/build/update-check.js +97 -16
- package/build/update-status.js +61 -71
- package/build/upgrade-advice.js +15 -11
- package/build/upgrade-cli.js +398 -17
- package/build/version.generated.js +3 -3
- package/build/worker-guard-hook-bin.js +3 -2
- package/build/worker-guard-hook-main.js +1 -1
- package/build/worktree-core.js +15 -2
- package/docs/EPIC-IMPLEMENTER-INSTALL.md +30 -12
- package/docs/EPIC-IMPLEMENTER.md +68 -2
- package/package.json +3 -3
- package/pipelines/greenfield-setup.json +4 -128
- 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
|
-
|
|
449
|
-
|
|
450
|
-
|
|
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:
|
|
453
|
+
darwin: WORKTREE_COMMAND_ASSETS_PROVISION_HINT,
|
|
458
454
|
linux: WORKTREE_COMMAND_ASSETS_HINT,
|
|
459
|
-
win32:
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
package/build/start-tickets.js
CHANGED
|
@@ -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
|
-
//
|
|
375
|
+
// Asset-recovery advisory composition
|
|
376
376
|
// ---------------------------------------------------------------------------
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
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
|
-
*
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
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
|
|
420
|
-
|
|
421
|
-
: tool
|
|
422
|
-
|
|
423
|
-
...
|
|
424
|
-
|
|
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
|
|
404
|
+
return base;
|
|
430
405
|
}
|
|
431
406
|
}
|
|
432
407
|
/**
|
|
433
|
-
* Compose
|
|
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
|
|
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
|
|
415
|
+
export function createComposedAdvisoryListHandler(inner, getRecovery) {
|
|
440
416
|
return async (request, extra) => {
|
|
441
|
-
const
|
|
417
|
+
const original = await inner(request, extra);
|
|
442
418
|
try {
|
|
443
|
-
|
|
444
|
-
return
|
|
419
|
+
const recovery = await getRecovery();
|
|
420
|
+
return composeListAdvisories(original, recovery);
|
|
445
421
|
}
|
|
446
422
|
catch {
|
|
447
|
-
|
|
448
|
-
return result;
|
|
423
|
+
return original;
|
|
449
424
|
}
|
|
450
425
|
};
|
|
451
426
|
}
|