@bridge_gpt/mcp-server 0.2.66 → 0.2.68
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 +421 -83
- package/build/agent-capabilities/probes.js +15 -5
- package/build/agent-launchers/claude-executor-adapter.js +70 -16
- package/build/agent-launchers/executor-adapter.js +7 -4
- package/build/agents.generated.js +22 -2
- package/build/bitbucket-workspace-remote.js +136 -0
- package/build/bootstrap-project-identity.js +24 -0
- package/build/bounded-wait.js +67 -10
- package/build/bridge-permissions.js +829 -0
- package/build/claude-settings.js +18 -0
- package/build/cli-help.js +21 -0
- package/build/command-assets-doctor.js +24 -70
- package/build/command-provisioning.js +29 -5
- package/build/commands.generated.js +12 -12
- package/build/conductor/bridge-api-client.js +857 -1
- package/build/conductor/bring-up-facts.js +1 -1
- package/build/conductor/command-drift-production.js +92 -0
- package/build/conductor/command-drift.js +89 -0
- package/build/conductor/doctor.js +72 -29
- package/build/conductor/epic-branch-ci-inspection.js +1375 -0
- package/build/conductor/epic-implementer-cli.js +98 -28
- package/build/conductor/errors.js +4 -3
- package/build/conductor/gate-discovery.js +890 -0
- package/build/conductor/guided-repair.js +6 -2
- package/build/conductor/hook-runtime-qualification.js +1 -1
- package/build/conductor/install-doctor.js +15 -3
- package/build/conductor/json-output.js +63 -0
- 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 +113 -12
- package/build/conductor/readiness-sections.js +410 -9
- package/build/conductor/readiness.js +388 -33
- package/build/conductor/recovery-cli.js +310 -12
- package/build/conductor/recovery-operations.js +167 -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/connect-bitbucket-api.js +41 -6
- package/build/connect-bitbucket.js +106 -33
- package/build/credential-store.js +207 -47
- package/build/docs.generated.js +62 -3
- package/build/doctor.js +92 -1
- package/build/drive-epic-key.js +6 -5
- package/build/drive-epic.js +129 -61
- package/build/epic-coherence-input.js +2 -1
- package/build/epic-coherence-input.test-fixtures.js +4 -3
- package/build/epic-implementer-bin.js +1379 -28
- package/build/epic-implementer-bundle-cli.js +2 -2
- package/build/epic-workarounds-path.js +39 -0
- package/build/executor/cli.js +33 -2
- package/build/executor/completion-contract.js +66 -10
- package/build/executor/completion-supervision.js +4 -0
- package/build/executor/deps.js +4 -1
- package/build/executor/env.js +19 -0
- package/build/executor/gate-qualification-job.js +499 -0
- package/build/executor/guarded-fast-forward-push.js +129 -0
- package/build/executor/heartbeat.js +1 -1
- package/build/executor/http-client.js +20 -0
- package/build/executor/integration-job.js +122 -8
- package/build/executor/job-errors.js +33 -1
- package/build/executor/job-runner.js +813 -87
- package/build/executor/likely-tests.js +290 -0
- package/build/executor/live-worker-registry.js +1 -1
- package/build/executor/observation.js +37 -3
- package/build/executor/packaged-recipe.js +106 -0
- package/build/executor/permissions.js +115 -28
- package/build/executor/platform.js +1 -1
- package/build/executor/preflight.js +112 -26
- package/build/executor/process-containment.js +36 -12
- package/build/executor/process.js +15 -7
- package/build/executor/prompt-spec.js +33 -1
- package/build/executor/recently-failed-tests-file.js +71 -0
- package/build/executor/reconcile-overlap-job.js +41 -23
- package/build/executor/required-verification.js +1 -1
- package/build/executor/required-work-ledger.js +111 -18
- package/build/executor/resume-pre-spawn.js +2 -0
- package/build/executor/retirement.js +69 -0
- package/build/executor/runner.js +122 -13
- package/build/executor/service-unit.js +7 -1
- package/build/executor/test-clock.js +15 -0
- package/build/executor/types.js +32 -0
- package/build/executor/viewer-tabs.js +58 -8
- package/build/executor/worker-command.js +23 -7
- package/build/executor/worker-diagnostics.js +37 -0
- package/build/executor/worker-finalization.js +113 -34
- package/build/executor/worker-guard-hook.js +316 -58
- package/build/executor/worker-log.js +4 -3
- package/build/executor/worker-verification-guidance.js +37 -0
- package/build/executor/worktree.js +166 -8
- package/build/first-build-advisory.js +97 -0
- package/build/generated-assets-advisory.js +14 -0
- package/build/generated-assets-catalog.js +66 -0
- package/build/generated-assets-context.js +46 -0
- package/build/generated-assets-dependencies.js +26 -0
- package/build/generated-assets-doctor.js +50 -0
- package/build/generated-assets-health.js +65 -0
- package/build/generated-assets-host.js +17 -0
- package/build/generated-assets-inventory.js +17 -0
- package/build/generated-assets-paths.js +60 -0
- package/build/generated-assets-publisher.js +133 -0
- package/build/generated-assets-remedy.js +25 -0
- package/build/generated-assets-repair.js +72 -0
- package/build/generated-assets-session.js +71 -0
- package/build/generated-assets-test-support.js +35 -0
- package/build/index.js +1224 -1054
- package/build/init.js +54 -177
- package/build/install-bridge-conductor-gate.js +1372 -0
- package/build/install-bridge-conductor.js +296 -41
- package/build/install-bridge.js +547 -40
- package/build/mcp-install-state.js +50 -4
- package/build/merge-workflow-install.js +3 -4
- package/build/phase-context-git-probe.js +68 -0
- package/build/phase-result-artifacts.js +173 -5
- package/build/pipeline-utils.js +6 -2
- package/build/pipelines.generated.js +34 -160
- package/build/plan-execution-ledger.js +2 -32
- 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 +136 -2
- package/build/plane/member-roster.js +94 -19
- package/build/plane/preflight.js +83 -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 +52 -0
- package/build/readiness-check.js +1 -1
- package/build/readme.generated.js +1 -1
- package/build/repair-assets.js +93 -0
- package/build/review-generation.js +22 -0
- package/build/run-unit-tests-launcher.js +130 -49
- package/build/secret-redaction.js +33 -0
- package/build/setup-epic.js +152 -22
- 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-prereqs.js +70 -16
- package/build/start-tickets.js +208 -36
- package/build/tool-error-envelope.js +61 -0
- package/build/tool-surface-gating.js +36 -0
- package/build/upgrade-cli.js +55 -1
- package/build/version.generated.js +3 -3
- package/build/visual-diff.js +22 -1
- package/build/worker-guard-hook-bin.js +4 -6
- package/build/worker-guard-hook-main.js +47 -0
- package/build/worktree-core.js +145 -6
- package/docs/EPIC-IMPLEMENTER-INSTALL.md +867 -0
- package/docs/EPIC-IMPLEMENTER.md +728 -0
- package/docs/install/mcp-tool-integrations.md +24 -15
- package/docs/install/sfcc-integration.md +12 -4
- package/package.json +3 -3
- package/pipelines/greenfield-setup.json +4 -128
- package/pipelines/implement-ticket.json +3 -2
- 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
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { recordProvisionedAssets } from "./generated-assets-inventory.js";
|
|
1
2
|
/**
|
|
2
3
|
* start-tickets — packaged CLI subcommand of the bridge-api-mcp-server bin.
|
|
3
4
|
*
|
|
@@ -136,7 +137,7 @@ export function getStartTicketsUsage() {
|
|
|
136
137
|
" --host claude-code|cursor|codex|copilot-vscode|windsurf Current host when --agent is omitted; Claude Code, Cursor, and Codex can launch sessions",
|
|
137
138
|
" Cursor with --auto launches headlessly after a bounded readiness check; without --auto it stays interactive for the review gate.",
|
|
138
139
|
" --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.",
|
|
140
|
+
" --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
141
|
" --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
142
|
" --rounds 1|2 Review round count forwarded to the review phase; review-only, valid only with --workflow review-and-implement",
|
|
142
143
|
" --terminal terminal|iterm Override the macOS terminal app (default: auto-detect via $TERM_PROGRAM); honored on macOS only",
|
|
@@ -619,7 +620,9 @@ resumeMode = false, workflow = "implement", reviewRounds,
|
|
|
619
620
|
// conductor runs always, and (BAPI-801/BAPI-847) for every plain run whose base
|
|
620
621
|
// resolved, `main` included — and threaded (BAPI-593) into the spawned workflow
|
|
621
622
|
// command.
|
|
622
|
-
baseBranch, bridgeApiBaseUrlOverride
|
|
623
|
+
baseBranch, bridgeApiBaseUrlOverride,
|
|
624
|
+
// BAPI-1365: a recognized operator `--tier`, forwarded to the review prompt.
|
|
625
|
+
manualTier) {
|
|
623
626
|
if (!isSupportedStartTicketsPlatform(deps.platform)) {
|
|
624
627
|
return { ok: false, error: unsupportedPlatformMessage(deps.platform) };
|
|
625
628
|
}
|
|
@@ -640,7 +643,7 @@ baseBranch, bridgeApiBaseUrlOverride) {
|
|
|
640
643
|
// falls back to the basename-derived repo name (the 403 root cause), and
|
|
641
644
|
// (BAPI-586/BAPI-801) the run base so the spawned worker opens its PR
|
|
642
645
|
// 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),
|
|
646
|
+
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
647
|
spawnTerminalTab: deps.spawnTerminalTab,
|
|
645
648
|
},
|
|
646
649
|
};
|
|
@@ -834,6 +837,7 @@ export function createDefaultStartTicketsDeps() {
|
|
|
834
837
|
encoding: "utf-8",
|
|
835
838
|
// Optional bounded timeout (e.g. cursor-agent --list-models probes).
|
|
836
839
|
timeout: options?.timeoutMs,
|
|
840
|
+
signal: options?.signal,
|
|
837
841
|
}, (error, stdout, stderr) => {
|
|
838
842
|
const exitCode = error && typeof error.code === "number"
|
|
839
843
|
? (error.code)
|
|
@@ -844,6 +848,7 @@ export function createDefaultStartTicketsDeps() {
|
|
|
844
848
|
stdout: stdout ?? "",
|
|
845
849
|
stderr: stderr ?? "",
|
|
846
850
|
exitCode,
|
|
851
|
+
...(error && typeof error.code === "string" ? { unavailable: true } : {}),
|
|
847
852
|
});
|
|
848
853
|
});
|
|
849
854
|
});
|
|
@@ -1072,6 +1077,19 @@ export async function refreshBaseBranch(deps, options) {
|
|
|
1072
1077
|
// `<baseBranch>` is checked out somewhere: fast-forward it in place. git refuses to
|
|
1073
1078
|
// force-move a checked-out branch ref, so we must go through merge.
|
|
1074
1079
|
if (basePath) {
|
|
1080
|
+
// BAPI-1362: a live plane member may be running out of this very checkout.
|
|
1081
|
+
// Fail closed — an unreadable or ambiguous record protects it. When
|
|
1082
|
+
// protected, nothing local is moved: the caller cuts from the fetched
|
|
1083
|
+
// `origin/<base>` SHA through the same fetch-only resolution the
|
|
1084
|
+
// non-mutating path uses.
|
|
1085
|
+
const decision = await evaluateGuardSafely(deps, basePath);
|
|
1086
|
+
if (decision.kind === "protect") {
|
|
1087
|
+
(deps.baseWorktreeGuardWarningLog ?? defaultGuardWarningLog)(formatBaseWorktreeGuardWarning(decision, baseBranch));
|
|
1088
|
+
const resolved = await fetchAndResolveBaseSha(deps, baseBranch);
|
|
1089
|
+
if (!resolved.ok)
|
|
1090
|
+
return { ok: false, error: resolved.error };
|
|
1091
|
+
return { ok: true, protectedBaseSha: resolved.base_sha };
|
|
1092
|
+
}
|
|
1075
1093
|
const merge = await deps.runCommand("git", ["merge", "--ff-only", originRef], { cwd: basePath });
|
|
1076
1094
|
if (!commandSucceeded(merge)) {
|
|
1077
1095
|
return {
|
|
@@ -1103,6 +1121,25 @@ export async function refreshBaseBranch(deps, options) {
|
|
|
1103
1121
|
}
|
|
1104
1122
|
return { ok: true };
|
|
1105
1123
|
}
|
|
1124
|
+
function defaultGuardWarningLog(message) {
|
|
1125
|
+
// stderr, never stdout: stdout carries the workflow's normal output.
|
|
1126
|
+
process.stderr.write(`${message}\n`);
|
|
1127
|
+
}
|
|
1128
|
+
/** Run the guard; any failure of the guard itself is a `protect` (fail closed). */
|
|
1129
|
+
async function evaluateGuardSafely(deps, basePath) {
|
|
1130
|
+
try {
|
|
1131
|
+
if (deps.evaluateBaseWorktreeGuard)
|
|
1132
|
+
return await deps.evaluateBaseWorktreeGuard(basePath);
|
|
1133
|
+
return await evaluateBaseWorktreeGuard(basePath, await createBaseWorktreeGuardDeps());
|
|
1134
|
+
}
|
|
1135
|
+
catch {
|
|
1136
|
+
return {
|
|
1137
|
+
kind: "protect",
|
|
1138
|
+
reason: "manifest-unreadable",
|
|
1139
|
+
manifestLocation: ".bridge/plane/plane.json",
|
|
1140
|
+
};
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1106
1143
|
/**
|
|
1107
1144
|
* Compatibility wrapper around {@link refreshBaseBranch} that preserves the
|
|
1108
1145
|
* historical `refreshMainBranch(deps, { refreshMain })` signature for callers
|
|
@@ -1195,6 +1232,7 @@ async function emitClaudeReviewWorkflowDriftAdvisory(deps, baseBranch, overrides
|
|
|
1195
1232
|
// import the VALUES here (so internal uses keep working) and re-export the same
|
|
1196
1233
|
// references so existing importers of `./start-tickets.js` (index.ts,
|
|
1197
1234
|
// review-tickets.ts) and the pinned start-tickets tests are unaffected.
|
|
1235
|
+
import { evaluateBaseWorktreeGuard, createBaseWorktreeGuardDeps, formatBaseWorktreeGuardWarning, } from "./plane/base-worktree-guard.js";
|
|
1198
1236
|
import { validateBranchName, fetchAndResolveBaseSha,
|
|
1199
1237
|
// BAPI-941: both live in the same leaf module so the interactive refresh path
|
|
1200
1238
|
// and the non-mutating conductor path share one drift implementation.
|
|
@@ -1339,8 +1377,9 @@ export async function resumeWorktrees(deps, options) {
|
|
|
1339
1377
|
* E1/F3b proved a clean TEXTUAL merge can still hide a SEMANTIC break (branch A
|
|
1340
1378
|
* adds a helper; branch B adds a contract test over all helpers), which only the
|
|
1341
1379
|
* FULL suite catches — so this instruction requires the worker to run the full
|
|
1342
|
-
* project test suite (
|
|
1343
|
-
* push/complete on green, escalating otherwise.
|
|
1380
|
+
* project test suite (every unit suite the project defines, the same suites its
|
|
1381
|
+
* pull request CI runs) and only push/complete on green, escalating otherwise.
|
|
1382
|
+
* The wording stays project-neutral because this CLI runs in other repositories.
|
|
1344
1383
|
*
|
|
1345
1384
|
* Single line (no newlines, no `;`, no quotes/apostrophes) so it stays safe when
|
|
1346
1385
|
* single-quoted into a shell command and embedded in terminal-launcher args.
|
|
@@ -1351,8 +1390,8 @@ export function buildResumeModeRemediationFinalizeInstruction() {
|
|
|
1351
1390
|
"the current base branch and resolve the merge conflicts. A clean textual merge can " +
|
|
1352
1391
|
"still break behavior, so inspect for semantic conflicts even when there are no textual " +
|
|
1353
1392
|
"conflict markers. Before you push or mark the ticket complete, run the full test suite " +
|
|
1354
|
-
"for the project (the full unit suite
|
|
1355
|
-
"
|
|
1393
|
+
"for the project (the full unit suite: every unit test suite the project defines, the " +
|
|
1394
|
+
"same suites its pull request CI runs) " +
|
|
1356
1395
|
"and do not rely on targeted subsets as your only verification. Push and mark the ticket " +
|
|
1357
1396
|
"complete only after the full suite is green. If you cannot make the full suite pass, " +
|
|
1358
1397
|
"report the ticket blocked and escalate rather than pushing a green-looking but broken merge.");
|
|
@@ -1385,6 +1424,8 @@ export function buildAgentPrompt(key, opts = {}) {
|
|
|
1385
1424
|
// Review-only arguments (`--rounds`, non-default `--base-branch`) must never
|
|
1386
1425
|
// leak into the legacy `/implement-ticket` prompt.
|
|
1387
1426
|
if (workflow === "review-and-implement") {
|
|
1427
|
+
if (opts.manualTier !== undefined)
|
|
1428
|
+
command += ` --tier=${opts.manualTier}`;
|
|
1388
1429
|
if (opts.agentName === "cursor-agent")
|
|
1389
1430
|
command += " --agent=cursor-agent";
|
|
1390
1431
|
if (opts.reviewRounds !== undefined) {
|
|
@@ -1457,13 +1498,13 @@ export function buildAgentInvocation(agent, prompt, quote, modelAlias, launchArg
|
|
|
1457
1498
|
}
|
|
1458
1499
|
}
|
|
1459
1500
|
/** 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);
|
|
1501
|
+
export function buildPosixAgentShellCommand(agent, key, worktreePath, autoApprove = false, modelAlias, conductorEnabled = false, resumeMode = false, workflow = "implement", reviewRounds, baseBranch, launchArgs = [], bridgeApiBaseUrl, manualTier) {
|
|
1502
|
+
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
1503
|
return `cd '${shSquoteInner(worktreePath)}' && ${prependBridgeApiBaseUrlEnvAssignment(invocation, bridgeApiBaseUrl, "darwin")}`;
|
|
1463
1504
|
}
|
|
1464
1505
|
/** 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);
|
|
1506
|
+
export function buildPowerShellAgentShellCommand(agent, key, worktreePath, autoApprove = false, modelAlias, conductorEnabled = false, resumeMode = false, workflow = "implement", reviewRounds, baseBranch, launchArgs = [], bridgeApiBaseUrl, manualTier) {
|
|
1507
|
+
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
1508
|
return `Set-Location -LiteralPath ${powershellSquote(worktreePath)}; ${prependBridgeApiBaseUrlEnvAssignment(invocation, bridgeApiBaseUrl, "win32")}`;
|
|
1468
1509
|
}
|
|
1469
1510
|
/**
|
|
@@ -1474,10 +1515,10 @@ export function buildPowerShellAgentShellCommand(agent, key, worktreePath, autoA
|
|
|
1474
1515
|
* injected as `--model` at the spawn boundary. `conductorEnabled` appends the
|
|
1475
1516
|
* BAPI-586 PR-base contract instruction to the prompt (opt-in via `--conductor`).
|
|
1476
1517
|
*/
|
|
1477
|
-
export function buildAgentShellCommand(agent, key, worktreePath, platform = "darwin", autoApprove = false, modelAlias, conductorEnabled = false, resumeMode = false, workflow = "implement", reviewRounds, baseBranch, launchArgs = [], bridgeApiBaseUrl) {
|
|
1518
|
+
export function buildAgentShellCommand(agent, key, worktreePath, platform = "darwin", autoApprove = false, modelAlias, conductorEnabled = false, resumeMode = false, workflow = "implement", reviewRounds, baseBranch, launchArgs = [], bridgeApiBaseUrl, manualTier) {
|
|
1478
1519
|
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);
|
|
1520
|
+
return buildPowerShellAgentShellCommand(agent, key, worktreePath, autoApprove, modelAlias, conductorEnabled, resumeMode, workflow, reviewRounds, baseBranch, launchArgs, bridgeApiBaseUrl, manualTier);
|
|
1521
|
+
return buildPosixAgentShellCommand(agent, key, worktreePath, autoApprove, modelAlias, conductorEnabled, resumeMode, workflow, reviewRounds, baseBranch, launchArgs, bridgeApiBaseUrl, manualTier);
|
|
1481
1522
|
}
|
|
1482
1523
|
/**
|
|
1483
1524
|
* Build the shell command run inside a spawned tab/session for an ARBITRARY
|
|
@@ -1605,7 +1646,7 @@ export async function spawnMacOSTerminalTab(deps, terminal, shellCommand, contex
|
|
|
1605
1646
|
: buildTerminalAppleScript(shellCommand, title);
|
|
1606
1647
|
const result = await deps.runCommand("osascript", ["-e", script]);
|
|
1607
1648
|
if (commandSucceeded(result))
|
|
1608
|
-
return { ok: true };
|
|
1649
|
+
return { ok: true, destination: terminal === "iterm" ? "iTerm tab" : "macOS Terminal tab" };
|
|
1609
1650
|
const reason = (result.stderr || result.stdout || "").trim();
|
|
1610
1651
|
return {
|
|
1611
1652
|
ok: false,
|
|
@@ -1693,7 +1734,7 @@ export async function spawnWindowsTerminalTab(deps, _terminal, shellCommand, con
|
|
|
1693
1734
|
const args = buildWindowsTerminalArgs(worktreePath, shellCommand, title);
|
|
1694
1735
|
const result = await deps.runCommand(WINDOWS_TERMINAL_COMMAND, args);
|
|
1695
1736
|
if (commandSucceeded(result))
|
|
1696
|
-
return { ok: true };
|
|
1737
|
+
return { ok: true, destination: "Windows Terminal tab" };
|
|
1697
1738
|
const reason = combineCommandOutput(result);
|
|
1698
1739
|
return {
|
|
1699
1740
|
ok: false,
|
|
@@ -1716,7 +1757,7 @@ export async function spawnWindowsTerminalTab(deps, _terminal, shellCommand, con
|
|
|
1716
1757
|
fallback,
|
|
1717
1758
|
]);
|
|
1718
1759
|
if (commandSucceeded(result))
|
|
1719
|
-
return { ok: true };
|
|
1760
|
+
return { ok: true, destination: "PowerShell fallback window" };
|
|
1720
1761
|
const reason = combineCommandOutput(result);
|
|
1721
1762
|
return {
|
|
1722
1763
|
ok: false,
|
|
@@ -1820,7 +1861,7 @@ export async function spawnLinuxTmuxTerminalTab(deps, _terminal, shellCommand, c
|
|
|
1820
1861
|
: buildTmuxNewSessionArgs(session, window, worktreePath, paneCommand);
|
|
1821
1862
|
const result = await deps.runCommand(TMUX_COMMAND, args);
|
|
1822
1863
|
if (commandSucceeded(result))
|
|
1823
|
-
return { ok: true };
|
|
1864
|
+
return { ok: true, destination: "detached tmux session" };
|
|
1824
1865
|
const reason = combineCommandOutput(result);
|
|
1825
1866
|
return {
|
|
1826
1867
|
ok: false,
|
|
@@ -2080,7 +2121,7 @@ export async function spawnTabsForCreatedWorktrees(deps, rows, terminal, buildSh
|
|
|
2080
2121
|
worktreePath: row.path,
|
|
2081
2122
|
});
|
|
2082
2123
|
if (result.ok) {
|
|
2083
|
-
out.push({ ...row, status: "spawned" });
|
|
2124
|
+
out.push({ ...row, status: "spawned", ...(result.destination ? { sessionDestination: result.destination } : {}) });
|
|
2084
2125
|
}
|
|
2085
2126
|
else {
|
|
2086
2127
|
out.push({ ...row, status: "spawn-failed", error: result.error });
|
|
@@ -2105,7 +2146,7 @@ export function buildDryRunResults(keys, overrides) {
|
|
|
2105
2146
|
* PowerShell on Windows, `wt` + POSIX on macOS/Linux, and a non-throwing `wt` +
|
|
2106
2147
|
* POSIX fallback for unsupported platforms.
|
|
2107
2148
|
*/
|
|
2108
|
-
export function getDryRunPlatformDetails(agent, platform = process.platform, env = process.env, autoApprove = false, conductorEnabled = false, repoName = null, workflow = "implement", reviewRounds, baseBranch) {
|
|
2149
|
+
export function getDryRunPlatformDetails(agent, platform = process.platform, env = process.env, autoApprove = false, conductorEnabled = false, repoName = null, workflow = "implement", reviewRounds, baseBranch, manualTier) {
|
|
2109
2150
|
return {
|
|
2110
2151
|
worktrunkBinary: resolveWorktrunkBinary(platform, env),
|
|
2111
2152
|
// The builder accepts an optional resolved modelAlias; the dry-run caller
|
|
@@ -2118,7 +2159,7 @@ export function getDryRunPlatformDetails(agent, platform = process.platform, env
|
|
|
2118
2159
|
// buildAgentShellCommand/buildAgentPrompt path as a real spawn. A Codex
|
|
2119
2160
|
// dry-run has no created worktree to bind, so its preview omits the
|
|
2120
2161
|
// 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),
|
|
2162
|
+
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
2163
|
};
|
|
2123
2164
|
}
|
|
2124
2165
|
/**
|
|
@@ -2157,8 +2198,8 @@ export function buildDryRunMcpProvisioningLines(worktreePath, platform = process
|
|
|
2157
2198
|
* the secret-free MCP provisioning preview. Pure platform formatting only — no
|
|
2158
2199
|
* preflight, no routing failures.
|
|
2159
2200
|
*/
|
|
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);
|
|
2201
|
+
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) {
|
|
2202
|
+
const { worktrunkBinary, buildAgentShellCommand: build } = getDryRunPlatformDetails(agent, platform, env, autoApprove, conductorEnabled, repoName, workflow, reviewRounds, baseBranch, manualTier);
|
|
2162
2203
|
const wtArgs = buildWtSwitchArgs(branch, false, baseBranch);
|
|
2163
2204
|
const agentInvocation = build(key, "<worktree-path>", modelAlias);
|
|
2164
2205
|
// BAPI-801/BAPI-847: one concise secondary detail naming the PR base, emitted
|
|
@@ -2182,6 +2223,23 @@ export function buildDryRunDetailLines(agent, key, branch, platform = process.pl
|
|
|
2182
2223
|
* ` path=PATH` when available) followed by warning lines for any create/spawn
|
|
2183
2224
|
* failures.
|
|
2184
2225
|
*/
|
|
2226
|
+
/** Explain only actual successful sessions; summary serialization stays stable. */
|
|
2227
|
+
export function formatSpawnedSessionExplanation(rows, context) {
|
|
2228
|
+
const spawned = rows.filter((row) => row.status === "spawned");
|
|
2229
|
+
if (!spawned.length)
|
|
2230
|
+
return "";
|
|
2231
|
+
const lines = ["Spawned sessions:"];
|
|
2232
|
+
for (const row of spawned) {
|
|
2233
|
+
lines.push(` ${row.key}: ${context.agent} runs in ${row.path ?? "a worktree whose path was not reported"} on branch ${row.branch}; ${row.sessionDestination ?? "terminal destination not reported"}.`);
|
|
2234
|
+
}
|
|
2235
|
+
lines.push("Work on each branch from its reported folder: it is already checked out there and normally cannot also be checked out in the original folder.");
|
|
2236
|
+
lines.push("Monitor the reported terminal session and its agent output; use the attach commands below on Linux. Session startup is not review or implementation completion.");
|
|
2237
|
+
if (context.workflow === "review-and-implement")
|
|
2238
|
+
lines.push("Each session reviews independently, then opens a fresh implementation session only after its review halt gate permits it.");
|
|
2239
|
+
if (spawned.length > 1)
|
|
2240
|
+
lines.push("Coordinate ownership across these independent sessions; pause affected work if overlapping changes or conflicts appear.");
|
|
2241
|
+
return lines.join("\n");
|
|
2242
|
+
}
|
|
2185
2243
|
export function formatSummaryReport(rows) {
|
|
2186
2244
|
const lines = ["Summary:"];
|
|
2187
2245
|
// Surface the single conductor run id near the top (first line stays
|
|
@@ -2285,6 +2343,7 @@ export function buildCommandProvisioningDeps(deps) {
|
|
|
2285
2343
|
runCommand: deps.runCommand,
|
|
2286
2344
|
platform: deps.platform,
|
|
2287
2345
|
cwd: deps.cwd,
|
|
2346
|
+
...(!deps.readFile && !deps.writeFile && !deps.mkdir ? { recordAssets: recordProvisionedAssets } : {}),
|
|
2288
2347
|
};
|
|
2289
2348
|
}
|
|
2290
2349
|
/**
|
|
@@ -2383,6 +2442,12 @@ export function formatModelRoutingDiagnosticLine(d) {
|
|
|
2383
2442
|
case "posture-unavailable":
|
|
2384
2443
|
return ("model routing: optimization posture was unavailable; Quality routing was retained " +
|
|
2385
2444
|
"(reason=optimization-posture-unavailable).");
|
|
2445
|
+
case "review-consumer-unrecognized":
|
|
2446
|
+
return (`model routing: the Bridge API server did not recognize the review consumer${ticketSuffix} ` +
|
|
2447
|
+
"(reason=consumer-unrecognized); the review tab uses the premium fallback. Upgrade the server.");
|
|
2448
|
+
case "review-threshold-legacy":
|
|
2449
|
+
return (`model routing: review thresholds are not configured${ticketSuffix} ` +
|
|
2450
|
+
"(reason=legacy); the review tab uses the premium fallback. Configure review model-tier thresholds.");
|
|
2386
2451
|
default:
|
|
2387
2452
|
return `model routing degraded: ${d.message} ${failOpen}`;
|
|
2388
2453
|
}
|
|
@@ -3023,7 +3088,43 @@ export function formatModelRoutingDecisionDiagnosticLine(input) {
|
|
|
3023
3088
|
return (`[model-routing] source=${input.source} baseline=${input.baseline} ` +
|
|
3024
3089
|
`posture=${input.posture} adjustment=${input.adjustment} effective=${input.effective} ` +
|
|
3025
3090
|
`alias=${input.alias ?? "unresolved"}` +
|
|
3026
|
-
(input.threshold ? ` threshold=${input.threshold}` : "")
|
|
3091
|
+
(input.threshold ? ` threshold=${input.threshold}` : "") +
|
|
3092
|
+
(input.phase === "review" ? " phase=review consumer=review" : ""));
|
|
3093
|
+
}
|
|
3094
|
+
/**
|
|
3095
|
+
* BAPI-1365: classify a successfully fetched review-consumer `/model-tier`
|
|
3096
|
+
* response. `consumer-unrecognized` = the server did not echo `consumer:
|
|
3097
|
+
* "review"` (its tier and marker are not a review decision and are never
|
|
3098
|
+
* interpreted); `legacy` = it echoed review but no stored review thresholds
|
|
3099
|
+
* decided the tier. `undefined` = a usable review decision, OR a
|
|
3100
|
+
* `source: "fallback"` response (the difficulty itself failed; callers keep the
|
|
3101
|
+
* existing lookup-failure handling for that).
|
|
3102
|
+
*/
|
|
3103
|
+
function reviewTierFallbackCause(value) {
|
|
3104
|
+
if (value.consumer !== "review")
|
|
3105
|
+
return "consumer-unrecognized";
|
|
3106
|
+
if (value.source === "fallback")
|
|
3107
|
+
return undefined;
|
|
3108
|
+
if (!isThresholdDecidedTierResponse(value))
|
|
3109
|
+
return "legacy";
|
|
3110
|
+
return undefined;
|
|
3111
|
+
}
|
|
3112
|
+
/** BAPI-1365: the secret-free operator warning and diagnostic for a review fallback cause. */
|
|
3113
|
+
function reviewTierFallbackNotice(cause, repoName, ticketKey) {
|
|
3114
|
+
const kind = cause === "consumer-unrecognized" ? "review-consumer-unrecognized" : "review-threshold-legacy";
|
|
3115
|
+
const warning = cause === "consumer-unrecognized"
|
|
3116
|
+
? `model routing: server did not recognize the review consumer for ${ticketKey} (reason=consumer-unrecognized)`
|
|
3117
|
+
: `model routing: review thresholds are not configured for ${ticketKey} (reason=legacy)`;
|
|
3118
|
+
return {
|
|
3119
|
+
warning,
|
|
3120
|
+
diagnostic: makeModelRoutingDiagnostic({
|
|
3121
|
+
kind,
|
|
3122
|
+
repoName,
|
|
3123
|
+
storeTarget: `bapi:${repoName}`,
|
|
3124
|
+
ticketKey,
|
|
3125
|
+
message: warning,
|
|
3126
|
+
}),
|
|
3127
|
+
};
|
|
3027
3128
|
}
|
|
3028
3129
|
/**
|
|
3029
3130
|
* BAPI-1343 R-6: the threshold marker for a successfully fetched `/model-tier`
|
|
@@ -3032,11 +3133,16 @@ export function formatModelRoutingDecisionDiagnosticLine(input) {
|
|
|
3032
3133
|
* (`configured` or `anomaly`). Every other case returns `undefined`, so the
|
|
3033
3134
|
* caller keeps today's exact downshift and text.
|
|
3034
3135
|
*/
|
|
3035
|
-
function thresholdSkipMarker(posture, tierResult) {
|
|
3136
|
+
function thresholdSkipMarker(posture, tierResult, reviewWorkflow = false) {
|
|
3036
3137
|
if (posture !== "maximize_subscription")
|
|
3037
3138
|
return undefined;
|
|
3038
3139
|
if (!tierResult || !tierResult.ok)
|
|
3039
3140
|
return undefined;
|
|
3141
|
+
// BAPI-1365: a review-consumer lookup counts only when the server echoed the
|
|
3142
|
+
// review consumer; an older server that ignored the parameter answered for
|
|
3143
|
+
// the implementation consumer, which says nothing about the review threshold.
|
|
3144
|
+
if (reviewWorkflow && tierResult.value.consumer !== "review")
|
|
3145
|
+
return undefined;
|
|
3040
3146
|
if (!isThresholdDecidedTierResponse(tierResult.value))
|
|
3041
3147
|
return undefined;
|
|
3042
3148
|
return tierResult.value.thresholdMode;
|
|
@@ -3206,6 +3312,12 @@ errorLog = (message) => console.error(message)) {
|
|
|
3206
3312
|
// produced it.
|
|
3207
3313
|
const postureResolution = await resolveOptimizationPostureForStartTickets(access);
|
|
3208
3314
|
const posture = postureResolution.posture;
|
|
3315
|
+
// BAPI-1365: the spawned tab of a `review-and-implement` launch is a REVIEW
|
|
3316
|
+
// tab, so its tier lookups ask for the review consumer. An ordinary
|
|
3317
|
+
// `implement` launch supplies no options and keeps the endpoint's default
|
|
3318
|
+
// implementation consumer. Derived once and shared by both fetch paths below.
|
|
3319
|
+
const isReviewWorkflow = options.workflow === "review-and-implement";
|
|
3320
|
+
const tierFetchOptions = isReviewWorkflow ? { consumer: "review" } : {};
|
|
3209
3321
|
// BAPI-1225: one deterministic, best-effort diagnostic line per repository-level
|
|
3210
3322
|
// resolution, emitted exactly here — outside the per-ticket row loop below, so a
|
|
3211
3323
|
// multi-ticket invocation still logs it once for this canonical repository. Uses
|
|
@@ -3232,14 +3344,14 @@ errorLog = (message) => console.error(message)) {
|
|
|
3232
3344
|
// hoisting pattern above.
|
|
3233
3345
|
//
|
|
3234
3346
|
// BAPI-1343 R-6: under `maximize_subscription` ONLY, each eligible row's
|
|
3235
|
-
// `/model-tier` is fetched once (the shared bounded fetch
|
|
3236
|
-
//
|
|
3347
|
+
// `/model-tier` is fetched once (the shared bounded fetch; BAPI-1365: with
|
|
3348
|
+
// the workflow's consumer) purely to read `threshold_mode`. A threshold-decided
|
|
3237
3349
|
// (`configured`/`anomaly`) response keeps the carried tier — stored
|
|
3238
3350
|
// thresholds made it final. A fetch failure, `legacy`, or anything else
|
|
3239
3351
|
// downshifts exactly as before. `quality`/`speed` never downshift, so they
|
|
3240
3352
|
// make no extra request.
|
|
3241
3353
|
const thresholdTierMap = posture === "maximize_subscription"
|
|
3242
|
-
? await fetchTicketModelTiersForRows(access, eligible, options.maxParallel, isEligible)
|
|
3354
|
+
? await fetchTicketModelTiersForRows(access, eligible, options.maxParallel, isEligible, tierFetchOptions)
|
|
3243
3355
|
: null;
|
|
3244
3356
|
const aliasByEffectiveTier = new Map();
|
|
3245
3357
|
const resolveAutoTierAlias = async (effectiveTier) => {
|
|
@@ -3269,7 +3381,14 @@ errorLog = (message) => console.error(message)) {
|
|
|
3269
3381
|
autoOut.push(row);
|
|
3270
3382
|
continue;
|
|
3271
3383
|
}
|
|
3272
|
-
const
|
|
3384
|
+
const markerResult = thresholdTierMap?.get(row.key);
|
|
3385
|
+
const threshold = thresholdSkipMarker(posture, markerResult, isReviewWorkflow);
|
|
3386
|
+
// BAPI-1365: for a review launch, say WHY a fetched marker did not decide
|
|
3387
|
+
// the tier (never a review-threshold claim). A failed fetch keeps the
|
|
3388
|
+
// existing silent downshift.
|
|
3389
|
+
const markerCause = isReviewWorkflow && markerResult && markerResult.ok
|
|
3390
|
+
? reviewTierFallbackCause(markerResult.value)
|
|
3391
|
+
: undefined;
|
|
3273
3392
|
const effectiveTier = posture === "maximize_subscription" && !threshold
|
|
3274
3393
|
? downshiftModelTier(autoTier)
|
|
3275
3394
|
: autoTier;
|
|
@@ -3277,7 +3396,7 @@ errorLog = (message) => console.error(message)) {
|
|
|
3277
3396
|
const lineageSuffix = threshold
|
|
3278
3397
|
? `; threshold:${threshold}`
|
|
3279
3398
|
: posture === "maximize_subscription"
|
|
3280
|
-
? `; auto-tier:${autoTier}->maximize:${effectiveTier}`
|
|
3399
|
+
? `; auto-tier:${autoTier}->maximize:${effectiveTier}${markerCause ? `; review-marker:${markerCause}` : ""}`
|
|
3281
3400
|
: "";
|
|
3282
3401
|
const { resolvedAlias, aliasWarning } = await resolveAutoTierAlias(effectiveTier);
|
|
3283
3402
|
const reason = resolvedAlias
|
|
@@ -3294,7 +3413,14 @@ errorLog = (message) => console.error(message)) {
|
|
|
3294
3413
|
? { modelRoutingDiagnostic: codexEntitlementDiagnostic(access.repoName) }
|
|
3295
3414
|
: {}),
|
|
3296
3415
|
};
|
|
3297
|
-
|
|
3416
|
+
let pushedRow = aliasWarning ? appendSummaryRowWarning(baseRow, aliasWarning) : baseRow;
|
|
3417
|
+
if (markerCause) {
|
|
3418
|
+
const notice = reviewTierFallbackNotice(markerCause, access.repoName, row.key);
|
|
3419
|
+
pushedRow = appendSummaryRowWarning(pushedRow, notice.warning);
|
|
3420
|
+
if (!pushedRow.modelRoutingDiagnostic)
|
|
3421
|
+
pushedRow = { ...pushedRow, modelRoutingDiagnostic: notice.diagnostic };
|
|
3422
|
+
}
|
|
3423
|
+
autoOut.push(pushedRow);
|
|
3298
3424
|
// BAPI-1236: one `automatic_snapshot` diagnostic per eligible row — the
|
|
3299
3425
|
// carried baseline, the newly resolved posture, the exact adjustment token,
|
|
3300
3426
|
// the calculated effective tier, and the validated alias (or `unresolved`).
|
|
@@ -3306,13 +3432,14 @@ errorLog = (message) => console.error(message)) {
|
|
|
3306
3432
|
effective: effectiveTier,
|
|
3307
3433
|
alias: resolvedAlias,
|
|
3308
3434
|
...(threshold ? { threshold } : {}),
|
|
3435
|
+
...(isReviewWorkflow ? { phase: "review" } : {}),
|
|
3309
3436
|
});
|
|
3310
3437
|
}
|
|
3311
3438
|
out = autoOut;
|
|
3312
3439
|
}
|
|
3313
3440
|
else {
|
|
3314
3441
|
// Ordinary per-ticket difficulty-lookup path.
|
|
3315
|
-
const tierMap = await fetchTicketModelTiersForRows(access, eligible, options.maxParallel, isEligible);
|
|
3442
|
+
const tierMap = await fetchTicketModelTiersForRows(access, eligible, options.maxParallel, isEligible, tierFetchOptions);
|
|
3316
3443
|
// Lazily resolve+validate the premium fallback alias at most once for this run
|
|
3317
3444
|
// (the alias is identical across rows; cursor-agent validation spawns a probe).
|
|
3318
3445
|
let premiumApplier = null;
|
|
@@ -3351,6 +3478,40 @@ errorLog = (message) => console.error(message)) {
|
|
|
3351
3478
|
continue;
|
|
3352
3479
|
}
|
|
3353
3480
|
const { difficulty, tier, source } = tierResult.value;
|
|
3481
|
+
// BAPI-1365: a review-launch response is a review-threshold decision only
|
|
3482
|
+
// when the server echoed `consumer: "review"` AND stored thresholds decided
|
|
3483
|
+
// it. Anything else takes the existing premium fallback with a closed
|
|
3484
|
+
// reason — the returned tier and marker are never interpreted.
|
|
3485
|
+
if (isReviewWorkflow) {
|
|
3486
|
+
const cause = reviewTierFallbackCause(tierResult.value);
|
|
3487
|
+
if (cause) {
|
|
3488
|
+
const notice = reviewTierFallbackNotice(cause, access.repoName, row.key);
|
|
3489
|
+
if (agent.name === "codex") {
|
|
3490
|
+
loopOut.push(codexDefaultRows([row], () => true, notice.warning, notice.diagnostic)[0]);
|
|
3491
|
+
continue;
|
|
3492
|
+
}
|
|
3493
|
+
loopOut.push(await applyPremiumFallback(row, notice.warning, notice.diagnostic));
|
|
3494
|
+
continue;
|
|
3495
|
+
}
|
|
3496
|
+
if (source === "fallback") {
|
|
3497
|
+
// The server could not obtain a difficulty: its premium tier is a
|
|
3498
|
+
// guess, not a review decision — keep the lookup-failure handling.
|
|
3499
|
+
const warning = `model routing: tier lookup failed for ${row.key}; using agent default model`;
|
|
3500
|
+
const diagnostic = makeModelRoutingDiagnostic({
|
|
3501
|
+
kind: "tier-fetch-unavailable",
|
|
3502
|
+
repoName: access.repoName,
|
|
3503
|
+
storeTarget: `bapi:${access.repoName}`,
|
|
3504
|
+
ticketKey: row.key,
|
|
3505
|
+
message: `tier lookup failed for ${row.key}`,
|
|
3506
|
+
});
|
|
3507
|
+
if (agent.name === "codex") {
|
|
3508
|
+
loopOut.push(codexDefaultRows([row], () => true, warning, diagnostic)[0]);
|
|
3509
|
+
continue;
|
|
3510
|
+
}
|
|
3511
|
+
loopOut.push(await applyPremiumFallback(row, warning, diagnostic));
|
|
3512
|
+
continue;
|
|
3513
|
+
}
|
|
3514
|
+
}
|
|
3354
3515
|
if (!tier) {
|
|
3355
3516
|
// Backend reported no usable difficulty — default to premium/Opus. (The
|
|
3356
3517
|
// backend now returns a premium fallback itself, so this is defensive.)
|
|
@@ -3396,7 +3557,7 @@ errorLog = (message) => console.error(message)) {
|
|
|
3396
3557
|
// diagnostic carry a distinct `threshold` marker instead. The harness-
|
|
3397
3558
|
// local premium fallback above is untouched.
|
|
3398
3559
|
const difficultyTier = tier;
|
|
3399
|
-
const threshold = thresholdSkipMarker(posture, tierResult);
|
|
3560
|
+
const threshold = thresholdSkipMarker(posture, tierResult, isReviewWorkflow);
|
|
3400
3561
|
const effectiveTier = posture === "maximize_subscription" && !threshold
|
|
3401
3562
|
? downshiftModelTier(difficultyTier)
|
|
3402
3563
|
: difficultyTier;
|
|
@@ -3406,7 +3567,10 @@ errorLog = (message) => console.error(message)) {
|
|
|
3406
3567
|
: posture === "maximize_subscription"
|
|
3407
3568
|
? `; difficulty:${difficultyTier}->maximize:${effectiveTier}`
|
|
3408
3569
|
: "";
|
|
3409
|
-
const thresholdField =
|
|
3570
|
+
const thresholdField = {
|
|
3571
|
+
...(threshold ? { threshold } : {}),
|
|
3572
|
+
...(isReviewWorkflow ? { phase: "review" } : {}),
|
|
3573
|
+
};
|
|
3410
3574
|
// BAPI-1236: stamp `difficulty_lookup` rather than copying the backend
|
|
3411
3575
|
// cache/computation source (`cached`/`computed`/`fallback`) into the
|
|
3412
3576
|
// summary's own routing-provenance field — that backend source is
|
|
@@ -3758,7 +3922,7 @@ export async function orchestrateStartTickets(deps, options, overrides = {}) {
|
|
|
3758
3922
|
// epic.base_branch override applied above) so the spawned worker gets
|
|
3759
3923
|
// BAPI_BASE_BRANCH — every conductor worker, and a plain worker whose base
|
|
3760
3924
|
// is non-default.
|
|
3761
|
-
options.baseBranch, bridgeApiBaseUrl);
|
|
3925
|
+
options.baseBranch, bridgeApiBaseUrl, isModelTier(options.injectedTier) ? options.injectedTier : undefined);
|
|
3762
3926
|
if (!platformConfig.ok)
|
|
3763
3927
|
return { ok: false, error: platformConfig.error };
|
|
3764
3928
|
// BAPI-527: resolve the base start point new worktrees are cut from BEFORE any
|
|
@@ -3787,6 +3951,11 @@ export async function orchestrateStartTickets(deps, options, overrides = {}) {
|
|
|
3787
3951
|
});
|
|
3788
3952
|
if (!refresh.ok)
|
|
3789
3953
|
return { ok: false, error: refresh.error };
|
|
3954
|
+
// BAPI-1362: a live plane member runs from the base worktree, so the local
|
|
3955
|
+
// branch was left where it was; cut from the fetched remote SHA instead.
|
|
3956
|
+
if (refresh.protectedBaseSha !== undefined) {
|
|
3957
|
+
effectiveBaseStartPoint = refresh.protectedBaseSha;
|
|
3958
|
+
}
|
|
3790
3959
|
}
|
|
3791
3960
|
// BAPI-941: the `claude-review` workflow-drift advisory, emitted ONCE per
|
|
3792
3961
|
// worker spawn at the one choke point both dispatch paths pass through, after
|
|
@@ -4023,7 +4192,7 @@ export async function runStartTicketsCli(argv, overrides = {}) {
|
|
|
4023
4192
|
const branch = resolveBranchForTicket(key, options.branchOverrides);
|
|
4024
4193
|
const routedRow = routedByKey.get(key);
|
|
4025
4194
|
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)) {
|
|
4195
|
+
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
4196
|
log(line);
|
|
4028
4197
|
}
|
|
4029
4198
|
log(`DRY-RUN: model routing: ${formatModelRoutingLine(routedRow ?? { key, branch, status: "dry-run" }, agent, { workflow: options.workflow })}`);
|
|
@@ -4065,6 +4234,9 @@ export async function runStartTicketsCli(argv, overrides = {}) {
|
|
|
4065
4234
|
return 1;
|
|
4066
4235
|
}
|
|
4067
4236
|
log(formatSummaryReport(result.rows));
|
|
4237
|
+
const explanation = formatSpawnedSessionExplanation(result.rows, { agent: options.agentName, workflow: options.workflow ?? "implement" });
|
|
4238
|
+
if (explanation)
|
|
4239
|
+
log(explanation);
|
|
4068
4240
|
// Linux: each spawned ticket runs in a detached tmux session — tell the user
|
|
4069
4241
|
// how to attach.
|
|
4070
4242
|
const spawned = result.rows.filter((r) => r.status === "spawned");
|
|
@@ -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 &&
|