@bridge_gpt/mcp-server 0.2.66 → 0.2.67
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 +252 -77
- package/build/agent-capabilities/probes.js +15 -5
- package/build/agent-launchers/claude-executor-adapter.js +43 -16
- package/build/agent-launchers/executor-adapter.js +7 -4
- package/build/agents.generated.js +1 -1
- package/build/bounded-wait.js +67 -10
- package/build/commands.generated.js +11 -11
- package/build/conductor/bridge-api-client.js +143 -1
- package/build/conductor/bring-up-facts.js +1 -1
- package/build/conductor/doctor.js +3 -3
- package/build/conductor/epic-implementer-cli.js +22 -6
- package/build/conductor/errors.js +4 -3
- package/build/conductor/guided-repair.js +6 -2
- package/build/conductor/hook-runtime-qualification.js +1 -1
- package/build/conductor/merge-execution-contract.js +27 -8
- package/build/conductor/merge-execution-eligibility.js +3 -2
- package/build/conductor/pr-ci-producer.js +1 -1
- package/build/conductor/readiness-cli.js +18 -2
- package/build/conductor/readiness-sections.js +41 -6
- package/build/conductor/readiness.js +181 -29
- package/build/conductor/recovery-cli.js +200 -8
- package/build/conductor/recovery-operations.js +69 -3
- package/build/conductor/repair-contract.js +54 -3
- package/build/conductor/repository-workflow-remedy.js +125 -0
- package/build/conductor/tools.js +2 -2
- package/build/conductor-bundle-artifacts.js +8 -0
- package/build/docs.generated.js +8 -3
- package/build/drive-epic-key.js +6 -5
- package/build/drive-epic.js +85 -54
- package/build/epic-coherence-input.js +2 -1
- package/build/epic-implementer-bin.js +22 -20
- package/build/epic-implementer-bundle-cli.js +2 -2
- package/build/executor/cli.js +3 -2
- package/build/executor/completion-contract.js +48 -5
- package/build/executor/completion-supervision.js +4 -0
- package/build/executor/env.js +19 -0
- package/build/executor/heartbeat.js +1 -1
- package/build/executor/http-client.js +20 -0
- package/build/executor/job-errors.js +25 -0
- package/build/executor/job-runner.js +225 -41
- package/build/executor/observation.js +6 -1
- package/build/executor/preflight.js +51 -14
- package/build/executor/required-work-ledger.js +6 -1
- package/build/executor/resume-pre-spawn.js +2 -0
- package/build/executor/runner.js +8 -2
- package/build/executor/service-unit.js +7 -1
- package/build/executor/worker-finalization.js +82 -29
- package/build/executor/worker-guard-hook.js +137 -33
- package/build/executor/worktree.js +82 -1
- package/build/index.js +403 -429
- package/build/install-bridge-conductor.js +8 -6
- package/build/install-bridge.js +3 -3
- package/build/phase-context-git-probe.js +68 -0
- package/build/phase-result-artifacts.js +173 -5
- package/build/pipelines.generated.js +18 -24
- package/build/plane/alembic-head.js +12 -3
- package/build/plane/base-worktree-guard.js +147 -0
- package/build/plane/cli.js +217 -17
- package/build/plane/code-snapshot.js +468 -0
- package/build/plane/defaults.js +48 -3
- package/build/plane/endpoint.js +41 -0
- package/build/plane/manifest.js +120 -1
- package/build/plane/member-roster.js +94 -19
- package/build/plane/preflight.js +30 -2
- package/build/plane/process-cwd.js +67 -0
- package/build/plane/restart.js +542 -0
- package/build/plane/shutdown.js +31 -0
- package/build/plane/status.js +143 -8
- package/build/plane/supervisor.js +586 -179
- package/build/plane/types.js +54 -2
- package/build/printed-remedy-commands.js +37 -0
- package/build/readiness-check.js +1 -1
- package/build/readme.generated.js +1 -1
- package/build/review-generation.js +22 -0
- package/build/setup-epic.js +44 -19
- package/build/sfcc/config.js +9 -0
- package/build/sfcc/credentials.js +100 -38
- package/build/sfcc/hostname.js +67 -0
- package/build/sfcc/permissions.js +17 -13
- package/build/sfcc/register.js +1 -1
- package/build/sfcc/tool-wrapper.js +39 -43
- package/build/sfcc/write-grants.js +17 -11
- package/build/sfcc/write-result.js +4 -3
- package/build/sfcc/writes-site-preference.js +20 -8
- package/build/start-tickets.js +179 -31
- package/build/tool-error-envelope.js +61 -0
- package/build/version.generated.js +3 -3
- package/build/visual-diff.js +22 -1
- package/build/worker-guard-hook-bin.js +3 -6
- package/build/worker-guard-hook-main.js +47 -0
- package/build/worktree-core.js +130 -4
- package/docs/EPIC-IMPLEMENTER-INSTALL.md +849 -0
- package/docs/EPIC-IMPLEMENTER.md +662 -0
- package/docs/install/mcp-tool-integrations.md +24 -15
- package/docs/install/sfcc-integration.md +12 -4
- package/package.json +1 -1
- package/pipelines/plan-epic.json +0 -5
- package/pipelines/review-ticket.json +1 -1
- package/docs/CONDUCTOR.md +0 -605
- package/pipelines/idea-to-pr.json +0 -49
package/build/start-tickets.js
CHANGED
|
@@ -136,7 +136,7 @@ export function getStartTicketsUsage() {
|
|
|
136
136
|
" --host claude-code|cursor|codex|copilot-vscode|windsurf Current host when --agent is omitted; Claude Code, Cursor, and Codex can launch sessions",
|
|
137
137
|
" Cursor with --auto launches headlessly after a bounded readiness check; without --auto it stays interactive for the review gate.",
|
|
138
138
|
" --workflow implement|review-and-implement Command each spawned worktree runs (default: implement). review-and-implement runs review-ticket then, after a per-ticket halt gate, hands off to a fresh implement-ticket session; --auto applies to both phases.",
|
|
139
|
-
" --tier cheap|basic|premium Strict manual model-routing override: bypasses the per-ticket difficulty/tier lookup and applies this tier to every ticket exactly as supplied, with no per-session posture adjustment. Claude/Cursor use registry aliases and repository overrides; Codex uses the authenticated Bridge catalog. It is NOT a raw --model alias and never carries an API key or credential. A malformed value requests premium routing when available. Mutually exclusive with --auto-tier.",
|
|
139
|
+
" --tier cheap|basic|premium Strict manual model-routing override: bypasses the per-ticket difficulty/tier lookup and applies this tier to every ticket exactly as supplied, with no per-session posture adjustment. Claude/Cursor use registry aliases and repository overrides; Codex uses the authenticated Bridge catalog. It is NOT a raw --model alias and never carries an API key or credential. A malformed value requests premium routing when available. With --workflow review-and-implement a valid value is also forwarded to the spawned command so the review tab and the fresh implementation session both run on it. Mutually exclusive with --auto-tier.",
|
|
140
140
|
" --auto-tier cheap|basic|premium Internal/advanced machine-carried raw backend tier for chained automation handoffs (e.g. /review-and-implement): bypasses the per-ticket lookup like --tier, but remains subject to per-session optimization-posture adjustment (may be downshifted under maximize_subscription, unless the server reports configured thresholds). Not the strict-override mechanism — use --tier for a human override. Mutually exclusive with --tier.",
|
|
141
141
|
" --rounds 1|2 Review round count forwarded to the review phase; review-only, valid only with --workflow review-and-implement",
|
|
142
142
|
" --terminal terminal|iterm Override the macOS terminal app (default: auto-detect via $TERM_PROGRAM); honored on macOS only",
|
|
@@ -619,7 +619,9 @@ resumeMode = false, workflow = "implement", reviewRounds,
|
|
|
619
619
|
// conductor runs always, and (BAPI-801/BAPI-847) for every plain run whose base
|
|
620
620
|
// resolved, `main` included — and threaded (BAPI-593) into the spawned workflow
|
|
621
621
|
// command.
|
|
622
|
-
baseBranch, bridgeApiBaseUrlOverride
|
|
622
|
+
baseBranch, bridgeApiBaseUrlOverride,
|
|
623
|
+
// BAPI-1365: a recognized operator `--tier`, forwarded to the review prompt.
|
|
624
|
+
manualTier) {
|
|
623
625
|
if (!isSupportedStartTicketsPlatform(deps.platform)) {
|
|
624
626
|
return { ok: false, error: unsupportedPlatformMessage(deps.platform) };
|
|
625
627
|
}
|
|
@@ -640,7 +642,7 @@ baseBranch, bridgeApiBaseUrlOverride) {
|
|
|
640
642
|
// falls back to the basename-derived repo name (the 403 root cause), and
|
|
641
643
|
// (BAPI-586/BAPI-801) the run base so the spawned worker opens its PR
|
|
642
644
|
// against it. The base assignment stays outermost.
|
|
643
|
-
buildAgentShellCommand: (key, worktreePath, modelAlias, launchArgs) => prependBaseBranchEnvAssignment(prependRepoNameEnvAssignment(buildAgentShellCommand(agent, key, worktreePath, platform, autoApprove, modelAlias, conductorEnabled, resumeMode, workflow, reviewRounds, baseBranch, launchArgs, bridgeApiBaseUrl), repoName, platform), prBaseBranch, platform),
|
|
645
|
+
buildAgentShellCommand: (key, worktreePath, modelAlias, launchArgs) => prependBaseBranchEnvAssignment(prependRepoNameEnvAssignment(buildAgentShellCommand(agent, key, worktreePath, platform, autoApprove, modelAlias, conductorEnabled, resumeMode, workflow, reviewRounds, baseBranch, launchArgs, bridgeApiBaseUrl, manualTier), repoName, platform), prBaseBranch, platform),
|
|
644
646
|
spawnTerminalTab: deps.spawnTerminalTab,
|
|
645
647
|
},
|
|
646
648
|
};
|
|
@@ -1072,6 +1074,19 @@ export async function refreshBaseBranch(deps, options) {
|
|
|
1072
1074
|
// `<baseBranch>` is checked out somewhere: fast-forward it in place. git refuses to
|
|
1073
1075
|
// force-move a checked-out branch ref, so we must go through merge.
|
|
1074
1076
|
if (basePath) {
|
|
1077
|
+
// BAPI-1362: a live plane member may be running out of this very checkout.
|
|
1078
|
+
// Fail closed — an unreadable or ambiguous record protects it. When
|
|
1079
|
+
// protected, nothing local is moved: the caller cuts from the fetched
|
|
1080
|
+
// `origin/<base>` SHA through the same fetch-only resolution the
|
|
1081
|
+
// non-mutating path uses.
|
|
1082
|
+
const decision = await evaluateGuardSafely(deps, basePath);
|
|
1083
|
+
if (decision.kind === "protect") {
|
|
1084
|
+
(deps.baseWorktreeGuardWarningLog ?? defaultGuardWarningLog)(formatBaseWorktreeGuardWarning(decision, baseBranch));
|
|
1085
|
+
const resolved = await fetchAndResolveBaseSha(deps, baseBranch);
|
|
1086
|
+
if (!resolved.ok)
|
|
1087
|
+
return { ok: false, error: resolved.error };
|
|
1088
|
+
return { ok: true, protectedBaseSha: resolved.base_sha };
|
|
1089
|
+
}
|
|
1075
1090
|
const merge = await deps.runCommand("git", ["merge", "--ff-only", originRef], { cwd: basePath });
|
|
1076
1091
|
if (!commandSucceeded(merge)) {
|
|
1077
1092
|
return {
|
|
@@ -1103,6 +1118,25 @@ export async function refreshBaseBranch(deps, options) {
|
|
|
1103
1118
|
}
|
|
1104
1119
|
return { ok: true };
|
|
1105
1120
|
}
|
|
1121
|
+
function defaultGuardWarningLog(message) {
|
|
1122
|
+
// stderr, never stdout: stdout carries the workflow's normal output.
|
|
1123
|
+
process.stderr.write(`${message}\n`);
|
|
1124
|
+
}
|
|
1125
|
+
/** Run the guard; any failure of the guard itself is a `protect` (fail closed). */
|
|
1126
|
+
async function evaluateGuardSafely(deps, basePath) {
|
|
1127
|
+
try {
|
|
1128
|
+
if (deps.evaluateBaseWorktreeGuard)
|
|
1129
|
+
return await deps.evaluateBaseWorktreeGuard(basePath);
|
|
1130
|
+
return await evaluateBaseWorktreeGuard(basePath, await createBaseWorktreeGuardDeps());
|
|
1131
|
+
}
|
|
1132
|
+
catch {
|
|
1133
|
+
return {
|
|
1134
|
+
kind: "protect",
|
|
1135
|
+
reason: "manifest-unreadable",
|
|
1136
|
+
manifestLocation: ".bridge/plane/plane.json",
|
|
1137
|
+
};
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1106
1140
|
/**
|
|
1107
1141
|
* Compatibility wrapper around {@link refreshBaseBranch} that preserves the
|
|
1108
1142
|
* historical `refreshMainBranch(deps, { refreshMain })` signature for callers
|
|
@@ -1195,6 +1229,7 @@ async function emitClaudeReviewWorkflowDriftAdvisory(deps, baseBranch, overrides
|
|
|
1195
1229
|
// import the VALUES here (so internal uses keep working) and re-export the same
|
|
1196
1230
|
// references so existing importers of `./start-tickets.js` (index.ts,
|
|
1197
1231
|
// review-tickets.ts) and the pinned start-tickets tests are unaffected.
|
|
1232
|
+
import { evaluateBaseWorktreeGuard, createBaseWorktreeGuardDeps, formatBaseWorktreeGuardWarning, } from "./plane/base-worktree-guard.js";
|
|
1198
1233
|
import { validateBranchName, fetchAndResolveBaseSha,
|
|
1199
1234
|
// BAPI-941: both live in the same leaf module so the interactive refresh path
|
|
1200
1235
|
// and the non-mutating conductor path share one drift implementation.
|
|
@@ -1339,8 +1374,9 @@ export async function resumeWorktrees(deps, options) {
|
|
|
1339
1374
|
* E1/F3b proved a clean TEXTUAL merge can still hide a SEMANTIC break (branch A
|
|
1340
1375
|
* adds a helper; branch B adds a contract test over all helpers), which only the
|
|
1341
1376
|
* FULL suite catches — so this instruction requires the worker to run the full
|
|
1342
|
-
* project test suite (
|
|
1343
|
-
* push/complete on green, escalating otherwise.
|
|
1377
|
+
* project test suite (every unit suite the project defines, the same suites its
|
|
1378
|
+
* pull request CI runs) and only push/complete on green, escalating otherwise.
|
|
1379
|
+
* The wording stays project-neutral because this CLI runs in other repositories.
|
|
1344
1380
|
*
|
|
1345
1381
|
* Single line (no newlines, no `;`, no quotes/apostrophes) so it stays safe when
|
|
1346
1382
|
* single-quoted into a shell command and embedded in terminal-launcher args.
|
|
@@ -1351,8 +1387,8 @@ export function buildResumeModeRemediationFinalizeInstruction() {
|
|
|
1351
1387
|
"the current base branch and resolve the merge conflicts. A clean textual merge can " +
|
|
1352
1388
|
"still break behavior, so inspect for semantic conflicts even when there are no textual " +
|
|
1353
1389
|
"conflict markers. Before you push or mark the ticket complete, run the full test suite " +
|
|
1354
|
-
"for the project (the full unit suite
|
|
1355
|
-
"
|
|
1390
|
+
"for the project (the full unit suite: every unit test suite the project defines, the " +
|
|
1391
|
+
"same suites its pull request CI runs) " +
|
|
1356
1392
|
"and do not rely on targeted subsets as your only verification. Push and mark the ticket " +
|
|
1357
1393
|
"complete only after the full suite is green. If you cannot make the full suite pass, " +
|
|
1358
1394
|
"report the ticket blocked and escalate rather than pushing a green-looking but broken merge.");
|
|
@@ -1385,6 +1421,8 @@ export function buildAgentPrompt(key, opts = {}) {
|
|
|
1385
1421
|
// Review-only arguments (`--rounds`, non-default `--base-branch`) must never
|
|
1386
1422
|
// leak into the legacy `/implement-ticket` prompt.
|
|
1387
1423
|
if (workflow === "review-and-implement") {
|
|
1424
|
+
if (opts.manualTier !== undefined)
|
|
1425
|
+
command += ` --tier=${opts.manualTier}`;
|
|
1388
1426
|
if (opts.agentName === "cursor-agent")
|
|
1389
1427
|
command += " --agent=cursor-agent";
|
|
1390
1428
|
if (opts.reviewRounds !== undefined) {
|
|
@@ -1457,13 +1495,13 @@ export function buildAgentInvocation(agent, prompt, quote, modelAlias, launchArg
|
|
|
1457
1495
|
}
|
|
1458
1496
|
}
|
|
1459
1497
|
/** POSIX agent shell command: `cd '<path>' && <agent> [--model '<alias>'] '<prompt>'`. */
|
|
1460
|
-
export function buildPosixAgentShellCommand(agent, key, worktreePath, autoApprove = false, modelAlias, conductorEnabled = false, resumeMode = false, workflow = "implement", reviewRounds, baseBranch, launchArgs = [], bridgeApiBaseUrl) {
|
|
1461
|
-
const invocation = buildAgentInvocation(agent, buildAgentPrompt(key, { autoApprove, conductorEnabled, resumeMode, workflow, reviewRounds, baseBranch, agentName: agent.name }), (p) => `'${shSquoteInner(p)}'`, modelAlias, agent.name === "cursor-agent" && autoApprove ? worktreePath : launchArgs);
|
|
1498
|
+
export function buildPosixAgentShellCommand(agent, key, worktreePath, autoApprove = false, modelAlias, conductorEnabled = false, resumeMode = false, workflow = "implement", reviewRounds, baseBranch, launchArgs = [], bridgeApiBaseUrl, manualTier) {
|
|
1499
|
+
const invocation = buildAgentInvocation(agent, buildAgentPrompt(key, { autoApprove, conductorEnabled, resumeMode, workflow, reviewRounds, baseBranch, agentName: agent.name, manualTier }), (p) => `'${shSquoteInner(p)}'`, modelAlias, agent.name === "cursor-agent" && autoApprove ? worktreePath : launchArgs);
|
|
1462
1500
|
return `cd '${shSquoteInner(worktreePath)}' && ${prependBridgeApiBaseUrlEnvAssignment(invocation, bridgeApiBaseUrl, "darwin")}`;
|
|
1463
1501
|
}
|
|
1464
1502
|
/** PowerShell agent shell command: `Set-Location -LiteralPath '<path>'; <agent> [--model '<alias>'] '<prompt>'`. */
|
|
1465
|
-
export function buildPowerShellAgentShellCommand(agent, key, worktreePath, autoApprove = false, modelAlias, conductorEnabled = false, resumeMode = false, workflow = "implement", reviewRounds, baseBranch, launchArgs = [], bridgeApiBaseUrl) {
|
|
1466
|
-
const invocation = buildAgentInvocation(agent, buildAgentPrompt(key, { autoApprove, conductorEnabled, resumeMode, workflow, reviewRounds, baseBranch, agentName: agent.name }), powershellSquote, modelAlias, agent.name === "cursor-agent" && autoApprove ? worktreePath : launchArgs);
|
|
1503
|
+
export function buildPowerShellAgentShellCommand(agent, key, worktreePath, autoApprove = false, modelAlias, conductorEnabled = false, resumeMode = false, workflow = "implement", reviewRounds, baseBranch, launchArgs = [], bridgeApiBaseUrl, manualTier) {
|
|
1504
|
+
const invocation = buildAgentInvocation(agent, buildAgentPrompt(key, { autoApprove, conductorEnabled, resumeMode, workflow, reviewRounds, baseBranch, agentName: agent.name, manualTier }), powershellSquote, modelAlias, agent.name === "cursor-agent" && autoApprove ? worktreePath : launchArgs);
|
|
1467
1505
|
return `Set-Location -LiteralPath ${powershellSquote(worktreePath)}; ${prependBridgeApiBaseUrlEnvAssignment(invocation, bridgeApiBaseUrl, "win32")}`;
|
|
1468
1506
|
}
|
|
1469
1507
|
/**
|
|
@@ -1474,10 +1512,10 @@ export function buildPowerShellAgentShellCommand(agent, key, worktreePath, autoA
|
|
|
1474
1512
|
* injected as `--model` at the spawn boundary. `conductorEnabled` appends the
|
|
1475
1513
|
* BAPI-586 PR-base contract instruction to the prompt (opt-in via `--conductor`).
|
|
1476
1514
|
*/
|
|
1477
|
-
export function buildAgentShellCommand(agent, key, worktreePath, platform = "darwin", autoApprove = false, modelAlias, conductorEnabled = false, resumeMode = false, workflow = "implement", reviewRounds, baseBranch, launchArgs = [], bridgeApiBaseUrl) {
|
|
1515
|
+
export function buildAgentShellCommand(agent, key, worktreePath, platform = "darwin", autoApprove = false, modelAlias, conductorEnabled = false, resumeMode = false, workflow = "implement", reviewRounds, baseBranch, launchArgs = [], bridgeApiBaseUrl, manualTier) {
|
|
1478
1516
|
if (platform === "win32")
|
|
1479
|
-
return buildPowerShellAgentShellCommand(agent, key, worktreePath, autoApprove, modelAlias, conductorEnabled, resumeMode, workflow, reviewRounds, baseBranch, launchArgs, bridgeApiBaseUrl);
|
|
1480
|
-
return buildPosixAgentShellCommand(agent, key, worktreePath, autoApprove, modelAlias, conductorEnabled, resumeMode, workflow, reviewRounds, baseBranch, launchArgs, bridgeApiBaseUrl);
|
|
1517
|
+
return buildPowerShellAgentShellCommand(agent, key, worktreePath, autoApprove, modelAlias, conductorEnabled, resumeMode, workflow, reviewRounds, baseBranch, launchArgs, bridgeApiBaseUrl, manualTier);
|
|
1518
|
+
return buildPosixAgentShellCommand(agent, key, worktreePath, autoApprove, modelAlias, conductorEnabled, resumeMode, workflow, reviewRounds, baseBranch, launchArgs, bridgeApiBaseUrl, manualTier);
|
|
1481
1519
|
}
|
|
1482
1520
|
/**
|
|
1483
1521
|
* Build the shell command run inside a spawned tab/session for an ARBITRARY
|
|
@@ -2105,7 +2143,7 @@ export function buildDryRunResults(keys, overrides) {
|
|
|
2105
2143
|
* PowerShell on Windows, `wt` + POSIX on macOS/Linux, and a non-throwing `wt` +
|
|
2106
2144
|
* POSIX fallback for unsupported platforms.
|
|
2107
2145
|
*/
|
|
2108
|
-
export function getDryRunPlatformDetails(agent, platform = process.platform, env = process.env, autoApprove = false, conductorEnabled = false, repoName = null, workflow = "implement", reviewRounds, baseBranch) {
|
|
2146
|
+
export function getDryRunPlatformDetails(agent, platform = process.platform, env = process.env, autoApprove = false, conductorEnabled = false, repoName = null, workflow = "implement", reviewRounds, baseBranch, manualTier) {
|
|
2109
2147
|
return {
|
|
2110
2148
|
worktrunkBinary: resolveWorktrunkBinary(platform, env),
|
|
2111
2149
|
// The builder accepts an optional resolved modelAlias; the dry-run caller
|
|
@@ -2118,7 +2156,7 @@ export function getDryRunPlatformDetails(agent, platform = process.platform, env
|
|
|
2118
2156
|
// buildAgentShellCommand/buildAgentPrompt path as a real spawn. A Codex
|
|
2119
2157
|
// dry-run has no created worktree to bind, so its preview omits the
|
|
2120
2158
|
// process-local -C/-c MCP scope that the real spawn resolves later.
|
|
2121
|
-
buildAgentShellCommand: (key, worktreePath, modelAlias, launchArgs) => prependBaseBranchEnvAssignment(prependRepoNameEnvAssignment(buildAgentShellCommand(agent, key, worktreePath, platform, autoApprove, modelAlias, conductorEnabled, false, workflow, reviewRounds, baseBranch, launchArgs, resolveSpawnBridgeApiBaseUrl(env)), repoName, platform), resolvePrBaseBranchEnvValue(conductorEnabled, baseBranch), platform),
|
|
2159
|
+
buildAgentShellCommand: (key, worktreePath, modelAlias, launchArgs) => prependBaseBranchEnvAssignment(prependRepoNameEnvAssignment(buildAgentShellCommand(agent, key, worktreePath, platform, autoApprove, modelAlias, conductorEnabled, false, workflow, reviewRounds, baseBranch, launchArgs, resolveSpawnBridgeApiBaseUrl(env), manualTier), repoName, platform), resolvePrBaseBranchEnvValue(conductorEnabled, baseBranch), platform),
|
|
2122
2160
|
};
|
|
2123
2161
|
}
|
|
2124
2162
|
/**
|
|
@@ -2157,8 +2195,8 @@ export function buildDryRunMcpProvisioningLines(worktreePath, platform = process
|
|
|
2157
2195
|
* the secret-free MCP provisioning preview. Pure platform formatting only — no
|
|
2158
2196
|
* preflight, no routing failures.
|
|
2159
2197
|
*/
|
|
2160
|
-
export function buildDryRunDetailLines(agent, key, branch, platform = process.platform, env = process.env, baseBranch = "main", autoApprove = false, modelAlias = null, conductorEnabled = false, repoName = null, mcpServerInvocation, workflow = "implement", reviewRounds) {
|
|
2161
|
-
const { worktrunkBinary, buildAgentShellCommand: build } = getDryRunPlatformDetails(agent, platform, env, autoApprove, conductorEnabled, repoName, workflow, reviewRounds, baseBranch);
|
|
2198
|
+
export function buildDryRunDetailLines(agent, key, branch, platform = process.platform, env = process.env, baseBranch = "main", autoApprove = false, modelAlias = null, conductorEnabled = false, repoName = null, mcpServerInvocation, workflow = "implement", reviewRounds, manualTier) {
|
|
2199
|
+
const { worktrunkBinary, buildAgentShellCommand: build } = getDryRunPlatformDetails(agent, platform, env, autoApprove, conductorEnabled, repoName, workflow, reviewRounds, baseBranch, manualTier);
|
|
2162
2200
|
const wtArgs = buildWtSwitchArgs(branch, false, baseBranch);
|
|
2163
2201
|
const agentInvocation = build(key, "<worktree-path>", modelAlias);
|
|
2164
2202
|
// BAPI-801/BAPI-847: one concise secondary detail naming the PR base, emitted
|
|
@@ -2383,6 +2421,12 @@ export function formatModelRoutingDiagnosticLine(d) {
|
|
|
2383
2421
|
case "posture-unavailable":
|
|
2384
2422
|
return ("model routing: optimization posture was unavailable; Quality routing was retained " +
|
|
2385
2423
|
"(reason=optimization-posture-unavailable).");
|
|
2424
|
+
case "review-consumer-unrecognized":
|
|
2425
|
+
return (`model routing: the Bridge API server did not recognize the review consumer${ticketSuffix} ` +
|
|
2426
|
+
"(reason=consumer-unrecognized); the review tab uses the premium fallback. Upgrade the server.");
|
|
2427
|
+
case "review-threshold-legacy":
|
|
2428
|
+
return (`model routing: review thresholds are not configured${ticketSuffix} ` +
|
|
2429
|
+
"(reason=legacy); the review tab uses the premium fallback. Configure review model-tier thresholds.");
|
|
2386
2430
|
default:
|
|
2387
2431
|
return `model routing degraded: ${d.message} ${failOpen}`;
|
|
2388
2432
|
}
|
|
@@ -3023,7 +3067,43 @@ export function formatModelRoutingDecisionDiagnosticLine(input) {
|
|
|
3023
3067
|
return (`[model-routing] source=${input.source} baseline=${input.baseline} ` +
|
|
3024
3068
|
`posture=${input.posture} adjustment=${input.adjustment} effective=${input.effective} ` +
|
|
3025
3069
|
`alias=${input.alias ?? "unresolved"}` +
|
|
3026
|
-
(input.threshold ? ` threshold=${input.threshold}` : "")
|
|
3070
|
+
(input.threshold ? ` threshold=${input.threshold}` : "") +
|
|
3071
|
+
(input.phase === "review" ? " phase=review consumer=review" : ""));
|
|
3072
|
+
}
|
|
3073
|
+
/**
|
|
3074
|
+
* BAPI-1365: classify a successfully fetched review-consumer `/model-tier`
|
|
3075
|
+
* response. `consumer-unrecognized` = the server did not echo `consumer:
|
|
3076
|
+
* "review"` (its tier and marker are not a review decision and are never
|
|
3077
|
+
* interpreted); `legacy` = it echoed review but no stored review thresholds
|
|
3078
|
+
* decided the tier. `undefined` = a usable review decision, OR a
|
|
3079
|
+
* `source: "fallback"` response (the difficulty itself failed; callers keep the
|
|
3080
|
+
* existing lookup-failure handling for that).
|
|
3081
|
+
*/
|
|
3082
|
+
function reviewTierFallbackCause(value) {
|
|
3083
|
+
if (value.consumer !== "review")
|
|
3084
|
+
return "consumer-unrecognized";
|
|
3085
|
+
if (value.source === "fallback")
|
|
3086
|
+
return undefined;
|
|
3087
|
+
if (!isThresholdDecidedTierResponse(value))
|
|
3088
|
+
return "legacy";
|
|
3089
|
+
return undefined;
|
|
3090
|
+
}
|
|
3091
|
+
/** BAPI-1365: the secret-free operator warning and diagnostic for a review fallback cause. */
|
|
3092
|
+
function reviewTierFallbackNotice(cause, repoName, ticketKey) {
|
|
3093
|
+
const kind = cause === "consumer-unrecognized" ? "review-consumer-unrecognized" : "review-threshold-legacy";
|
|
3094
|
+
const warning = cause === "consumer-unrecognized"
|
|
3095
|
+
? `model routing: server did not recognize the review consumer for ${ticketKey} (reason=consumer-unrecognized)`
|
|
3096
|
+
: `model routing: review thresholds are not configured for ${ticketKey} (reason=legacy)`;
|
|
3097
|
+
return {
|
|
3098
|
+
warning,
|
|
3099
|
+
diagnostic: makeModelRoutingDiagnostic({
|
|
3100
|
+
kind,
|
|
3101
|
+
repoName,
|
|
3102
|
+
storeTarget: `bapi:${repoName}`,
|
|
3103
|
+
ticketKey,
|
|
3104
|
+
message: warning,
|
|
3105
|
+
}),
|
|
3106
|
+
};
|
|
3027
3107
|
}
|
|
3028
3108
|
/**
|
|
3029
3109
|
* BAPI-1343 R-6: the threshold marker for a successfully fetched `/model-tier`
|
|
@@ -3032,11 +3112,16 @@ export function formatModelRoutingDecisionDiagnosticLine(input) {
|
|
|
3032
3112
|
* (`configured` or `anomaly`). Every other case returns `undefined`, so the
|
|
3033
3113
|
* caller keeps today's exact downshift and text.
|
|
3034
3114
|
*/
|
|
3035
|
-
function thresholdSkipMarker(posture, tierResult) {
|
|
3115
|
+
function thresholdSkipMarker(posture, tierResult, reviewWorkflow = false) {
|
|
3036
3116
|
if (posture !== "maximize_subscription")
|
|
3037
3117
|
return undefined;
|
|
3038
3118
|
if (!tierResult || !tierResult.ok)
|
|
3039
3119
|
return undefined;
|
|
3120
|
+
// BAPI-1365: a review-consumer lookup counts only when the server echoed the
|
|
3121
|
+
// review consumer; an older server that ignored the parameter answered for
|
|
3122
|
+
// the implementation consumer, which says nothing about the review threshold.
|
|
3123
|
+
if (reviewWorkflow && tierResult.value.consumer !== "review")
|
|
3124
|
+
return undefined;
|
|
3040
3125
|
if (!isThresholdDecidedTierResponse(tierResult.value))
|
|
3041
3126
|
return undefined;
|
|
3042
3127
|
return tierResult.value.thresholdMode;
|
|
@@ -3206,6 +3291,12 @@ errorLog = (message) => console.error(message)) {
|
|
|
3206
3291
|
// produced it.
|
|
3207
3292
|
const postureResolution = await resolveOptimizationPostureForStartTickets(access);
|
|
3208
3293
|
const posture = postureResolution.posture;
|
|
3294
|
+
// BAPI-1365: the spawned tab of a `review-and-implement` launch is a REVIEW
|
|
3295
|
+
// tab, so its tier lookups ask for the review consumer. An ordinary
|
|
3296
|
+
// `implement` launch supplies no options and keeps the endpoint's default
|
|
3297
|
+
// implementation consumer. Derived once and shared by both fetch paths below.
|
|
3298
|
+
const isReviewWorkflow = options.workflow === "review-and-implement";
|
|
3299
|
+
const tierFetchOptions = isReviewWorkflow ? { consumer: "review" } : {};
|
|
3209
3300
|
// BAPI-1225: one deterministic, best-effort diagnostic line per repository-level
|
|
3210
3301
|
// resolution, emitted exactly here — outside the per-ticket row loop below, so a
|
|
3211
3302
|
// multi-ticket invocation still logs it once for this canonical repository. Uses
|
|
@@ -3232,14 +3323,14 @@ errorLog = (message) => console.error(message)) {
|
|
|
3232
3323
|
// hoisting pattern above.
|
|
3233
3324
|
//
|
|
3234
3325
|
// BAPI-1343 R-6: under `maximize_subscription` ONLY, each eligible row's
|
|
3235
|
-
// `/model-tier` is fetched once (the shared bounded fetch
|
|
3236
|
-
//
|
|
3326
|
+
// `/model-tier` is fetched once (the shared bounded fetch; BAPI-1365: with
|
|
3327
|
+
// the workflow's consumer) purely to read `threshold_mode`. A threshold-decided
|
|
3237
3328
|
// (`configured`/`anomaly`) response keeps the carried tier — stored
|
|
3238
3329
|
// thresholds made it final. A fetch failure, `legacy`, or anything else
|
|
3239
3330
|
// downshifts exactly as before. `quality`/`speed` never downshift, so they
|
|
3240
3331
|
// make no extra request.
|
|
3241
3332
|
const thresholdTierMap = posture === "maximize_subscription"
|
|
3242
|
-
? await fetchTicketModelTiersForRows(access, eligible, options.maxParallel, isEligible)
|
|
3333
|
+
? await fetchTicketModelTiersForRows(access, eligible, options.maxParallel, isEligible, tierFetchOptions)
|
|
3243
3334
|
: null;
|
|
3244
3335
|
const aliasByEffectiveTier = new Map();
|
|
3245
3336
|
const resolveAutoTierAlias = async (effectiveTier) => {
|
|
@@ -3269,7 +3360,14 @@ errorLog = (message) => console.error(message)) {
|
|
|
3269
3360
|
autoOut.push(row);
|
|
3270
3361
|
continue;
|
|
3271
3362
|
}
|
|
3272
|
-
const
|
|
3363
|
+
const markerResult = thresholdTierMap?.get(row.key);
|
|
3364
|
+
const threshold = thresholdSkipMarker(posture, markerResult, isReviewWorkflow);
|
|
3365
|
+
// BAPI-1365: for a review launch, say WHY a fetched marker did not decide
|
|
3366
|
+
// the tier (never a review-threshold claim). A failed fetch keeps the
|
|
3367
|
+
// existing silent downshift.
|
|
3368
|
+
const markerCause = isReviewWorkflow && markerResult && markerResult.ok
|
|
3369
|
+
? reviewTierFallbackCause(markerResult.value)
|
|
3370
|
+
: undefined;
|
|
3273
3371
|
const effectiveTier = posture === "maximize_subscription" && !threshold
|
|
3274
3372
|
? downshiftModelTier(autoTier)
|
|
3275
3373
|
: autoTier;
|
|
@@ -3277,7 +3375,7 @@ errorLog = (message) => console.error(message)) {
|
|
|
3277
3375
|
const lineageSuffix = threshold
|
|
3278
3376
|
? `; threshold:${threshold}`
|
|
3279
3377
|
: posture === "maximize_subscription"
|
|
3280
|
-
? `; auto-tier:${autoTier}->maximize:${effectiveTier}`
|
|
3378
|
+
? `; auto-tier:${autoTier}->maximize:${effectiveTier}${markerCause ? `; review-marker:${markerCause}` : ""}`
|
|
3281
3379
|
: "";
|
|
3282
3380
|
const { resolvedAlias, aliasWarning } = await resolveAutoTierAlias(effectiveTier);
|
|
3283
3381
|
const reason = resolvedAlias
|
|
@@ -3294,7 +3392,14 @@ errorLog = (message) => console.error(message)) {
|
|
|
3294
3392
|
? { modelRoutingDiagnostic: codexEntitlementDiagnostic(access.repoName) }
|
|
3295
3393
|
: {}),
|
|
3296
3394
|
};
|
|
3297
|
-
|
|
3395
|
+
let pushedRow = aliasWarning ? appendSummaryRowWarning(baseRow, aliasWarning) : baseRow;
|
|
3396
|
+
if (markerCause) {
|
|
3397
|
+
const notice = reviewTierFallbackNotice(markerCause, access.repoName, row.key);
|
|
3398
|
+
pushedRow = appendSummaryRowWarning(pushedRow, notice.warning);
|
|
3399
|
+
if (!pushedRow.modelRoutingDiagnostic)
|
|
3400
|
+
pushedRow = { ...pushedRow, modelRoutingDiagnostic: notice.diagnostic };
|
|
3401
|
+
}
|
|
3402
|
+
autoOut.push(pushedRow);
|
|
3298
3403
|
// BAPI-1236: one `automatic_snapshot` diagnostic per eligible row — the
|
|
3299
3404
|
// carried baseline, the newly resolved posture, the exact adjustment token,
|
|
3300
3405
|
// the calculated effective tier, and the validated alias (or `unresolved`).
|
|
@@ -3306,13 +3411,14 @@ errorLog = (message) => console.error(message)) {
|
|
|
3306
3411
|
effective: effectiveTier,
|
|
3307
3412
|
alias: resolvedAlias,
|
|
3308
3413
|
...(threshold ? { threshold } : {}),
|
|
3414
|
+
...(isReviewWorkflow ? { phase: "review" } : {}),
|
|
3309
3415
|
});
|
|
3310
3416
|
}
|
|
3311
3417
|
out = autoOut;
|
|
3312
3418
|
}
|
|
3313
3419
|
else {
|
|
3314
3420
|
// Ordinary per-ticket difficulty-lookup path.
|
|
3315
|
-
const tierMap = await fetchTicketModelTiersForRows(access, eligible, options.maxParallel, isEligible);
|
|
3421
|
+
const tierMap = await fetchTicketModelTiersForRows(access, eligible, options.maxParallel, isEligible, tierFetchOptions);
|
|
3316
3422
|
// Lazily resolve+validate the premium fallback alias at most once for this run
|
|
3317
3423
|
// (the alias is identical across rows; cursor-agent validation spawns a probe).
|
|
3318
3424
|
let premiumApplier = null;
|
|
@@ -3351,6 +3457,40 @@ errorLog = (message) => console.error(message)) {
|
|
|
3351
3457
|
continue;
|
|
3352
3458
|
}
|
|
3353
3459
|
const { difficulty, tier, source } = tierResult.value;
|
|
3460
|
+
// BAPI-1365: a review-launch response is a review-threshold decision only
|
|
3461
|
+
// when the server echoed `consumer: "review"` AND stored thresholds decided
|
|
3462
|
+
// it. Anything else takes the existing premium fallback with a closed
|
|
3463
|
+
// reason — the returned tier and marker are never interpreted.
|
|
3464
|
+
if (isReviewWorkflow) {
|
|
3465
|
+
const cause = reviewTierFallbackCause(tierResult.value);
|
|
3466
|
+
if (cause) {
|
|
3467
|
+
const notice = reviewTierFallbackNotice(cause, access.repoName, row.key);
|
|
3468
|
+
if (agent.name === "codex") {
|
|
3469
|
+
loopOut.push(codexDefaultRows([row], () => true, notice.warning, notice.diagnostic)[0]);
|
|
3470
|
+
continue;
|
|
3471
|
+
}
|
|
3472
|
+
loopOut.push(await applyPremiumFallback(row, notice.warning, notice.diagnostic));
|
|
3473
|
+
continue;
|
|
3474
|
+
}
|
|
3475
|
+
if (source === "fallback") {
|
|
3476
|
+
// The server could not obtain a difficulty: its premium tier is a
|
|
3477
|
+
// guess, not a review decision — keep the lookup-failure handling.
|
|
3478
|
+
const warning = `model routing: tier lookup failed for ${row.key}; using agent default model`;
|
|
3479
|
+
const diagnostic = makeModelRoutingDiagnostic({
|
|
3480
|
+
kind: "tier-fetch-unavailable",
|
|
3481
|
+
repoName: access.repoName,
|
|
3482
|
+
storeTarget: `bapi:${access.repoName}`,
|
|
3483
|
+
ticketKey: row.key,
|
|
3484
|
+
message: `tier lookup failed for ${row.key}`,
|
|
3485
|
+
});
|
|
3486
|
+
if (agent.name === "codex") {
|
|
3487
|
+
loopOut.push(codexDefaultRows([row], () => true, warning, diagnostic)[0]);
|
|
3488
|
+
continue;
|
|
3489
|
+
}
|
|
3490
|
+
loopOut.push(await applyPremiumFallback(row, warning, diagnostic));
|
|
3491
|
+
continue;
|
|
3492
|
+
}
|
|
3493
|
+
}
|
|
3354
3494
|
if (!tier) {
|
|
3355
3495
|
// Backend reported no usable difficulty — default to premium/Opus. (The
|
|
3356
3496
|
// backend now returns a premium fallback itself, so this is defensive.)
|
|
@@ -3396,7 +3536,7 @@ errorLog = (message) => console.error(message)) {
|
|
|
3396
3536
|
// diagnostic carry a distinct `threshold` marker instead. The harness-
|
|
3397
3537
|
// local premium fallback above is untouched.
|
|
3398
3538
|
const difficultyTier = tier;
|
|
3399
|
-
const threshold = thresholdSkipMarker(posture, tierResult);
|
|
3539
|
+
const threshold = thresholdSkipMarker(posture, tierResult, isReviewWorkflow);
|
|
3400
3540
|
const effectiveTier = posture === "maximize_subscription" && !threshold
|
|
3401
3541
|
? downshiftModelTier(difficultyTier)
|
|
3402
3542
|
: difficultyTier;
|
|
@@ -3406,7 +3546,10 @@ errorLog = (message) => console.error(message)) {
|
|
|
3406
3546
|
: posture === "maximize_subscription"
|
|
3407
3547
|
? `; difficulty:${difficultyTier}->maximize:${effectiveTier}`
|
|
3408
3548
|
: "";
|
|
3409
|
-
const thresholdField =
|
|
3549
|
+
const thresholdField = {
|
|
3550
|
+
...(threshold ? { threshold } : {}),
|
|
3551
|
+
...(isReviewWorkflow ? { phase: "review" } : {}),
|
|
3552
|
+
};
|
|
3410
3553
|
// BAPI-1236: stamp `difficulty_lookup` rather than copying the backend
|
|
3411
3554
|
// cache/computation source (`cached`/`computed`/`fallback`) into the
|
|
3412
3555
|
// summary's own routing-provenance field — that backend source is
|
|
@@ -3758,7 +3901,7 @@ export async function orchestrateStartTickets(deps, options, overrides = {}) {
|
|
|
3758
3901
|
// epic.base_branch override applied above) so the spawned worker gets
|
|
3759
3902
|
// BAPI_BASE_BRANCH — every conductor worker, and a plain worker whose base
|
|
3760
3903
|
// is non-default.
|
|
3761
|
-
options.baseBranch, bridgeApiBaseUrl);
|
|
3904
|
+
options.baseBranch, bridgeApiBaseUrl, isModelTier(options.injectedTier) ? options.injectedTier : undefined);
|
|
3762
3905
|
if (!platformConfig.ok)
|
|
3763
3906
|
return { ok: false, error: platformConfig.error };
|
|
3764
3907
|
// BAPI-527: resolve the base start point new worktrees are cut from BEFORE any
|
|
@@ -3787,6 +3930,11 @@ export async function orchestrateStartTickets(deps, options, overrides = {}) {
|
|
|
3787
3930
|
});
|
|
3788
3931
|
if (!refresh.ok)
|
|
3789
3932
|
return { ok: false, error: refresh.error };
|
|
3933
|
+
// BAPI-1362: a live plane member runs from the base worktree, so the local
|
|
3934
|
+
// branch was left where it was; cut from the fetched remote SHA instead.
|
|
3935
|
+
if (refresh.protectedBaseSha !== undefined) {
|
|
3936
|
+
effectiveBaseStartPoint = refresh.protectedBaseSha;
|
|
3937
|
+
}
|
|
3790
3938
|
}
|
|
3791
3939
|
// BAPI-941: the `claude-review` workflow-drift advisory, emitted ONCE per
|
|
3792
3940
|
// worker spawn at the one choke point both dispatch paths pass through, after
|
|
@@ -4023,7 +4171,7 @@ export async function runStartTicketsCli(argv, overrides = {}) {
|
|
|
4023
4171
|
const branch = resolveBranchForTicket(key, options.branchOverrides);
|
|
4024
4172
|
const routedRow = routedByKey.get(key);
|
|
4025
4173
|
const modelAlias = routedRow?.modelAlias ?? null;
|
|
4026
|
-
for (const line of buildDryRunDetailLines(agent, key, branch, deps.platform, deps.env, options.baseBranch, options.autoApprove, modelAlias, options.conductorEnabled ?? false, dryRunRepoName, dryRunMcpInvocation, options.workflow, options.reviewRounds)) {
|
|
4174
|
+
for (const line of buildDryRunDetailLines(agent, key, branch, deps.platform, deps.env, options.baseBranch, options.autoApprove, modelAlias, options.conductorEnabled ?? false, dryRunRepoName, dryRunMcpInvocation, options.workflow, options.reviewRounds, isModelTier(options.injectedTier) ? options.injectedTier : undefined)) {
|
|
4027
4175
|
log(line);
|
|
4028
4176
|
}
|
|
4029
4177
|
log(`DRY-RUN: model routing: ${formatModelRoutingLine(routedRow ?? { key, branch, status: "dry-run" }, agent, { workflow: options.workflow })}`);
|
|
@@ -7,6 +7,10 @@
|
|
|
7
7
|
* "this operation has no local-backend equivalent" refusal, which the ticket
|
|
8
8
|
* routes return as HTTP 409 with `detail.error = "UNSUPPORTED_IN_LOCAL_MODE"`.
|
|
9
9
|
*
|
|
10
|
+
* BAPI-1382 adds a second trusted pair: the plan-entitlement denial, which the
|
|
11
|
+
* server returns as HTTP 403 with `detail.error = "ENTITLEMENT_REQUIRED"`. It
|
|
12
|
+
* tells a plan limit apart from a bad credential, which is also a 403.
|
|
13
|
+
*
|
|
10
14
|
* Recognition is by the exact `(error, status)` PAIR — never by message text.
|
|
11
15
|
* Message text is prose that gets reworded; matching on it would make a copy
|
|
12
16
|
* edit silently change orchestration control flow.
|
|
@@ -18,6 +22,54 @@
|
|
|
18
22
|
export const UNSUPPORTED_IN_LOCAL_MODE_ERROR = "UNSUPPORTED_IN_LOCAL_MODE";
|
|
19
23
|
/** The HTTP status that marker is only ever valid with. */
|
|
20
24
|
export const UNSUPPORTED_IN_LOCAL_MODE_STATUS = 409;
|
|
25
|
+
/** The plan-entitlement marker (BAPI-1382): the account's plan lacks a feature. */
|
|
26
|
+
export const ENTITLEMENT_REQUIRED_ERROR = "ENTITLEMENT_REQUIRED";
|
|
27
|
+
/** The HTTP status that marker is only ever valid with. */
|
|
28
|
+
export const ENTITLEMENT_REQUIRED_STATUS = 403;
|
|
29
|
+
/**
|
|
30
|
+
* The request-time too-little-ticket-detail refusal (BAPI-1382): the generate
|
|
31
|
+
* routes answer HTTP 422 with `detail.error = "INSUFFICIENT_TICKET_DETAIL"`
|
|
32
|
+
* plus the measured length, threshold, and description source.
|
|
33
|
+
*/
|
|
34
|
+
export const INSUFFICIENT_TICKET_DETAIL_ERROR = "INSUFFICIENT_TICKET_DETAIL";
|
|
35
|
+
/** The HTTP status that marker is only ever valid with. */
|
|
36
|
+
export const INSUFFICIENT_TICKET_DETAIL_STATUS = 422;
|
|
37
|
+
/**
|
|
38
|
+
* The server `(error, status)` pairs the MCP relay keeps as received. Every
|
|
39
|
+
* other server-provided `error` is replaced by the relay's own status-derived
|
|
40
|
+
* code, so an arbitrary detail object can never forge a code.
|
|
41
|
+
*/
|
|
42
|
+
export const TRUSTED_UPSTREAM_ERROR_PAIRS = [
|
|
43
|
+
[UNSUPPORTED_IN_LOCAL_MODE_ERROR, UNSUPPORTED_IN_LOCAL_MODE_STATUS],
|
|
44
|
+
[ENTITLEMENT_REQUIRED_ERROR, ENTITLEMENT_REQUIRED_STATUS],
|
|
45
|
+
[INSUFFICIENT_TICKET_DETAIL_ERROR, INSUFFICIENT_TICKET_DETAIL_STATUS],
|
|
46
|
+
];
|
|
47
|
+
/** True only for an exact trusted `(error, status)` pair. */
|
|
48
|
+
export function isTrustedUpstreamErrorPair(error, status) {
|
|
49
|
+
return TRUSTED_UPSTREAM_ERROR_PAIRS.some(([code, code_status]) => error === code && status === code_status);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Add the typed `operation` to a serialized error envelope (BAPI-1382). A text
|
|
53
|
+
* that is not an envelope is wrapped into one with the given status, so a
|
|
54
|
+
* refused request is always parseable and never a prose-prefixed string.
|
|
55
|
+
*/
|
|
56
|
+
export function withRequestOperation(envelopeText, operation, status) {
|
|
57
|
+
let parsed = null;
|
|
58
|
+
try {
|
|
59
|
+
parsed = JSON.parse(envelopeText);
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
parsed = null;
|
|
63
|
+
}
|
|
64
|
+
if (parsed !== null &&
|
|
65
|
+
typeof parsed === "object" &&
|
|
66
|
+
!Array.isArray(parsed) &&
|
|
67
|
+
typeof parsed.error === "string" &&
|
|
68
|
+
typeof parsed.status === "number") {
|
|
69
|
+
return JSON.stringify({ ...parsed, operation });
|
|
70
|
+
}
|
|
71
|
+
return JSON.stringify({ error: "UNKNOWN_ERROR", status, message: envelopeText, operation });
|
|
72
|
+
}
|
|
21
73
|
/**
|
|
22
74
|
* Parse `text` as a tool-error envelope, or return `null`.
|
|
23
75
|
*
|
|
@@ -52,8 +104,17 @@ export function parseToolErrorEnvelope(text) {
|
|
|
52
104
|
if (Array.isArray(obj.unsupported_fields)) {
|
|
53
105
|
envelope.unsupported_fields = obj.unsupported_fields.filter((v) => typeof v === "string");
|
|
54
106
|
}
|
|
107
|
+
if (typeof obj.operation === "string") {
|
|
108
|
+
envelope.operation = obj.operation;
|
|
109
|
+
}
|
|
55
110
|
return envelope;
|
|
56
111
|
}
|
|
112
|
+
/** True for the exact plan-entitlement contract: the marker AND HTTP 403. */
|
|
113
|
+
export function isEntitlementRequiredEnvelope(envelope) {
|
|
114
|
+
return (envelope !== null &&
|
|
115
|
+
envelope.error === ENTITLEMENT_REQUIRED_ERROR &&
|
|
116
|
+
envelope.status === ENTITLEMENT_REQUIRED_STATUS);
|
|
117
|
+
}
|
|
57
118
|
/** True for the exact unsupported-local contract: the marker AND HTTP 409. */
|
|
58
119
|
export function isUnsupportedInLocalModeEnvelope(envelope) {
|
|
59
120
|
return (envelope !== null &&
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
// AUTO-GENERATED — do not edit manually. Regenerate with: npm run build
|
|
2
|
-
export const VERSION = "0.2.
|
|
3
|
-
export const BUILD_COMMIT = "
|
|
4
|
-
export const LAUNCHER_ARGS = ["-y", "--prefer-offline", "@bridge_gpt/mcp-server@0.2.
|
|
2
|
+
export const VERSION = "0.2.67";
|
|
3
|
+
export const BUILD_COMMIT = "8eb6ba563e5f";
|
|
4
|
+
export const LAUNCHER_ARGS = ["-y", "--prefer-offline", "@bridge_gpt/mcp-server@0.2.67", "serve"];
|
package/build/visual-diff.js
CHANGED
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
* mismatch budget (never 0%, which AA/font rendering makes unreachable).
|
|
19
19
|
*/
|
|
20
20
|
import path from "path";
|
|
21
|
+
import { createRequire } from "module";
|
|
21
22
|
import { Worker } from "worker_threads";
|
|
22
23
|
import { computePngVisualDiff, DEFAULT_PASS_MISMATCH_PCT, PIXELMATCH_COLOR_THRESHOLD, } from "./visual-diff-worker.js";
|
|
23
24
|
// ---------------------------------------------------------------------------
|
|
@@ -247,6 +248,26 @@ export async function loadPlaywright() {
|
|
|
247
248
|
return { ok: false };
|
|
248
249
|
}
|
|
249
250
|
}
|
|
251
|
+
/**
|
|
252
|
+
* The Chromium install command matching the bundled `playwright-core`.
|
|
253
|
+
*
|
|
254
|
+
* A bare `npx playwright install chromium` fetches whatever Playwright is
|
|
255
|
+
* newest, whose browser build may not match the one this server drives.
|
|
256
|
+
* Resolution is best-effort: any failure returns the historic unversioned
|
|
257
|
+
* command rather than throwing, since this only builds a hint.
|
|
258
|
+
*/
|
|
259
|
+
export function chromiumInstallCommand(readMetadata = () => createRequire(import.meta.url)("playwright-core/package.json")) {
|
|
260
|
+
try {
|
|
261
|
+
const version = readMetadata()?.version;
|
|
262
|
+
if (typeof version === "string" && version.length > 0) {
|
|
263
|
+
return `npx playwright@${version} install chromium`;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
catch {
|
|
267
|
+
// Not resolvable: fall through to the unversioned command.
|
|
268
|
+
}
|
|
269
|
+
return "npx playwright install chromium";
|
|
270
|
+
}
|
|
250
271
|
export async function launchBrowser(playwright) {
|
|
251
272
|
try {
|
|
252
273
|
const browser = await playwright.chromium.launch({ headless: true });
|
|
@@ -258,7 +279,7 @@ export async function launchBrowser(playwright) {
|
|
|
258
279
|
ok: false,
|
|
259
280
|
error: "BROWSER_UNAVAILABLE",
|
|
260
281
|
status: 503,
|
|
261
|
-
message: `Chromium could not be launched: ${err instanceof Error ? err.message : "unknown error"}. Run "
|
|
282
|
+
message: `Chromium could not be launched: ${err instanceof Error ? err.message : "unknown error"}. Run "${chromiumInstallCommand()}".`,
|
|
262
283
|
};
|
|
263
284
|
}
|
|
264
285
|
}
|