@bridge_gpt/mcp-server 0.2.65 → 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 +269 -82
- package/build/agent-capabilities/probes.js +15 -5
- package/build/agent-launchers/claude-executor-adapter.js +55 -34
- package/build/agent-launchers/executor-adapter.js +34 -4
- package/build/agents.generated.js +1 -1
- package/build/bounded-wait.js +67 -10
- package/build/bridge-config.js +104 -0
- package/build/codex-bridge-profile.js +10 -1
- package/build/commands.generated.js +12 -12
- package/build/conductor/bridge-api-client.js +372 -10
- package/build/conductor/bring-up-facts.js +1 -1
- package/build/conductor/doctor.js +3 -3
- package/build/conductor/epic-child-continuation.js +61 -34
- package/build/conductor/epic-implementer-cli.js +34 -6
- package/build/conductor/errors.js +4 -3
- package/build/conductor/guided-repair.js +327 -0
- 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 +46 -9
- package/build/conductor/readiness-sections.js +41 -6
- package/build/conductor/readiness.js +204 -30
- package/build/conductor/recovery-cli.js +703 -20
- package/build/conductor/recovery-operations.js +392 -3
- package/build/conductor/repair-contract.js +112 -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/credential-store.js +5 -4
- package/build/diagnostic-text.js +36 -0
- package/build/docs.generated.js +8 -3
- package/build/doctor.js +70 -10
- package/build/drive-epic-key.js +6 -5
- package/build/drive-epic.js +479 -131
- package/build/epic-coherence-input.js +2 -1
- package/build/epic-implementer-bin.js +30 -26
- package/build/epic-implementer-bundle-cli.js +2 -2
- package/build/epic-launch-diagnostic.js +31 -0
- package/build/epic-launch-profile.js +44 -14
- package/build/executor/cli.js +3 -2
- package/build/executor/completion-contract.js +48 -5
- package/build/executor/completion-supervision.js +9 -0
- package/build/executor/correction-dispatch.js +60 -0
- package/build/executor/env.js +25 -0
- package/build/executor/heartbeat.js +1 -1
- package/build/executor/http-client.js +35 -0
- package/build/executor/integration-job.js +6 -1
- package/build/executor/job-errors.js +38 -4
- package/build/executor/job-log-registry.js +340 -73
- package/build/executor/job-runner.js +485 -74
- package/build/executor/job-types.js +8 -13
- package/build/executor/observation.js +6 -1
- package/build/executor/preflight.js +136 -10
- package/build/executor/prompt-spec.js +113 -9
- package/build/executor/required-work-ledger.js +6 -1
- package/build/executor/resume-pre-spawn.js +2 -0
- package/build/executor/runner.js +31 -2
- package/build/executor/service-unit.js +7 -1
- package/build/executor/stale-artifacts.js +5 -4
- package/build/executor/types.js +1 -0
- package/build/executor/watch-cli.js +35 -8
- package/build/executor/worker-command.js +43 -43
- package/build/executor/worker-finalization.js +161 -43
- package/build/executor/worker-guard-hook.js +152 -12
- package/build/executor/worker-log.js +137 -70
- package/build/executor/worktree-gc.js +30 -6
- package/build/executor/worktree.js +82 -1
- package/build/index.js +482 -660
- package/build/init.js +107 -4
- package/build/install-bridge-conductor.js +8 -6
- package/build/install-bridge.js +287 -23
- package/build/install-doctor.js +172 -30
- package/build/install-reexec.js +69 -7
- package/build/macos-terminal-permission-guidance.js +16 -0
- package/build/model-thresholds-tool.js +185 -0
- package/build/phase-context-git-probe.js +68 -0
- package/build/phase-result-artifacts.js +173 -5
- package/build/pipelines.generated.js +19 -25
- 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 +145 -6
- package/build/plane/supervisor.js +588 -177
- package/build/plane/types.js +54 -2
- package/build/plane/worker-exit-code.js +2 -0
- package/build/printed-remedy-commands.js +37 -0
- package/build/project-root.js +99 -0
- package/build/readiness-check.js +1 -1
- package/build/readme.generated.js +1 -1
- package/build/review-generation.js +22 -0
- package/build/review-tickets.js +156 -13
- package/build/setup-epic.js +224 -127
- 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/setup-status.js +2 -0
- package/build/sfcc/tool-wrapper.js +41 -43
- package/build/sfcc/tools-configured.js +96 -0
- 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 +332 -79
- package/build/tool-error-envelope.js +61 -0
- package/build/upgrade-cli.js +109 -24
- package/build/version.generated.js +3 -3
- package/build/visual-diff.js +22 -1
- package/build/windows-npm-entry.js +103 -0
- package/build/worker-guard-hook-bin.js +3 -5
- package/build/worker-guard-hook-main.js +47 -0
- package/build/worktree-core.js +136 -9
- package/docs/EPIC-IMPLEMENTER-INSTALL.md +849 -0
- package/docs/EPIC-IMPLEMENTER.md +662 -0
- package/docs/install/mcp-tool-integrations.md +25 -15
- package/docs/install/sfcc-integration.md +21 -8
- package/package.json +1 -1
- package/pipelines/implement-ticket.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
|
@@ -54,6 +54,7 @@
|
|
|
54
54
|
* unit-testable on Linux CI without spawning real commands or terminals.
|
|
55
55
|
*/
|
|
56
56
|
import { execFile } from "child_process";
|
|
57
|
+
import { macosTerminalPermissionGuidance } from "./macos-terminal-permission-guidance.js";
|
|
57
58
|
import { readFile, writeFile, mkdir, mkdtemp, stat, lstat, symlink, unlink, copyFile, readdir, rm, } from "fs/promises";
|
|
58
59
|
import os from "node:os";
|
|
59
60
|
import { createHash } from "node:crypto";
|
|
@@ -135,8 +136,8 @@ export function getStartTicketsUsage() {
|
|
|
135
136
|
" --host claude-code|cursor|codex|copilot-vscode|windsurf Current host when --agent is omitted; Claude Code, Cursor, and Codex can launch sessions",
|
|
136
137
|
" Cursor with --auto launches headlessly after a bounded readiness check; without --auto it stays interactive for the review gate.",
|
|
137
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.",
|
|
138
|
-
" --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
|
-
" --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). Not the strict-override mechanism — use --tier for a human override. Mutually exclusive with --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
|
+
" --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.",
|
|
140
141
|
" --rounds 1|2 Review round count forwarded to the review phase; review-only, valid only with --workflow review-and-implement",
|
|
141
142
|
" --terminal terminal|iterm Override the macOS terminal app (default: auto-detect via $TERM_PROGRAM); honored on macOS only",
|
|
142
143
|
" --dry-run Print intended actions; creates no worktrees and opens no tabs, but DOES resolve model routing read-only (may compute+cache a ticket's difficulty) to preview the --model each tab would use",
|
|
@@ -618,7 +619,9 @@ resumeMode = false, workflow = "implement", reviewRounds,
|
|
|
618
619
|
// conductor runs always, and (BAPI-801/BAPI-847) for every plain run whose base
|
|
619
620
|
// resolved, `main` included — and threaded (BAPI-593) into the spawned workflow
|
|
620
621
|
// command.
|
|
621
|
-
baseBranch, bridgeApiBaseUrlOverride
|
|
622
|
+
baseBranch, bridgeApiBaseUrlOverride,
|
|
623
|
+
// BAPI-1365: a recognized operator `--tier`, forwarded to the review prompt.
|
|
624
|
+
manualTier) {
|
|
622
625
|
if (!isSupportedStartTicketsPlatform(deps.platform)) {
|
|
623
626
|
return { ok: false, error: unsupportedPlatformMessage(deps.platform) };
|
|
624
627
|
}
|
|
@@ -639,7 +642,7 @@ baseBranch, bridgeApiBaseUrlOverride) {
|
|
|
639
642
|
// falls back to the basename-derived repo name (the 403 root cause), and
|
|
640
643
|
// (BAPI-586/BAPI-801) the run base so the spawned worker opens its PR
|
|
641
644
|
// against it. The base assignment stays outermost.
|
|
642
|
-
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),
|
|
643
646
|
spawnTerminalTab: deps.spawnTerminalTab,
|
|
644
647
|
},
|
|
645
648
|
};
|
|
@@ -1071,6 +1074,19 @@ export async function refreshBaseBranch(deps, options) {
|
|
|
1071
1074
|
// `<baseBranch>` is checked out somewhere: fast-forward it in place. git refuses to
|
|
1072
1075
|
// force-move a checked-out branch ref, so we must go through merge.
|
|
1073
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
|
+
}
|
|
1074
1090
|
const merge = await deps.runCommand("git", ["merge", "--ff-only", originRef], { cwd: basePath });
|
|
1075
1091
|
if (!commandSucceeded(merge)) {
|
|
1076
1092
|
return {
|
|
@@ -1102,6 +1118,25 @@ export async function refreshBaseBranch(deps, options) {
|
|
|
1102
1118
|
}
|
|
1103
1119
|
return { ok: true };
|
|
1104
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
|
+
}
|
|
1105
1140
|
/**
|
|
1106
1141
|
* Compatibility wrapper around {@link refreshBaseBranch} that preserves the
|
|
1107
1142
|
* historical `refreshMainBranch(deps, { refreshMain })` signature for callers
|
|
@@ -1194,6 +1229,7 @@ async function emitClaudeReviewWorkflowDriftAdvisory(deps, baseBranch, overrides
|
|
|
1194
1229
|
// import the VALUES here (so internal uses keep working) and re-export the same
|
|
1195
1230
|
// references so existing importers of `./start-tickets.js` (index.ts,
|
|
1196
1231
|
// review-tickets.ts) and the pinned start-tickets tests are unaffected.
|
|
1232
|
+
import { evaluateBaseWorktreeGuard, createBaseWorktreeGuardDeps, formatBaseWorktreeGuardWarning, } from "./plane/base-worktree-guard.js";
|
|
1197
1233
|
import { validateBranchName, fetchAndResolveBaseSha,
|
|
1198
1234
|
// BAPI-941: both live in the same leaf module so the interactive refresh path
|
|
1199
1235
|
// and the non-mutating conductor path share one drift implementation.
|
|
@@ -1338,8 +1374,9 @@ export async function resumeWorktrees(deps, options) {
|
|
|
1338
1374
|
* E1/F3b proved a clean TEXTUAL merge can still hide a SEMANTIC break (branch A
|
|
1339
1375
|
* adds a helper; branch B adds a contract test over all helpers), which only the
|
|
1340
1376
|
* FULL suite catches — so this instruction requires the worker to run the full
|
|
1341
|
-
* project test suite (
|
|
1342
|
-
* 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.
|
|
1343
1380
|
*
|
|
1344
1381
|
* Single line (no newlines, no `;`, no quotes/apostrophes) so it stays safe when
|
|
1345
1382
|
* single-quoted into a shell command and embedded in terminal-launcher args.
|
|
@@ -1350,8 +1387,8 @@ export function buildResumeModeRemediationFinalizeInstruction() {
|
|
|
1350
1387
|
"the current base branch and resolve the merge conflicts. A clean textual merge can " +
|
|
1351
1388
|
"still break behavior, so inspect for semantic conflicts even when there are no textual " +
|
|
1352
1389
|
"conflict markers. Before you push or mark the ticket complete, run the full test suite " +
|
|
1353
|
-
"for the project (the full unit suite
|
|
1354
|
-
"
|
|
1390
|
+
"for the project (the full unit suite: every unit test suite the project defines, the " +
|
|
1391
|
+
"same suites its pull request CI runs) " +
|
|
1355
1392
|
"and do not rely on targeted subsets as your only verification. Push and mark the ticket " +
|
|
1356
1393
|
"complete only after the full suite is green. If you cannot make the full suite pass, " +
|
|
1357
1394
|
"report the ticket blocked and escalate rather than pushing a green-looking but broken merge.");
|
|
@@ -1384,6 +1421,8 @@ export function buildAgentPrompt(key, opts = {}) {
|
|
|
1384
1421
|
// Review-only arguments (`--rounds`, non-default `--base-branch`) must never
|
|
1385
1422
|
// leak into the legacy `/implement-ticket` prompt.
|
|
1386
1423
|
if (workflow === "review-and-implement") {
|
|
1424
|
+
if (opts.manualTier !== undefined)
|
|
1425
|
+
command += ` --tier=${opts.manualTier}`;
|
|
1387
1426
|
if (opts.agentName === "cursor-agent")
|
|
1388
1427
|
command += " --agent=cursor-agent";
|
|
1389
1428
|
if (opts.reviewRounds !== undefined) {
|
|
@@ -1456,13 +1495,13 @@ export function buildAgentInvocation(agent, prompt, quote, modelAlias, launchArg
|
|
|
1456
1495
|
}
|
|
1457
1496
|
}
|
|
1458
1497
|
/** POSIX agent shell command: `cd '<path>' && <agent> [--model '<alias>'] '<prompt>'`. */
|
|
1459
|
-
export function buildPosixAgentShellCommand(agent, key, worktreePath, autoApprove = false, modelAlias, conductorEnabled = false, resumeMode = false, workflow = "implement", reviewRounds, baseBranch, launchArgs = [], bridgeApiBaseUrl) {
|
|
1460
|
-
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);
|
|
1461
1500
|
return `cd '${shSquoteInner(worktreePath)}' && ${prependBridgeApiBaseUrlEnvAssignment(invocation, bridgeApiBaseUrl, "darwin")}`;
|
|
1462
1501
|
}
|
|
1463
1502
|
/** PowerShell agent shell command: `Set-Location -LiteralPath '<path>'; <agent> [--model '<alias>'] '<prompt>'`. */
|
|
1464
|
-
export function buildPowerShellAgentShellCommand(agent, key, worktreePath, autoApprove = false, modelAlias, conductorEnabled = false, resumeMode = false, workflow = "implement", reviewRounds, baseBranch, launchArgs = [], bridgeApiBaseUrl) {
|
|
1465
|
-
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);
|
|
1466
1505
|
return `Set-Location -LiteralPath ${powershellSquote(worktreePath)}; ${prependBridgeApiBaseUrlEnvAssignment(invocation, bridgeApiBaseUrl, "win32")}`;
|
|
1467
1506
|
}
|
|
1468
1507
|
/**
|
|
@@ -1473,10 +1512,10 @@ export function buildPowerShellAgentShellCommand(agent, key, worktreePath, autoA
|
|
|
1473
1512
|
* injected as `--model` at the spawn boundary. `conductorEnabled` appends the
|
|
1474
1513
|
* BAPI-586 PR-base contract instruction to the prompt (opt-in via `--conductor`).
|
|
1475
1514
|
*/
|
|
1476
|
-
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) {
|
|
1477
1516
|
if (platform === "win32")
|
|
1478
|
-
return buildPowerShellAgentShellCommand(agent, key, worktreePath, autoApprove, modelAlias, conductorEnabled, resumeMode, workflow, reviewRounds, baseBranch, launchArgs, bridgeApiBaseUrl);
|
|
1479
|
-
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);
|
|
1480
1519
|
}
|
|
1481
1520
|
/**
|
|
1482
1521
|
* Build the shell command run inside a spawned tab/session for an ARBITRARY
|
|
@@ -2104,7 +2143,7 @@ export function buildDryRunResults(keys, overrides) {
|
|
|
2104
2143
|
* PowerShell on Windows, `wt` + POSIX on macOS/Linux, and a non-throwing `wt` +
|
|
2105
2144
|
* POSIX fallback for unsupported platforms.
|
|
2106
2145
|
*/
|
|
2107
|
-
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) {
|
|
2108
2147
|
return {
|
|
2109
2148
|
worktrunkBinary: resolveWorktrunkBinary(platform, env),
|
|
2110
2149
|
// The builder accepts an optional resolved modelAlias; the dry-run caller
|
|
@@ -2117,7 +2156,7 @@ export function getDryRunPlatformDetails(agent, platform = process.platform, env
|
|
|
2117
2156
|
// buildAgentShellCommand/buildAgentPrompt path as a real spawn. A Codex
|
|
2118
2157
|
// dry-run has no created worktree to bind, so its preview omits the
|
|
2119
2158
|
// process-local -C/-c MCP scope that the real spawn resolves later.
|
|
2120
|
-
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),
|
|
2121
2160
|
};
|
|
2122
2161
|
}
|
|
2123
2162
|
/**
|
|
@@ -2156,8 +2195,8 @@ export function buildDryRunMcpProvisioningLines(worktreePath, platform = process
|
|
|
2156
2195
|
* the secret-free MCP provisioning preview. Pure platform formatting only — no
|
|
2157
2196
|
* preflight, no routing failures.
|
|
2158
2197
|
*/
|
|
2159
|
-
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) {
|
|
2160
|
-
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);
|
|
2161
2200
|
const wtArgs = buildWtSwitchArgs(branch, false, baseBranch);
|
|
2162
2201
|
const agentInvocation = build(key, "<worktree-path>", modelAlias);
|
|
2163
2202
|
// BAPI-801/BAPI-847: one concise secondary detail naming the PR base, emitted
|
|
@@ -2382,6 +2421,12 @@ export function formatModelRoutingDiagnosticLine(d) {
|
|
|
2382
2421
|
case "posture-unavailable":
|
|
2383
2422
|
return ("model routing: optimization posture was unavailable; Quality routing was retained " +
|
|
2384
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.");
|
|
2385
2430
|
default:
|
|
2386
2431
|
return `model routing degraded: ${d.message} ${failOpen}`;
|
|
2387
2432
|
}
|
|
@@ -2749,14 +2794,32 @@ export async function fetchCodexModelCatalog(access) {
|
|
|
2749
2794
|
return invalid();
|
|
2750
2795
|
return { ok: true, catalog: { models: models, revision } };
|
|
2751
2796
|
}
|
|
2797
|
+
/**
|
|
2798
|
+
* True only when stored thresholds decided the tier (`configured` or
|
|
2799
|
+
* `anomaly`). The merged resolver maps every `anomaly` tier to `premium` as a
|
|
2800
|
+
* fail-safe, so a threshold-decided tier is final and must never be downshifted.
|
|
2801
|
+
*
|
|
2802
|
+
* A `source: "fallback"` response is never threshold-decided: the server could
|
|
2803
|
+
* not obtain a difficulty and assumed premium, even though it still reports the
|
|
2804
|
+
* stored thresholds' mode. Treating it as decided would give review tabs a
|
|
2805
|
+
* premium fallback and skip the maximize downshift for a guess.
|
|
2806
|
+
*/
|
|
2807
|
+
export function isThresholdDecidedTierResponse(value) {
|
|
2808
|
+
if (value.source === "fallback")
|
|
2809
|
+
return false;
|
|
2810
|
+
return value.thresholdMode === "configured" || value.thresholdMode === "anomaly";
|
|
2811
|
+
}
|
|
2752
2812
|
/**
|
|
2753
2813
|
* Fetch the coarse tier for one ticket with a bounded timeout. Never throws to
|
|
2754
2814
|
* the batch orchestrator: failures/timeouts return a structured warning plus a
|
|
2755
2815
|
* classified diagnostic that preserves the ticket key in metadata.
|
|
2756
2816
|
*/
|
|
2757
|
-
export async function fetchTicketModelTierForStartTickets(access, ticket) {
|
|
2817
|
+
export async function fetchTicketModelTierForStartTickets(access, ticket, fetchOptions = {}) {
|
|
2758
2818
|
try {
|
|
2759
|
-
const
|
|
2819
|
+
const params = { repo_name: access.repoName };
|
|
2820
|
+
if (fetchOptions.consumer)
|
|
2821
|
+
params.consumer = fetchOptions.consumer;
|
|
2822
|
+
const url = buildStartTicketsJiraUrl(access.baseUrl, `/tickets/${encodeURIComponent(ticket)}/model-tier`, params);
|
|
2760
2823
|
const body = await fetchJsonWithTimeout(url, startTicketsGetHeaders(access), TICKET_MODEL_TIER_FETCH_TIMEOUT_MS);
|
|
2761
2824
|
const obj = (body ?? {});
|
|
2762
2825
|
const tier = isModelTier(obj.tier) ? obj.tier : null;
|
|
@@ -2764,7 +2827,17 @@ export async function fetchTicketModelTierForStartTickets(access, ticket) {
|
|
|
2764
2827
|
const source = obj.source === "cached" || obj.source === "computed" || obj.source === "fallback"
|
|
2765
2828
|
? obj.source
|
|
2766
2829
|
: "fallback";
|
|
2767
|
-
|
|
2830
|
+
const value = { difficulty, tier, source };
|
|
2831
|
+
// BAPI-1343: additive fields are attached only when the server sent them,
|
|
2832
|
+
// so an older server's response parses to exactly the pre-feature shape.
|
|
2833
|
+
if (obj.threshold_mode !== undefined && obj.threshold_mode !== null) {
|
|
2834
|
+
value.thresholdMode =
|
|
2835
|
+
obj.threshold_mode === "configured" || obj.threshold_mode === "anomaly" ? obj.threshold_mode : "legacy";
|
|
2836
|
+
}
|
|
2837
|
+
if (obj.consumer === "review" || obj.consumer === "implementation") {
|
|
2838
|
+
value.consumer = obj.consumer;
|
|
2839
|
+
}
|
|
2840
|
+
return { ok: true, value };
|
|
2768
2841
|
}
|
|
2769
2842
|
catch (err) {
|
|
2770
2843
|
return {
|
|
@@ -2779,15 +2852,23 @@ export async function fetchTicketModelTierForStartTickets(access, ticket) {
|
|
|
2779
2852
|
};
|
|
2780
2853
|
}
|
|
2781
2854
|
}
|
|
2855
|
+
/**
|
|
2856
|
+
* Fetch tiers for ticket keys with bounded parallelism (capped at 3). One
|
|
2857
|
+
* ticket's failure never aborts the others; results are keyed by ticket. The
|
|
2858
|
+
* shared bounded fetch behind both `start-tickets` and `/review-tickets`.
|
|
2859
|
+
*/
|
|
2860
|
+
export async function fetchTicketModelTiersForKeys(access, keys, maxParallel, fetchOptions = {}) {
|
|
2861
|
+
const limit = Math.min(maxParallel, 3);
|
|
2862
|
+
const results = await runWithConcurrency(keys, limit, (key) => fetchTicketModelTierForStartTickets(access, key, fetchOptions).then((res) => [key, res]));
|
|
2863
|
+
return new Map(results);
|
|
2864
|
+
}
|
|
2782
2865
|
/**
|
|
2783
2866
|
* Fetch tiers for the still-spawnable rows with bounded parallelism (capped at
|
|
2784
2867
|
* 3). One ticket's failure never aborts the others; results are keyed by ticket.
|
|
2785
2868
|
*/
|
|
2786
|
-
export async function fetchTicketModelTiersForRows(access, rows, maxParallel, isEligible = (r) => r.status === "created" && !!r.path) {
|
|
2869
|
+
export async function fetchTicketModelTiersForRows(access, rows, maxParallel, isEligible = (r) => r.status === "created" && !!r.path, fetchOptions = {}) {
|
|
2787
2870
|
const eligible = rows.filter(isEligible);
|
|
2788
|
-
|
|
2789
|
-
const results = await runWithConcurrency(eligible, limit, (row) => fetchTicketModelTierForStartTickets(access, row.key).then((res) => [row.key, res]));
|
|
2790
|
-
return new Map(results);
|
|
2871
|
+
return fetchTicketModelTiersForKeys(access, eligible.map((row) => row.key), maxParallel, fetchOptions);
|
|
2791
2872
|
}
|
|
2792
2873
|
/**
|
|
2793
2874
|
* Parse a model-list command's stdout into a set of advertised model aliases.
|
|
@@ -2874,7 +2955,15 @@ export function applyDefaultModelRoutingMetadata(row, reason, diagnostic) {
|
|
|
2874
2955
|
...(diagnostic ? { modelRoutingDiagnostic: diagnostic } : {}),
|
|
2875
2956
|
};
|
|
2876
2957
|
}
|
|
2877
|
-
|
|
2958
|
+
/**
|
|
2959
|
+
* Map a coarse tier to the agent's alias: the Codex authenticated catalog for
|
|
2960
|
+
* `codex`, otherwise the agent registry plus per-repo
|
|
2961
|
+
* `difficulty_model_tier_overrides`. Exported (BAPI-1343) so `/review-tickets`
|
|
2962
|
+
* maps review-threshold tiers exactly as `start-tickets` does, without copying
|
|
2963
|
+
* the registry or catalog rules. The result still needs
|
|
2964
|
+
* {@link validateResolvedModelAliasForAgent} before it reaches a shell.
|
|
2965
|
+
*/
|
|
2966
|
+
export function resolveRoutingAlias(agent, tier, overrides, catalog) {
|
|
2878
2967
|
return agent.name === "codex"
|
|
2879
2968
|
? catalog?.models[tier] ?? null
|
|
2880
2969
|
: resolveModelAlias(agent, tier, overrides);
|
|
@@ -2951,8 +3040,12 @@ const isDryRunRoutingEligible = (r) => r.status === "dry-run";
|
|
|
2951
3040
|
* routing (posture `not_evaluated_manual_override`) and for `quality`/`speed`;
|
|
2952
3041
|
* the exact transition token — including the `cheap` floor — under
|
|
2953
3042
|
* `maximize_subscription`. Total and exhaustive over {@link ModelTier}.
|
|
3043
|
+
* BAPI-1343: `none` whenever a `threshold` marker is supplied, because a
|
|
3044
|
+
* threshold-decided tier is final and is never downshifted.
|
|
2954
3045
|
*/
|
|
2955
|
-
function resolveModelRoutingDecisionAdjustment(posture, baseline) {
|
|
3046
|
+
function resolveModelRoutingDecisionAdjustment(posture, baseline, threshold) {
|
|
3047
|
+
if (threshold)
|
|
3048
|
+
return "none";
|
|
2956
3049
|
if (posture !== "maximize_subscription")
|
|
2957
3050
|
return "none";
|
|
2958
3051
|
switch (baseline) {
|
|
@@ -2973,7 +3066,65 @@ function resolveModelRoutingDecisionAdjustment(posture, baseline) {
|
|
|
2973
3066
|
export function formatModelRoutingDecisionDiagnosticLine(input) {
|
|
2974
3067
|
return (`[model-routing] source=${input.source} baseline=${input.baseline} ` +
|
|
2975
3068
|
`posture=${input.posture} adjustment=${input.adjustment} effective=${input.effective} ` +
|
|
2976
|
-
`alias=${input.alias ?? "unresolved"}`
|
|
3069
|
+
`alias=${input.alias ?? "unresolved"}` +
|
|
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
|
+
};
|
|
3107
|
+
}
|
|
3108
|
+
/**
|
|
3109
|
+
* BAPI-1343 R-6: the threshold marker for a successfully fetched `/model-tier`
|
|
3110
|
+
* response when — and only when — the maximize downshift must be skipped:
|
|
3111
|
+
* posture is `maximize_subscription` AND the response is threshold-decided
|
|
3112
|
+
* (`configured` or `anomaly`). Every other case returns `undefined`, so the
|
|
3113
|
+
* caller keeps today's exact downshift and text.
|
|
3114
|
+
*/
|
|
3115
|
+
function thresholdSkipMarker(posture, tierResult, reviewWorkflow = false) {
|
|
3116
|
+
if (posture !== "maximize_subscription")
|
|
3117
|
+
return undefined;
|
|
3118
|
+
if (!tierResult || !tierResult.ok)
|
|
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;
|
|
3125
|
+
if (!isThresholdDecidedTierResponse(tierResult.value))
|
|
3126
|
+
return undefined;
|
|
3127
|
+
return tierResult.value.thresholdMode;
|
|
2977
3128
|
}
|
|
2978
3129
|
/**
|
|
2979
3130
|
* Best-effort emitter: calls the supplied stderr sink once and swallows any
|
|
@@ -3140,6 +3291,12 @@ errorLog = (message) => console.error(message)) {
|
|
|
3140
3291
|
// produced it.
|
|
3141
3292
|
const postureResolution = await resolveOptimizationPostureForStartTickets(access);
|
|
3142
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" } : {};
|
|
3143
3300
|
// BAPI-1225: one deterministic, best-effort diagnostic line per repository-level
|
|
3144
3301
|
// resolution, emitted exactly here — outside the per-ticket row loop below, so a
|
|
3145
3302
|
// multi-ticket invocation still logs it once for this canonical repository. Uses
|
|
@@ -3160,36 +3317,70 @@ errorLog = (message) => console.error(message)) {
|
|
|
3160
3317
|
// chained automation handoff (e.g. `/review-and-implement`'s `--auto-tier`).
|
|
3161
3318
|
// Routed through the CURRENT session's posture EXACTLY ONCE — unlike the
|
|
3162
3319
|
// manual override above, this path IS subject to `downshiftModelTier()`
|
|
3163
|
-
// under `maximize_subscription` — while
|
|
3164
|
-
// per-ticket
|
|
3165
|
-
//
|
|
3166
|
-
//
|
|
3167
|
-
|
|
3168
|
-
|
|
3169
|
-
|
|
3170
|
-
|
|
3171
|
-
|
|
3172
|
-
|
|
3173
|
-
|
|
3174
|
-
|
|
3175
|
-
|
|
3176
|
-
|
|
3177
|
-
|
|
3178
|
-
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
|
|
3320
|
+
// under `maximize_subscription` — while never re-deriving the tier from a
|
|
3321
|
+
// per-ticket lookup. The alias is resolved+validated at most ONCE per
|
|
3322
|
+
// distinct effective tier, matching the premium fallback / injected-tier
|
|
3323
|
+
// hoisting pattern above.
|
|
3324
|
+
//
|
|
3325
|
+
// BAPI-1343 R-6: under `maximize_subscription` ONLY, each eligible row's
|
|
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
|
|
3328
|
+
// (`configured`/`anomaly`) response keeps the carried tier — stored
|
|
3329
|
+
// thresholds made it final. A fetch failure, `legacy`, or anything else
|
|
3330
|
+
// downshifts exactly as before. `quality`/`speed` never downshift, so they
|
|
3331
|
+
// make no extra request.
|
|
3332
|
+
const thresholdTierMap = posture === "maximize_subscription"
|
|
3333
|
+
? await fetchTicketModelTiersForRows(access, eligible, options.maxParallel, isEligible, tierFetchOptions)
|
|
3334
|
+
: null;
|
|
3335
|
+
const aliasByEffectiveTier = new Map();
|
|
3336
|
+
const resolveAutoTierAlias = async (effectiveTier) => {
|
|
3337
|
+
const cached = aliasByEffectiveTier.get(effectiveTier);
|
|
3338
|
+
if (cached)
|
|
3339
|
+
return cached;
|
|
3340
|
+
const alias = resolveRoutingAlias(agent, effectiveTier, overrides, catalog);
|
|
3341
|
+
let resolved;
|
|
3342
|
+
if (!alias) {
|
|
3343
|
+
resolved = {
|
|
3344
|
+
resolvedAlias: null,
|
|
3345
|
+
aliasWarning: `model routing: no valid alias for tier=${effectiveTier}; using agent default`,
|
|
3346
|
+
};
|
|
3182
3347
|
}
|
|
3183
3348
|
else {
|
|
3184
|
-
|
|
3349
|
+
const validation = await validateResolvedModelAliasForAgent(deps, agent, alias);
|
|
3350
|
+
resolved = validation.ok
|
|
3351
|
+
? { resolvedAlias: validation.alias, aliasWarning: null }
|
|
3352
|
+
: { resolvedAlias: null, aliasWarning: validation.warning };
|
|
3185
3353
|
}
|
|
3186
|
-
|
|
3187
|
-
|
|
3188
|
-
|
|
3189
|
-
|
|
3190
|
-
|
|
3191
|
-
if (!isEligible(row))
|
|
3192
|
-
|
|
3354
|
+
aliasByEffectiveTier.set(effectiveTier, resolved);
|
|
3355
|
+
return resolved;
|
|
3356
|
+
};
|
|
3357
|
+
const autoOut = [];
|
|
3358
|
+
for (const row of rows) {
|
|
3359
|
+
if (!isEligible(row)) {
|
|
3360
|
+
autoOut.push(row);
|
|
3361
|
+
continue;
|
|
3362
|
+
}
|
|
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;
|
|
3371
|
+
const effectiveTier = posture === "maximize_subscription" && !threshold
|
|
3372
|
+
? downshiftModelTier(autoTier)
|
|
3373
|
+
: autoTier;
|
|
3374
|
+
const adjustment = resolveModelRoutingDecisionAdjustment(posture, autoTier, threshold);
|
|
3375
|
+
const lineageSuffix = threshold
|
|
3376
|
+
? `; threshold:${threshold}`
|
|
3377
|
+
: posture === "maximize_subscription"
|
|
3378
|
+
? `; auto-tier:${autoTier}->maximize:${effectiveTier}${markerCause ? `; review-marker:${markerCause}` : ""}`
|
|
3379
|
+
: "";
|
|
3380
|
+
const { resolvedAlias, aliasWarning } = await resolveAutoTierAlias(effectiveTier);
|
|
3381
|
+
const reason = resolvedAlias
|
|
3382
|
+
? `auto-tier=${autoTier} model=${resolvedAlias} (posture=${posture})${lineageSuffix}`
|
|
3383
|
+
: `${aliasWarning}${lineageSuffix}`;
|
|
3193
3384
|
const baseRow = {
|
|
3194
3385
|
...row,
|
|
3195
3386
|
difficulty: null,
|
|
@@ -3201,14 +3392,17 @@ errorLog = (message) => console.error(message)) {
|
|
|
3201
3392
|
? { modelRoutingDiagnostic: codexEntitlementDiagnostic(access.repoName) }
|
|
3202
3393
|
: {}),
|
|
3203
3394
|
};
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
|
|
3208
|
-
|
|
3209
|
-
|
|
3210
|
-
|
|
3211
|
-
|
|
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);
|
|
3403
|
+
// BAPI-1236: one `automatic_snapshot` diagnostic per eligible row — the
|
|
3404
|
+
// carried baseline, the newly resolved posture, the exact adjustment token,
|
|
3405
|
+
// the calculated effective tier, and the validated alias (or `unresolved`).
|
|
3212
3406
|
emitModelRoutingDecisionDiagnostic(errorLog, {
|
|
3213
3407
|
source: "automatic_snapshot",
|
|
3214
3408
|
baseline: autoTier,
|
|
@@ -3216,12 +3410,15 @@ errorLog = (message) => console.error(message)) {
|
|
|
3216
3410
|
adjustment,
|
|
3217
3411
|
effective: effectiveTier,
|
|
3218
3412
|
alias: resolvedAlias,
|
|
3413
|
+
...(threshold ? { threshold } : {}),
|
|
3414
|
+
...(isReviewWorkflow ? { phase: "review" } : {}),
|
|
3219
3415
|
});
|
|
3220
3416
|
}
|
|
3417
|
+
out = autoOut;
|
|
3221
3418
|
}
|
|
3222
3419
|
else {
|
|
3223
3420
|
// Ordinary per-ticket difficulty-lookup path.
|
|
3224
|
-
const tierMap = await fetchTicketModelTiersForRows(access, eligible, options.maxParallel, isEligible);
|
|
3421
|
+
const tierMap = await fetchTicketModelTiersForRows(access, eligible, options.maxParallel, isEligible, tierFetchOptions);
|
|
3225
3422
|
// Lazily resolve+validate the premium fallback alias at most once for this run
|
|
3226
3423
|
// (the alias is identical across rows; cursor-agent validation spawns a probe).
|
|
3227
3424
|
let premiumApplier = null;
|
|
@@ -3260,6 +3457,40 @@ errorLog = (message) => console.error(message)) {
|
|
|
3260
3457
|
continue;
|
|
3261
3458
|
}
|
|
3262
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
|
+
}
|
|
3263
3494
|
if (!tier) {
|
|
3264
3495
|
// Backend reported no usable difficulty — default to premium/Opus. (The
|
|
3265
3496
|
// backend now returns a premium fallback itself, so this is defensive.)
|
|
@@ -3298,12 +3529,27 @@ errorLog = (message) => console.error(message)) {
|
|
|
3298
3529
|
// alias table. Copilot never reaches this loop at all: it has no
|
|
3299
3530
|
// `AGENT_REGISTRY` entry and `isRunnableHostTarget()` rejects hosts
|
|
3300
3531
|
// without a launch agent before an `AgentSpec` is ever resolved.
|
|
3532
|
+
//
|
|
3533
|
+
// BAPI-1343 R-6: the response is already in hand — when it is
|
|
3534
|
+
// threshold-decided (`configured`/`anomaly`), stored thresholds made the
|
|
3535
|
+
// tier final, so the maximize downshift is skipped and the lineage and
|
|
3536
|
+
// diagnostic carry a distinct `threshold` marker instead. The harness-
|
|
3537
|
+
// local premium fallback above is untouched.
|
|
3301
3538
|
const difficultyTier = tier;
|
|
3302
|
-
const
|
|
3303
|
-
const
|
|
3304
|
-
|
|
3305
|
-
|
|
3306
|
-
|
|
3539
|
+
const threshold = thresholdSkipMarker(posture, tierResult, isReviewWorkflow);
|
|
3540
|
+
const effectiveTier = posture === "maximize_subscription" && !threshold
|
|
3541
|
+
? downshiftModelTier(difficultyTier)
|
|
3542
|
+
: difficultyTier;
|
|
3543
|
+
const adjustment = resolveModelRoutingDecisionAdjustment(posture, difficultyTier, threshold);
|
|
3544
|
+
const lineageSuffix = threshold
|
|
3545
|
+
? `; threshold:${threshold}`
|
|
3546
|
+
: posture === "maximize_subscription"
|
|
3547
|
+
? `; difficulty:${difficultyTier}->maximize:${effectiveTier}`
|
|
3548
|
+
: "";
|
|
3549
|
+
const thresholdField = {
|
|
3550
|
+
...(threshold ? { threshold } : {}),
|
|
3551
|
+
...(isReviewWorkflow ? { phase: "review" } : {}),
|
|
3552
|
+
};
|
|
3307
3553
|
// BAPI-1236: stamp `difficulty_lookup` rather than copying the backend
|
|
3308
3554
|
// cache/computation source (`cached`/`computed`/`fallback`) into the
|
|
3309
3555
|
// summary's own routing-provenance field — that backend source is
|
|
@@ -3325,6 +3571,7 @@ errorLog = (message) => console.error(message)) {
|
|
|
3325
3571
|
adjustment,
|
|
3326
3572
|
effective: effectiveTier,
|
|
3327
3573
|
alias: null,
|
|
3574
|
+
...thresholdField,
|
|
3328
3575
|
});
|
|
3329
3576
|
continue;
|
|
3330
3577
|
}
|
|
@@ -3338,6 +3585,7 @@ errorLog = (message) => console.error(message)) {
|
|
|
3338
3585
|
adjustment,
|
|
3339
3586
|
effective: effectiveTier,
|
|
3340
3587
|
alias: null,
|
|
3588
|
+
...thresholdField,
|
|
3341
3589
|
});
|
|
3342
3590
|
continue;
|
|
3343
3591
|
}
|
|
@@ -3356,6 +3604,7 @@ errorLog = (message) => console.error(message)) {
|
|
|
3356
3604
|
adjustment,
|
|
3357
3605
|
effective: effectiveTier,
|
|
3358
3606
|
alias: validation.alias,
|
|
3607
|
+
...thresholdField,
|
|
3359
3608
|
});
|
|
3360
3609
|
}
|
|
3361
3610
|
out = loopOut;
|
|
@@ -3652,7 +3901,7 @@ export async function orchestrateStartTickets(deps, options, overrides = {}) {
|
|
|
3652
3901
|
// epic.base_branch override applied above) so the spawned worker gets
|
|
3653
3902
|
// BAPI_BASE_BRANCH — every conductor worker, and a plain worker whose base
|
|
3654
3903
|
// is non-default.
|
|
3655
|
-
options.baseBranch, bridgeApiBaseUrl);
|
|
3904
|
+
options.baseBranch, bridgeApiBaseUrl, isModelTier(options.injectedTier) ? options.injectedTier : undefined);
|
|
3656
3905
|
if (!platformConfig.ok)
|
|
3657
3906
|
return { ok: false, error: platformConfig.error };
|
|
3658
3907
|
// BAPI-527: resolve the base start point new worktrees are cut from BEFORE any
|
|
@@ -3681,6 +3930,11 @@ export async function orchestrateStartTickets(deps, options, overrides = {}) {
|
|
|
3681
3930
|
});
|
|
3682
3931
|
if (!refresh.ok)
|
|
3683
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
|
+
}
|
|
3684
3938
|
}
|
|
3685
3939
|
// BAPI-941: the `claude-review` workflow-drift advisory, emitted ONCE per
|
|
3686
3940
|
// worker spawn at the one choke point both dispatch paths pass through, after
|
|
@@ -3835,14 +4089,13 @@ export async function orchestrateStartTickets(deps, options, overrides = {}) {
|
|
|
3835
4089
|
const rows = await spawnTabsFn(deps, scoped, terminal, platformConfig.config.buildAgentShellCommand);
|
|
3836
4090
|
return { ok: true, rows };
|
|
3837
4091
|
}
|
|
3838
|
-
/**
|
|
3839
|
-
|
|
4092
|
+
/**
|
|
4093
|
+
* Platform-specific guidance printed when one or more tabs fail to spawn.
|
|
4094
|
+
* Exported for the BAPI-1319 byte-identity pin on the darwin sentence.
|
|
4095
|
+
*/
|
|
4096
|
+
export function spawnFailureHintForPlatform(platform) {
|
|
3840
4097
|
if (platform === "darwin") {
|
|
3841
|
-
return
|
|
3842
|
-
"required macOS permissions under System Settings -> Privacy & Security, then re-run: " +
|
|
3843
|
-
"Automation (to let the terminal app be controlled) and, for the Terminal.app path, " +
|
|
3844
|
-
"Accessibility (the new-tab step sends Cmd-T via System Events keystrokes, which Automation " +
|
|
3845
|
-
"alone does not cover).");
|
|
4098
|
+
return "One or more tabs failed to spawn. " + macosTerminalPermissionGuidance();
|
|
3846
4099
|
}
|
|
3847
4100
|
if (platform === "win32") {
|
|
3848
4101
|
return ("One or more tabs failed to spawn. Ensure Windows Terminal (wt.exe) or PowerShell is " +
|
|
@@ -3918,7 +4171,7 @@ export async function runStartTicketsCli(argv, overrides = {}) {
|
|
|
3918
4171
|
const branch = resolveBranchForTicket(key, options.branchOverrides);
|
|
3919
4172
|
const routedRow = routedByKey.get(key);
|
|
3920
4173
|
const modelAlias = routedRow?.modelAlias ?? null;
|
|
3921
|
-
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)) {
|
|
3922
4175
|
log(line);
|
|
3923
4176
|
}
|
|
3924
4177
|
log(`DRY-RUN: model routing: ${formatModelRoutingLine(routedRow ?? { key, branch, status: "dry-run" }, agent, { workflow: options.workflow })}`);
|