@bridge_gpt/mcp-server 0.2.57 → 0.2.59
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 +112 -113
- package/build/agent-registry.js +15 -0
- package/build/agent-utils.js +16 -8
- package/build/claude-user-config-doctor.js +1 -35
- package/build/codex-bridge-profile.js +5 -1
- package/build/codex-launch-scope.js +13 -2
- package/build/commands.generated.js +5 -6
- package/build/conductor/bridge-api-client.js +149 -0
- package/build/conductor/bring-up-facts.js +1 -1
- package/build/conductor/claude-hook.js +16 -16
- package/build/conductor/doctor.js +24 -19
- package/build/conductor/epic-implementer-cli.js +38 -5
- package/build/conductor/epic-reconcile.js +1 -1
- package/build/conductor/epic-runtime.js +10 -10
- package/build/conductor/errors.js +3 -3
- package/build/conductor/file-scope-guard.js +4 -4
- package/build/conductor/git-ci-types.js +1 -1
- package/build/conductor/git-inspection.js +2 -2
- package/build/conductor/install-doctor.js +6 -6
- package/build/conductor/merge-ledger.js +2 -2
- package/build/conductor/pr-ci-producer.js +6 -6
- package/build/conductor/pr-review-producer.js +2 -2
- package/build/conductor/producer-ledger.js +2 -0
- package/build/conductor/readiness-sections.js +7 -7
- package/build/conductor/readiness.js +2 -2
- package/build/conductor/recovery-cli.js +243 -12
- package/build/conductor/recovery-operations.js +56 -4
- package/build/conductor/repair-contract.js +38 -4
- package/build/conductor/spec-review-producer.js +6 -6
- package/build/conductor/store.js +10 -10
- package/build/conductor/supervisor-config.js +14 -14
- package/build/conductor/supervisor-ledger.js +2 -2
- package/build/conductor/supervisor-message-relay.js +2 -2
- package/build/conductor/worker-ledger-cli.js +19 -19
- package/build/conductor-bundle-artifacts.js +2 -2
- package/build/cursor-agent-progress.js +72 -0
- package/build/doctor.js +20 -296
- package/build/drive-epic.js +65 -71
- package/build/epic-implementer-bin.js +22 -17
- package/build/epic-implementer-claude-hook-bin.js +4 -2
- package/build/epic-implementer-env.js +155 -0
- package/build/epic-integration-pr.js +2 -2
- package/build/epic-shared/bridge-client.js +773 -0
- package/build/epic-shared/cut-protocol.js +392 -0
- package/build/executor/env.js +19 -8
- package/build/executor/heartbeat.js +1 -1
- package/build/executor/job-errors.js +3 -3
- package/build/executor/job-runner.js +1 -1
- package/build/executor/merge-job.js +35 -3
- package/build/executor/observation.js +1 -1
- package/build/executor/preflight.js +1 -1
- package/build/executor/prompt-spec.js +1 -1
- package/build/executor/reconcile-overlap-job.js +10 -130
- package/build/executor/reconcile-overlap-types.js +1 -1
- package/build/executor/runner.js +1 -1
- package/build/implement-epic/cli.js +1 -1
- package/build/index.js +187 -224
- package/build/init.js +20 -28
- package/build/install-bridge-conductor.js +7 -7
- package/build/install-bridge.js +203 -10
- package/build/install-doctor.js +1 -1
- package/build/mcp-host-entry-adapter.js +6 -0
- package/build/mcp-host-targets.js +2 -0
- package/build/mcp-invoke.js +5 -5
- package/build/mcp-profile.js +2 -2
- package/build/mcp-provisioning.js +2 -2
- package/build/mcp-server-invocation.js +3 -3
- package/build/merge-pull-request.js +15 -0
- package/build/model-routing-host.js +25 -0
- package/build/model-routing-preferences.js +99 -0
- package/build/pipelines.generated.js +11 -11
- package/build/plan-epic-implementer-eligibility.js +215 -0
- package/build/plan-execution-ledger.js +8 -0
- package/build/plane/member-roster.js +5 -5
- package/build/plane/supervisor.js +1 -1
- package/build/plane/types.js +5 -5
- package/build/readiness-check.js +1 -1
- package/build/readme.generated.js +1 -1
- package/build/review-tickets.js +4 -4
- package/build/setup-epic.js +8 -8
- package/build/start-tickets-conductor.js +32 -32
- package/build/start-tickets-prereqs.js +4 -9
- package/build/start-tickets.js +494 -98
- package/build/version.generated.js +3 -3
- package/docs/CONDUCTOR.md +69 -18
- package/docs/install/mcp-tool-integrations.md +2 -1
- package/package.json +2 -2
- package/pipelines/implement-ticket.json +2 -2
- package/pipelines/plan-epic.json +2 -2
- package/pipelines/review-ticket.json +1 -1
package/build/start-tickets.js
CHANGED
|
@@ -58,6 +58,7 @@ import { readFile, writeFile, mkdir, mkdtemp, stat, lstat, symlink, unlink, copy
|
|
|
58
58
|
import os from "node:os";
|
|
59
59
|
import { createHash } from "node:crypto";
|
|
60
60
|
import path from "path";
|
|
61
|
+
import { fileURLToPath } from "node:url";
|
|
61
62
|
import { validateHttpBaseUrl } from "./base-url.js";
|
|
62
63
|
import { VERSION } from "./version.generated.js";
|
|
63
64
|
import { resolveBapiCredentials, getPrimaryCredentialStorePath, } from "./credential-store.js";
|
|
@@ -76,7 +77,7 @@ import { TICKET_KEY_PATTERN as SHARED_TICKET_KEY_PATTERN, TICKET_KEY_USAGE_SUMMA
|
|
|
76
77
|
// can never drift. `start-tickets.ts` imports VALUES from there; the prereqs
|
|
77
78
|
// module imports only TYPES back, so the runtime graph stays acyclic.
|
|
78
79
|
import { WORKTRUNK_BINARY_OVERRIDE_ENV, WINDOWS_TERMINAL_COMMAND, WINDOWS_POWERSHELL_CANDIDATES, DEFAULT_WINDOWS_WORKTRUNK_BINARY, DEFAULT_POSIX_WORKTRUNK_BINARY, TMUX_COMMAND, GIT_FOR_WINDOWS_BASH_HINT, isSupportedStartTicketsPlatform, unsupportedPlatformMessage, resolveWorktrunkBinary, commandSucceeded, getCommandProbe, isCommandOnPath, resolveFirstCommandOnPath, enforcePreflightPrerequisites, appendDoctorHint, } from "./start-tickets-prereqs.js";
|
|
79
|
-
import { DEFAULT_AGENT_NAME, resolveAgentSpec, isAgentName, resolveModelAlias, isValidModelAlias, isModelTier, } from "./agent-registry.js";
|
|
80
|
+
import { DEFAULT_AGENT_NAME, resolveAgentSpec, isAgentName, resolveModelAlias, isValidModelAlias, isModelTier, downshiftModelTier, } from "./agent-registry.js";
|
|
80
81
|
import { agentForPlatform, getHostTarget, getWorktreeHostTargets, isRunnableHostTarget, isHostPlatformId } from "./mcp-host-targets.js";
|
|
81
82
|
import { resolveCodexLaunchScope } from "./codex-launch-scope.js";
|
|
82
83
|
import { tmpdir } from "node:os";
|
|
@@ -134,7 +135,8 @@ export function getStartTicketsUsage() {
|
|
|
134
135
|
" --host claude-code|cursor|codex|copilot-vscode|windsurf Current host when --agent is omitted; Claude Code, Cursor, and Codex can launch sessions",
|
|
135
136
|
" Cursor with --auto launches headlessly after a bounded readiness check; without --auto it stays interactive for the review gate.",
|
|
136
137
|
" --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.",
|
|
137
|
-
" --tier cheap|basic|premium
|
|
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.",
|
|
138
140
|
" --rounds 1|2 Review round count forwarded to the review phase; review-only, valid only with --workflow review-and-implement",
|
|
139
141
|
" --terminal terminal|iterm Override the macOS terminal app (default: auto-detect via $TERM_PROGRAM); honored on macOS only",
|
|
140
142
|
" --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",
|
|
@@ -142,16 +144,16 @@ export function getStartTicketsUsage() {
|
|
|
142
144
|
" --base-branch BRANCH Cut new worktrees from BRANCH and refresh origin/BRANCH (default: main)",
|
|
143
145
|
" --no-refresh-main Skip refresh of the configured base branch (default main); historical name retained for backward compatibility",
|
|
144
146
|
" --max-parallel N Max worktrees to create concurrently (default: 3)",
|
|
145
|
-
" --conductor Enable the Conductor system: per-worker hook injection +
|
|
147
|
+
" --conductor Enable the Conductor system: per-worker hook injection + BAPI_EPIC_IMPLEMENTER_* env, lifecycle ledger events, and a supervisor peer tab (default: off — a plain run just spawns cd <worktree> && <agent> '/implement-ticket <KEY>'). Implies --guard-stale-branch: a pre-existing ticket branch carrying commits beyond the resolved base is refused, not silently reused.",
|
|
146
148
|
" --guard-stale-branch Refuse to reuse a pre-existing feature/KEY branch whose tip is NOT an ancestor of the resolved base (it carries commits the base does not). The ticket is reported create-failed with the stale-worktree remedy instead of spawning a worker on leftover code. Implied by --conductor; pass it explicitly on the LLM-conductor pilot cut path. Off by default, so a plain interactive run keeps reusing a same-named branch.",
|
|
147
149
|
" -h, --help Show this help",
|
|
148
150
|
"",
|
|
149
151
|
"Environment:",
|
|
150
152
|
` ${WORKTRUNK_BINARY_OVERRIDE_ENV} Override the Worktrunk executable name/path for nonstandard installs`,
|
|
151
153
|
` ${TMUX_SESSION_OVERRIDE_ENV} Override the tmux session-name prefix on Linux (default: ${DEFAULT_TMUX_SESSION_PREFIX})`,
|
|
152
|
-
"
|
|
153
|
-
"
|
|
154
|
-
"
|
|
154
|
+
" BAPI_EPIC_IMPLEMENTER_GATE_NAME Conductor gate name for this run (default: implement-ticket)",
|
|
155
|
+
" BAPI_EPIC_IMPLEMENTER_SUPERVISOR_MODE Conductor supervisor mode (default: auto when --auto, else interactive)",
|
|
156
|
+
" BAPI_EPIC_IMPLEMENTER_ENABLE_PRE_TOOL_USE Set 1/true to also register a PreToolUse conductor hook",
|
|
155
157
|
"",
|
|
156
158
|
"Conductor observability (opt-in via --conductor):",
|
|
157
159
|
" With --conductor, real Claude Code workers launched by start-tickets receive",
|
|
@@ -159,7 +161,7 @@ export function getStartTicketsUsage() {
|
|
|
159
161
|
" local lifecycle events into the conductor ledger. Each such run mints one run_id",
|
|
160
162
|
" and attributes worker events by worker_id, ticket key, and worktree path, and a",
|
|
161
163
|
" supervisor peer tab is opened. Without --conductor none of this happens. Inspect",
|
|
162
|
-
" the ledger with the `conductor` CLI. The
|
|
164
|
+
" the ledger with the `conductor` CLI. The BAPI_EPIC_IMPLEMENTER_* env vars above apply",
|
|
163
165
|
" only when --conductor is set.",
|
|
164
166
|
"",
|
|
165
167
|
"Prerequisites:",
|
|
@@ -206,6 +208,13 @@ export function parseStartTicketsArgs(argv) {
|
|
|
206
208
|
// the parsed options); a `ModelTier` = usable override; INJECTED_TIER_UNRESOLVED
|
|
207
209
|
// = supplied-but-malformed (fail open to premium). NEVER an error/throw.
|
|
208
210
|
let injectedTier;
|
|
211
|
+
// BAPI-1236 `--auto-tier` machine-carried raw tier. `undefined` = flag absent.
|
|
212
|
+
// Unlike `--tier`, an invalid/repeated occurrence is a hard parser error —
|
|
213
|
+
// there is no fail-open sentinel — because this flag is meant for chained
|
|
214
|
+
// automation, not interactive typing, so a malformed value should surface
|
|
215
|
+
// immediately rather than silently degrade routing.
|
|
216
|
+
let autoTier;
|
|
217
|
+
let autoTierSeen = false;
|
|
209
218
|
const branchEntries = [];
|
|
210
219
|
const keys = [];
|
|
211
220
|
for (let i = 0; i < argv.length; i++) {
|
|
@@ -309,6 +318,39 @@ export function parseStartTicketsArgs(argv) {
|
|
|
309
318
|
injectedTier = isModelTier(value) ? value : INJECTED_TIER_UNRESOLVED;
|
|
310
319
|
continue;
|
|
311
320
|
}
|
|
321
|
+
if (arg === "--auto-tier" || arg.startsWith("--auto-tier=")) {
|
|
322
|
+
// BAPI-1236: strict machine-carried tier boundary. Unlike `--tier`, a
|
|
323
|
+
// malformed/empty/missing/repeated value is a hard parser error — there
|
|
324
|
+
// is no INJECTED_TIER_UNRESOLVED-style fail-open sentinel for this flag.
|
|
325
|
+
let value;
|
|
326
|
+
if (arg.startsWith("--auto-tier=")) {
|
|
327
|
+
value = arg.slice("--auto-tier=".length);
|
|
328
|
+
}
|
|
329
|
+
else {
|
|
330
|
+
// Same lookahead rule as `--tier`: never swallow a following option or
|
|
331
|
+
// ticket-key token as the value.
|
|
332
|
+
const next = i + 1 < argv.length ? argv[i + 1] : undefined;
|
|
333
|
+
if (next !== undefined && !next.startsWith("-") && !TICKET_KEY_PATTERN.test(next)) {
|
|
334
|
+
value = takeValue();
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
// Validate the value directly with `isModelTier()` — no trimming, case
|
|
338
|
+
// normalization, or reuse of `INJECTED_TIER_UNRESOLVED` — before ever
|
|
339
|
+
// checking repetition, so a malformed occurrence always fails as an
|
|
340
|
+
// invalid-value error rather than a repeated-flag or mutual-exclusion one.
|
|
341
|
+
if (!isModelTier(value)) {
|
|
342
|
+
return {
|
|
343
|
+
status: "error",
|
|
344
|
+
message: `Invalid --auto-tier value: '${value ?? ""}' (accepted values: cheap, basic, premium).`,
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
if (autoTierSeen) {
|
|
348
|
+
return { status: "error", message: "--auto-tier may only be supplied once." };
|
|
349
|
+
}
|
|
350
|
+
autoTierSeen = true;
|
|
351
|
+
autoTier = value;
|
|
352
|
+
continue;
|
|
353
|
+
}
|
|
312
354
|
if (arg === "--terminal" || arg.startsWith("--terminal=")) {
|
|
313
355
|
let value;
|
|
314
356
|
if (arg.startsWith("--terminal=")) {
|
|
@@ -403,6 +445,18 @@ export function parseStartTicketsArgs(argv) {
|
|
|
403
445
|
}
|
|
404
446
|
keys.push(arg);
|
|
405
447
|
}
|
|
448
|
+
// --- tier-flag mutual exclusion (BAPI-1236) ---
|
|
449
|
+
// Checked immediately after the full argument scan, before any ticket/option
|
|
450
|
+
// semantic validation, so the conflict exits before orchestration or any
|
|
451
|
+
// network-capable dependency can run. Fires whenever BOTH flag names were
|
|
452
|
+
// supplied at all — including a malformed `--tier` (INJECTED_TIER_UNRESOLVED),
|
|
453
|
+
// since the flag was still supplied — regardless of argument order.
|
|
454
|
+
if (injectedTier !== undefined && autoTier !== undefined) {
|
|
455
|
+
return {
|
|
456
|
+
status: "error",
|
|
457
|
+
message: "--tier and --auto-tier cannot be used together.",
|
|
458
|
+
};
|
|
459
|
+
}
|
|
406
460
|
// --- key validation ---
|
|
407
461
|
if (keys.length === 0) {
|
|
408
462
|
return {
|
|
@@ -507,6 +561,9 @@ export function parseStartTicketsArgs(argv) {
|
|
|
507
561
|
// supplied, so an omitted flag leaves the legacy option shape untouched
|
|
508
562
|
// (no own `injectedTier` key, not even `undefined`).
|
|
509
563
|
...(injectedTier !== undefined ? { injectedTier } : {}),
|
|
564
|
+
// BAPI-1236: include the auto-tier property ONLY when `--auto-tier` was
|
|
565
|
+
// supplied, mirroring `injectedTier`'s omitted-flag contract.
|
|
566
|
+
...(autoTier !== undefined ? { autoTier } : {}),
|
|
510
567
|
},
|
|
511
568
|
};
|
|
512
569
|
}
|
|
@@ -1363,7 +1420,12 @@ export function buildAgentInvocationArgv(agent, prompt, modelAlias, launchArgsOr
|
|
|
1363
1420
|
if (agent.name === "codex" && Array.isArray(launchArgsOrCursorWorkspace))
|
|
1364
1421
|
argv.push(...launchArgsOrCursorWorkspace);
|
|
1365
1422
|
if (agent.name === "cursor-agent" && typeof launchArgsOrCursorWorkspace === "string" && launchArgsOrCursorWorkspace) {
|
|
1366
|
-
|
|
1423
|
+
// Cursor's text mode can buffer the entire headless turn. Route NDJSON
|
|
1424
|
+
// through our progress renderer, which prints assistant prose and tool
|
|
1425
|
+
// names without dumping tool arguments or results into the terminal.
|
|
1426
|
+
const progressScript = fileURLToPath(new URL("./cursor-agent-progress.js", import.meta.url));
|
|
1427
|
+
argv.splice(0, 1, "node", progressScript, "--", agent.command);
|
|
1428
|
+
argv.push("-p", "--output-format", "stream-json", "--trust", "--approve-mcps", "--force", "--workspace", launchArgsOrCursorWorkspace);
|
|
1367
1429
|
}
|
|
1368
1430
|
if (agent.supportsModelOverride &&
|
|
1369
1431
|
typeof modelAlias === "string" &&
|
|
@@ -2197,7 +2259,7 @@ export function buildMcpProvisioningDeps(deps, bridgeApiBaseUrlOverride) {
|
|
|
2197
2259
|
// conductor's own on-disk build (absolute-build-path primary, npm-channel
|
|
2198
2260
|
// fallback) — never from process.cwd(). `nodeExecutable` is kept as "node"
|
|
2199
2261
|
// here: Ticket 3 (P2) can later replace this single field with
|
|
2200
|
-
//
|
|
2262
|
+
// EPIC_IMPLEMENTER_NODE_PATH without re-plumbing argv construction.
|
|
2201
2263
|
mcpServerInvocation: resolveMcpShimInvocationForRuntime({
|
|
2202
2264
|
moduleUrl: import.meta.url,
|
|
2203
2265
|
nodeExecutable: "node",
|
|
@@ -2317,6 +2379,9 @@ export function formatModelRoutingDiagnosticLine(d) {
|
|
|
2317
2379
|
return `model routing degraded: Codex model catalog failed validation. ${failOpen}`;
|
|
2318
2380
|
case "codex-entitlement-unverified":
|
|
2319
2381
|
return "model routing: Bridge permits the selected model; local Codex account entitlement cannot be verified non-interactively.";
|
|
2382
|
+
case "posture-unavailable":
|
|
2383
|
+
return ("model routing: optimization posture was unavailable; Quality routing was retained " +
|
|
2384
|
+
"(reason=optimization-posture-unavailable).");
|
|
2320
2385
|
default:
|
|
2321
2386
|
return `model routing degraded: ${d.message} ${failOpen}`;
|
|
2322
2387
|
}
|
|
@@ -2562,6 +2627,77 @@ export async function fetchDifficultyModelRoutingConfig(access, includeOverrides
|
|
|
2562
2627
|
};
|
|
2563
2628
|
}
|
|
2564
2629
|
}
|
|
2630
|
+
/** Type guard: true only for a recognized posture value. */
|
|
2631
|
+
export function isOptimizationPosture(value) {
|
|
2632
|
+
return value === "quality" || value === "speed" || value === "maximize_subscription";
|
|
2633
|
+
}
|
|
2634
|
+
const POSTURE_UNAVAILABLE_WARNING = "model routing: optimization posture was unavailable; Quality routing was retained " +
|
|
2635
|
+
"(reason=optimization-posture-unavailable)";
|
|
2636
|
+
/**
|
|
2637
|
+
* BAPI-1225: the single stable message every posture-resolution emitter uses,
|
|
2638
|
+
* verbatim. Textually identical to (but independently defined from) the Python
|
|
2639
|
+
* contract in `src/python/library/optimization_posture_observability.py` — the
|
|
2640
|
+
* two runtimes share no import, only the agreed-on literal string.
|
|
2641
|
+
*/
|
|
2642
|
+
const OPTIMIZATION_POSTURE_RESOLVED_MESSAGE = "optimization posture resolved";
|
|
2643
|
+
/** BAPI-1225: this harness diagnostic's fixed resolution-path value. */
|
|
2644
|
+
const RESOLUTION_PATH_HARNESS_ROUTING = "harness_routing";
|
|
2645
|
+
/**
|
|
2646
|
+
* Format the BAPI-1225 repository-level harness routing-diagnostic line.
|
|
2647
|
+
* Deterministic and secret-free: only the normalized closed posture value, the
|
|
2648
|
+
* canonical repo name already supplied to routing-config resolution, and the
|
|
2649
|
+
* constant resolution path — no raw config, resolved alias/model/tier, file
|
|
2650
|
+
* path, ticket content, error, credential, or fallback provenance.
|
|
2651
|
+
*/
|
|
2652
|
+
function formatOptimizationPostureResolvedLine(posture, repoName) {
|
|
2653
|
+
return (`${OPTIMIZATION_POSTURE_RESOLVED_MESSAGE} optimization_posture=${posture} ` +
|
|
2654
|
+
`repo_name=${repoName} resolution_path=${RESOLUTION_PATH_HARNESS_ROUTING}`);
|
|
2655
|
+
}
|
|
2656
|
+
/** Build the shared, secret-free, invocation-scoped posture-unavailable diagnostic. */
|
|
2657
|
+
function makePostureUnavailableDiagnostic() {
|
|
2658
|
+
// Deliberately carries no repoName/ticketKey/status so every degraded row's
|
|
2659
|
+
// diagnostic shares the same de-dup key (BAPI-377) and collapses to one line.
|
|
2660
|
+
return makeModelRoutingDiagnostic({
|
|
2661
|
+
kind: "posture-unavailable",
|
|
2662
|
+
message: "optimization posture unavailable; Quality routing retained",
|
|
2663
|
+
});
|
|
2664
|
+
}
|
|
2665
|
+
/**
|
|
2666
|
+
* Read and normalize the repo's `optimization_posture` config field in its own
|
|
2667
|
+
* total `try/catch`, entirely separate from {@link fetchDifficultyModelRoutingConfig}.
|
|
2668
|
+
* A posture read can never turn into that function's `{ ok: false }` result or
|
|
2669
|
+
* trigger the premium fallback — it only ever yields `"quality"` on any
|
|
2670
|
+
* unset/malformed/unreadable outcome, with a sanitized warning+diagnostic
|
|
2671
|
+
* attached when the outcome was NOT the ordinary unset case.
|
|
2672
|
+
*/
|
|
2673
|
+
export async function resolveOptimizationPostureForStartTickets(access) {
|
|
2674
|
+
try {
|
|
2675
|
+
const raw = await fetchStartTicketsConfigField(access, "optimization_posture");
|
|
2676
|
+
if (raw === null || raw === undefined) {
|
|
2677
|
+
// Ordinary S1 defaulting — NULL/unset resolves to quality, no diagnostic.
|
|
2678
|
+
return { posture: "quality", degraded: false };
|
|
2679
|
+
}
|
|
2680
|
+
if (isOptimizationPosture(raw)) {
|
|
2681
|
+
return { posture: raw, degraded: false };
|
|
2682
|
+
}
|
|
2683
|
+
// Unknown/malformed non-null value.
|
|
2684
|
+
return {
|
|
2685
|
+
posture: "quality",
|
|
2686
|
+
degraded: true,
|
|
2687
|
+
warning: POSTURE_UNAVAILABLE_WARNING,
|
|
2688
|
+
diagnostic: makePostureUnavailableDiagnostic(),
|
|
2689
|
+
};
|
|
2690
|
+
}
|
|
2691
|
+
catch {
|
|
2692
|
+
// Structured fetch failure or thrown error — never leak exception text.
|
|
2693
|
+
return {
|
|
2694
|
+
posture: "quality",
|
|
2695
|
+
degraded: true,
|
|
2696
|
+
warning: POSTURE_UNAVAILABLE_WARNING,
|
|
2697
|
+
diagnostic: makePostureUnavailableDiagnostic(),
|
|
2698
|
+
};
|
|
2699
|
+
}
|
|
2700
|
+
}
|
|
2565
2701
|
/** One bounded authenticated fetch; reject partial, unsafe, or stale wire data. */
|
|
2566
2702
|
export async function fetchCodexModelCatalog(access) {
|
|
2567
2703
|
let body;
|
|
@@ -2810,6 +2946,67 @@ async function applyPremiumFallbackToEligibleRows(deps, rows, isEligible, agent,
|
|
|
2810
2946
|
const isCreatedRoutingEligible = (r) => r.status === "created" && !!r.path;
|
|
2811
2947
|
/** Dry-run path: rows have no worktree path yet but are eligible for a preview. */
|
|
2812
2948
|
const isDryRunRoutingEligible = (r) => r.status === "dry-run";
|
|
2949
|
+
/**
|
|
2950
|
+
* Pure resolver for the diagnostic's `adjustment` token. `none` for manual
|
|
2951
|
+
* routing (posture `not_evaluated_manual_override`) and for `quality`/`speed`;
|
|
2952
|
+
* the exact transition token — including the `cheap` floor — under
|
|
2953
|
+
* `maximize_subscription`. Total and exhaustive over {@link ModelTier}.
|
|
2954
|
+
*/
|
|
2955
|
+
function resolveModelRoutingDecisionAdjustment(posture, baseline) {
|
|
2956
|
+
if (posture !== "maximize_subscription")
|
|
2957
|
+
return "none";
|
|
2958
|
+
switch (baseline) {
|
|
2959
|
+
case "premium":
|
|
2960
|
+
return "premium->basic";
|
|
2961
|
+
case "basic":
|
|
2962
|
+
return "basic->cheap";
|
|
2963
|
+
case "cheap":
|
|
2964
|
+
return "cheap->cheap_floor";
|
|
2965
|
+
}
|
|
2966
|
+
}
|
|
2967
|
+
/**
|
|
2968
|
+
* Format the exact deterministic diagnostic line. Inputs are restricted to
|
|
2969
|
+
* closed tier/posture/adjustment/source values plus an already-validated
|
|
2970
|
+
* alias, so raw command arguments, backend responses, URLs, headers, and
|
|
2971
|
+
* credentials cannot enter the line.
|
|
2972
|
+
*/
|
|
2973
|
+
export function formatModelRoutingDecisionDiagnosticLine(input) {
|
|
2974
|
+
return (`[model-routing] source=${input.source} baseline=${input.baseline} ` +
|
|
2975
|
+
`posture=${input.posture} adjustment=${input.adjustment} effective=${input.effective} ` +
|
|
2976
|
+
`alias=${input.alias ?? "unresolved"}`);
|
|
2977
|
+
}
|
|
2978
|
+
/**
|
|
2979
|
+
* Best-effort emitter: calls the supplied stderr sink once and swallows any
|
|
2980
|
+
* logger exception so diagnostic emission can never affect routing.
|
|
2981
|
+
*/
|
|
2982
|
+
function emitModelRoutingDecisionDiagnostic(errorLog, input) {
|
|
2983
|
+
try {
|
|
2984
|
+
errorLog(formatModelRoutingDecisionDiagnosticLine(input));
|
|
2985
|
+
}
|
|
2986
|
+
catch {
|
|
2987
|
+
// Diagnostic emission is best-effort and must never affect routing.
|
|
2988
|
+
}
|
|
2989
|
+
}
|
|
2990
|
+
/**
|
|
2991
|
+
* Emit one `manual_override` diagnostic per eligible row for the two manual
|
|
2992
|
+
* `--tier` routing paths (a recognized tier, or the malformed-value premium
|
|
2993
|
+
* fallback). `tier` is both the baseline and the effective tier — a manual
|
|
2994
|
+
* override is applied exactly as supplied, with posture never evaluated.
|
|
2995
|
+
*/
|
|
2996
|
+
function emitManualOverrideDiagnostics(errorLog, routedRows, isEligible, tier) {
|
|
2997
|
+
for (const row of routedRows) {
|
|
2998
|
+
if (!isEligible(row))
|
|
2999
|
+
continue;
|
|
3000
|
+
emitModelRoutingDecisionDiagnostic(errorLog, {
|
|
3001
|
+
source: "manual_override",
|
|
3002
|
+
baseline: tier,
|
|
3003
|
+
posture: "not_evaluated_manual_override",
|
|
3004
|
+
adjustment: "none",
|
|
3005
|
+
effective: tier,
|
|
3006
|
+
alias: row.modelAlias ?? null,
|
|
3007
|
+
});
|
|
3008
|
+
}
|
|
3009
|
+
}
|
|
2813
3010
|
/**
|
|
2814
3011
|
* BAPI-642 — apply a single explicitly injected tier to every eligible row,
|
|
2815
3012
|
* bypassing the per-ticket difficulty/tier HTTP lookup. The tier is still mapped
|
|
@@ -2858,7 +3055,15 @@ async function applyInjectedTierRoutingToEligibleRows(deps, rows, isEligible, ag
|
|
|
2858
3055
|
* `--model`; Claude/Cursor retain their premium fallback. Non-eligible rows
|
|
2859
3056
|
* pass through untouched, and routing failures never abort worktree creation.
|
|
2860
3057
|
*/
|
|
2861
|
-
async function resolveModelRoutingForEligible(deps, rows, options, agent, isEligible
|
|
3058
|
+
async function resolveModelRoutingForEligible(deps, rows, options, agent, isEligible,
|
|
3059
|
+
/**
|
|
3060
|
+
* BAPI-1225: injected sink for the harness routing-diagnostic line, matching
|
|
3061
|
+
* the shape of the existing `modelRoutingWarningLog`/`errorLog` seams. Every
|
|
3062
|
+
* caller's production path already has a `console.error`-backed logger in
|
|
3063
|
+
* scope; a missing injection (e.g. an older test double) falls back to
|
|
3064
|
+
* `console.error` directly, never `console.log`/stdout.
|
|
3065
|
+
*/
|
|
3066
|
+
errorLog = (message) => console.error(message)) {
|
|
2862
3067
|
const eligible = rows.filter(isEligible);
|
|
2863
3068
|
if (eligible.length === 0)
|
|
2864
3069
|
return rows;
|
|
@@ -2901,98 +3106,276 @@ async function resolveModelRoutingForEligible(deps, rows, options, agent, isElig
|
|
|
2901
3106
|
// difficulty/tier lookup — `fetchTicketModelTiersForRows` is NEVER called for
|
|
2902
3107
|
// any injected value. A recognized tier is applied to every eligible row; a
|
|
2903
3108
|
// malformed value (INJECTED_TIER_UNRESOLVED) fails open to the premium
|
|
2904
|
-
// fallback. When the flag is absent (`injected === undefined`) the
|
|
2905
|
-
// difficulty-lookup path below runs
|
|
3109
|
+
// fallback. When the flag is absent (`injected === undefined`) the
|
|
3110
|
+
// automatic-snapshot / difficulty-lookup path below runs.
|
|
3111
|
+
//
|
|
3112
|
+
// BAPI-1224: an explicit `--tier` is an operator-forced MANUAL override and is
|
|
3113
|
+
// applied exactly as supplied, below, with no posture downshifting — the
|
|
3114
|
+
// optimization posture is never even read on this path (it is resolved only
|
|
3115
|
+
// further down, immediately before the automatic-snapshot and per-ticket
|
|
3116
|
+
// difficulty-lookup paths).
|
|
2906
3117
|
const injected = options.injectedTier;
|
|
2907
3118
|
if (injected === INJECTED_TIER_UNRESOLVED) {
|
|
2908
3119
|
const warning = "model routing: --tier was not one of cheap|basic|premium; defaulting to premium routing";
|
|
2909
3120
|
const apply = await makePremiumFallbackApplier(deps, agent, overrides, catalog, access.repoName);
|
|
2910
|
-
|
|
3121
|
+
const routed = rows.map((r) => (isEligible(r) ? apply(r, warning) : r));
|
|
3122
|
+
// BAPI-1236: the malformed-value premium fallback is still a manual
|
|
3123
|
+
// override — never posture-evaluated — so it emits `manual_override` too.
|
|
3124
|
+
emitManualOverrideDiagnostics(errorLog, routed, isEligible, "premium");
|
|
3125
|
+
return routed;
|
|
2911
3126
|
}
|
|
2912
3127
|
if (injected) {
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
//
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
|
|
3128
|
+
const routed = await applyInjectedTierRoutingToEligibleRows(deps, rows, isEligible, agent, overrides, injected, catalog, access.repoName);
|
|
3129
|
+
emitManualOverrideDiagnostics(errorLog, routed, isEligible, injected);
|
|
3130
|
+
return routed;
|
|
3131
|
+
}
|
|
3132
|
+
// BAPI-1224/BAPI-1236: resolve the repo's optimization posture ONCE, shared by
|
|
3133
|
+
// both the automatic-snapshot path (BAPI-1236's `--auto-tier`, immediately
|
|
3134
|
+
// below) and the ordinary per-ticket difficulty-lookup path — it cannot
|
|
3135
|
+
// affect any earlier short-circuit (no-eligible-row, unsupported agent,
|
|
3136
|
+
// access/config failure, routing-disabled, or the manual injected-tier
|
|
3137
|
+
// override above). The resolver is total and never throws; a degraded read
|
|
3138
|
+
// fails safe to "quality" (no downshift) plus a sanitized, invocation-scoped
|
|
3139
|
+
// warning/diagnostic attached to `out` below, regardless of which branch
|
|
3140
|
+
// produced it.
|
|
3141
|
+
const postureResolution = await resolveOptimizationPostureForStartTickets(access);
|
|
3142
|
+
const posture = postureResolution.posture;
|
|
3143
|
+
// BAPI-1225: one deterministic, best-effort diagnostic line per repository-level
|
|
3144
|
+
// resolution, emitted exactly here — outside the per-ticket row loop below, so a
|
|
3145
|
+
// multi-ticket invocation still logs it once for this canonical repository. Uses
|
|
3146
|
+
// the SAME normalized `posture` value that decides the S5 downshift immediately
|
|
3147
|
+
// below; this never re-resolves configuration. The existing `postureResolution`
|
|
3148
|
+
// degraded-warning/diagnostic plumbing is untouched — this is an addition, not a
|
|
3149
|
+
// replacement.
|
|
3150
|
+
try {
|
|
3151
|
+
errorLog(formatOptimizationPostureResolvedLine(posture, access.repoName));
|
|
3152
|
+
}
|
|
3153
|
+
catch {
|
|
3154
|
+
// Diagnostic emission is best-effort and must never affect routing.
|
|
3155
|
+
}
|
|
3156
|
+
let out;
|
|
3157
|
+
const autoTier = options.autoTier;
|
|
3158
|
+
if (autoTier) {
|
|
3159
|
+
// BAPI-1236 — automatic-snapshot routing: a machine-carried raw tier from a
|
|
3160
|
+
// chained automation handoff (e.g. `/review-and-implement`'s `--auto-tier`).
|
|
3161
|
+
// Routed through the CURRENT session's posture EXACTLY ONCE — unlike the
|
|
3162
|
+
// manual override above, this path IS subject to `downshiftModelTier()`
|
|
3163
|
+
// under `maximize_subscription` — while skipping ONLY the redundant
|
|
3164
|
+
// per-ticket `fetchTicketModelTiersForRows()` lookup. The alias is
|
|
3165
|
+
// resolved+validated ONCE (identical for every row), matching the premium
|
|
3166
|
+
// fallback / injected-tier hoisting pattern above.
|
|
3167
|
+
const effectiveTier = posture === "maximize_subscription" ? downshiftModelTier(autoTier) : autoTier;
|
|
3168
|
+
const adjustment = resolveModelRoutingDecisionAdjustment(posture, autoTier);
|
|
3169
|
+
const lineageSuffix = posture === "maximize_subscription"
|
|
3170
|
+
? `; auto-tier:${autoTier}->maximize:${effectiveTier}`
|
|
3171
|
+
: "";
|
|
3172
|
+
const alias = resolveRoutingAlias(agent, effectiveTier, overrides, catalog);
|
|
3173
|
+
let resolvedAlias = null;
|
|
3174
|
+
let aliasWarning = null;
|
|
3175
|
+
if (!alias) {
|
|
3176
|
+
aliasWarning = `model routing: no valid alias for tier=${effectiveTier}; using agent default`;
|
|
3177
|
+
}
|
|
3178
|
+
else {
|
|
3179
|
+
const validation = await validateResolvedModelAliasForAgent(deps, agent, alias);
|
|
3180
|
+
if (validation.ok) {
|
|
3181
|
+
resolvedAlias = validation.alias;
|
|
3182
|
+
}
|
|
3183
|
+
else {
|
|
3184
|
+
aliasWarning = validation.warning;
|
|
3185
|
+
}
|
|
2929
3186
|
}
|
|
2930
|
-
const
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
:
|
|
2940
|
-
|
|
3187
|
+
const reason = resolvedAlias
|
|
3188
|
+
? `auto-tier=${autoTier} model=${resolvedAlias} (posture=${posture})${lineageSuffix}`
|
|
3189
|
+
: `${aliasWarning}${lineageSuffix}`;
|
|
3190
|
+
out = rows.map((row) => {
|
|
3191
|
+
if (!isEligible(row))
|
|
3192
|
+
return row;
|
|
3193
|
+
const baseRow = {
|
|
3194
|
+
...row,
|
|
3195
|
+
difficulty: null,
|
|
3196
|
+
modelTier: effectiveTier,
|
|
3197
|
+
modelAlias: resolvedAlias,
|
|
3198
|
+
modelRoutingSource: "automatic_snapshot",
|
|
3199
|
+
modelRoutingReason: reason,
|
|
3200
|
+
...(agent.name === "codex" && resolvedAlias
|
|
3201
|
+
? { modelRoutingDiagnostic: codexEntitlementDiagnostic(access.repoName) }
|
|
3202
|
+
: {}),
|
|
3203
|
+
};
|
|
3204
|
+
return aliasWarning ? appendSummaryRowWarning(baseRow, aliasWarning) : baseRow;
|
|
3205
|
+
});
|
|
3206
|
+
// BAPI-1236: one `automatic_snapshot` diagnostic per eligible row — the
|
|
3207
|
+
// carried baseline, the newly resolved posture, the exact adjustment token,
|
|
3208
|
+
// the calculated effective tier, and the validated alias (or `unresolved`).
|
|
3209
|
+
for (const row of rows) {
|
|
3210
|
+
if (!isEligible(row))
|
|
3211
|
+
continue;
|
|
3212
|
+
emitModelRoutingDecisionDiagnostic(errorLog, {
|
|
3213
|
+
source: "automatic_snapshot",
|
|
3214
|
+
baseline: autoTier,
|
|
3215
|
+
posture,
|
|
3216
|
+
adjustment,
|
|
3217
|
+
effective: effectiveTier,
|
|
3218
|
+
alias: resolvedAlias,
|
|
3219
|
+
});
|
|
3220
|
+
}
|
|
3221
|
+
}
|
|
3222
|
+
else {
|
|
3223
|
+
// Ordinary per-ticket difficulty-lookup path.
|
|
3224
|
+
const tierMap = await fetchTicketModelTiersForRows(access, eligible, options.maxParallel, isEligible);
|
|
3225
|
+
// Lazily resolve+validate the premium fallback alias at most once for this run
|
|
3226
|
+
// (the alias is identical across rows; cursor-agent validation spawns a probe).
|
|
3227
|
+
let premiumApplier = null;
|
|
3228
|
+
const applyPremiumFallback = async (row, warning, diagnostic) => {
|
|
3229
|
+
if (!premiumApplier)
|
|
3230
|
+
premiumApplier = await makePremiumFallbackApplier(deps, agent, overrides, catalog, access.repoName);
|
|
3231
|
+
return premiumApplier(row, warning, diagnostic);
|
|
3232
|
+
};
|
|
3233
|
+
const loopOut = [];
|
|
3234
|
+
for (const row of rows) {
|
|
3235
|
+
if (!isEligible(row)) {
|
|
3236
|
+
loopOut.push(row);
|
|
3237
|
+
continue;
|
|
3238
|
+
}
|
|
3239
|
+
const tierResult = tierMap.get(row.key);
|
|
3240
|
+
if (!tierResult || !tierResult.ok) {
|
|
3241
|
+
// Error reaching the per-ticket tier endpoint — default to premium/Opus,
|
|
3242
|
+
// stamping the per-ticket tier-fetch diagnostic.
|
|
3243
|
+
const warning = tierResult && !tierResult.ok
|
|
3244
|
+
? tierResult.warning
|
|
3245
|
+
: `model routing: no tier resolved for ${row.key}; assuming hard ticket`;
|
|
3246
|
+
const diagnostic = tierResult && !tierResult.ok
|
|
3247
|
+
? tierResult.diagnostic
|
|
3248
|
+
: makeModelRoutingDiagnostic({
|
|
3249
|
+
kind: "tier-fetch-unavailable",
|
|
3250
|
+
repoName: access.repoName,
|
|
3251
|
+
storeTarget: `bapi:${access.repoName}`,
|
|
3252
|
+
ticketKey: row.key,
|
|
3253
|
+
message: `no tier resolved for ${row.key}`,
|
|
3254
|
+
});
|
|
3255
|
+
if (agent.name === "codex") {
|
|
3256
|
+
loopOut.push(codexDefaultRows([row], () => true, warning, diagnostic)[0]);
|
|
3257
|
+
continue;
|
|
3258
|
+
}
|
|
3259
|
+
loopOut.push(await applyPremiumFallback(row, warning, diagnostic));
|
|
3260
|
+
continue;
|
|
3261
|
+
}
|
|
3262
|
+
const { difficulty, tier, source } = tierResult.value;
|
|
3263
|
+
if (!tier) {
|
|
3264
|
+
// Backend reported no usable difficulty — default to premium/Opus. (The
|
|
3265
|
+
// backend now returns a premium fallback itself, so this is defensive.)
|
|
3266
|
+
// BAPI-1224: this is the harness-LOCAL premium fallback, deliberately
|
|
3267
|
+
// exempt from maximize downshifting — this fail-safe-to-strong path must
|
|
3268
|
+
// stay strong regardless of posture, so `makePremiumFallbackApplier`
|
|
3269
|
+
// receives the literal "premium" tier unchanged.
|
|
3270
|
+
const baseRow = { ...row, difficulty: difficulty ?? null, modelTier: tier ?? null, modelRoutingSource: source };
|
|
3271
|
+
const warning = `model routing: ${row.key} resolved no tier (source=${source}); assuming hard ticket`;
|
|
3272
|
+
const diagnostic = makeModelRoutingDiagnostic({
|
|
3273
|
+
kind: "no-tier",
|
|
2941
3274
|
repoName: access.repoName,
|
|
2942
3275
|
storeTarget: `bapi:${access.repoName}`,
|
|
2943
3276
|
ticketKey: row.key,
|
|
2944
|
-
message:
|
|
3277
|
+
message: `${row.key} resolved no tier (source=${source})`,
|
|
2945
3278
|
});
|
|
2946
|
-
|
|
2947
|
-
|
|
3279
|
+
if (agent.name === "codex") {
|
|
3280
|
+
loopOut.push(codexDefaultRows([baseRow], () => true, warning, diagnostic)[0]);
|
|
3281
|
+
continue;
|
|
3282
|
+
}
|
|
3283
|
+
loopOut.push(await applyPremiumFallback(baseRow, warning, diagnostic));
|
|
2948
3284
|
continue;
|
|
2949
3285
|
}
|
|
2950
|
-
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
|
|
2959
|
-
|
|
2960
|
-
|
|
2961
|
-
//
|
|
2962
|
-
//
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
3286
|
+
// BAPI-1224 — automatic downshift call site: the backend-returned tier
|
|
3287
|
+
// (`difficultyTier`, retained regardless of whether its source is
|
|
3288
|
+
// `cached`/`computed`/`fallback` — a valid backend "fallback" tier is NOT
|
|
3289
|
+
// the harness-local fallback above and IS downshifted) is transformed to
|
|
3290
|
+
// `effectiveTier` only under `maximize_subscription`; every other posture
|
|
3291
|
+
// passes it through unchanged. The downshift happens here, before
|
|
3292
|
+
// `resolveRoutingAlias()`, so `applyInjectedTierRoutingToEligibleRows()`
|
|
3293
|
+
// and `makePremiumFallbackApplier()` — which call `resolveRoutingAlias()`
|
|
3294
|
+
// with an injected or literal "premium" tier — can never be downshifted
|
|
3295
|
+
// by accident. Claude and Cursor resolve `effectiveTier` through their
|
|
3296
|
+
// unmodified registry `tierModels`; Codex indexes the fetched
|
|
3297
|
+
// `catalog.models[effectiveTier]` — neither gains a maximize-specific
|
|
3298
|
+
// alias table. Copilot never reaches this loop at all: it has no
|
|
3299
|
+
// `AGENT_REGISTRY` entry and `isRunnableHostTarget()` rejects hosts
|
|
3300
|
+
// without a launch agent before an `AgentSpec` is ever resolved.
|
|
3301
|
+
const difficultyTier = tier;
|
|
3302
|
+
const effectiveTier = posture === "maximize_subscription" ? downshiftModelTier(difficultyTier) : difficultyTier;
|
|
3303
|
+
const adjustment = resolveModelRoutingDecisionAdjustment(posture, difficultyTier);
|
|
3304
|
+
const lineageSuffix = posture === "maximize_subscription"
|
|
3305
|
+
? `; difficulty:${difficultyTier}->maximize:${effectiveTier}`
|
|
3306
|
+
: "";
|
|
3307
|
+
// BAPI-1236: stamp `difficulty_lookup` rather than copying the backend
|
|
3308
|
+
// cache/computation source (`cached`/`computed`/`fallback`) into the
|
|
3309
|
+
// summary's own routing-provenance field — that backend source is
|
|
3310
|
+
// preserved separately in `modelRoutingReason` below via `(source=...)`.
|
|
3311
|
+
const baseRow = {
|
|
3312
|
+
...row,
|
|
3313
|
+
difficulty: difficulty ?? null,
|
|
3314
|
+
modelTier: effectiveTier,
|
|
3315
|
+
modelRoutingSource: "difficulty_lookup",
|
|
3316
|
+
};
|
|
3317
|
+
const alias = resolveRoutingAlias(agent, effectiveTier, overrides, catalog);
|
|
3318
|
+
if (!alias) {
|
|
3319
|
+
const warning = `model routing: no valid alias for tier=${effectiveTier}; using agent default`;
|
|
3320
|
+
loopOut.push(appendSummaryRowWarning({ ...baseRow, modelAlias: null, modelRoutingReason: warning + lineageSuffix }, warning));
|
|
3321
|
+
emitModelRoutingDecisionDiagnostic(errorLog, {
|
|
3322
|
+
source: "difficulty_lookup",
|
|
3323
|
+
baseline: difficultyTier,
|
|
3324
|
+
posture,
|
|
3325
|
+
adjustment,
|
|
3326
|
+
effective: effectiveTier,
|
|
3327
|
+
alias: null,
|
|
3328
|
+
});
|
|
2973
3329
|
continue;
|
|
2974
3330
|
}
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
3331
|
+
const validation = await validateResolvedModelAliasForAgent(deps, agent, alias);
|
|
3332
|
+
if (!validation.ok) {
|
|
3333
|
+
loopOut.push(appendSummaryRowWarning({ ...baseRow, modelAlias: null, modelRoutingReason: validation.warning + lineageSuffix }, validation.warning));
|
|
3334
|
+
emitModelRoutingDecisionDiagnostic(errorLog, {
|
|
3335
|
+
source: "difficulty_lookup",
|
|
3336
|
+
baseline: difficultyTier,
|
|
3337
|
+
posture,
|
|
3338
|
+
adjustment,
|
|
3339
|
+
effective: effectiveTier,
|
|
3340
|
+
alias: null,
|
|
3341
|
+
});
|
|
3342
|
+
continue;
|
|
3343
|
+
}
|
|
3344
|
+
loopOut.push({
|
|
3345
|
+
...baseRow,
|
|
3346
|
+
modelAlias: validation.alias,
|
|
3347
|
+
modelRoutingReason: `tier=${effectiveTier} model=${validation.alias} (source=${source})${lineageSuffix}`,
|
|
3348
|
+
...(agent.name === "codex"
|
|
3349
|
+
? { modelRoutingDiagnostic: codexEntitlementDiagnostic(access.repoName) }
|
|
3350
|
+
: {}),
|
|
3351
|
+
});
|
|
3352
|
+
emitModelRoutingDecisionDiagnostic(errorLog, {
|
|
3353
|
+
source: "difficulty_lookup",
|
|
3354
|
+
baseline: difficultyTier,
|
|
3355
|
+
posture,
|
|
3356
|
+
adjustment,
|
|
3357
|
+
effective: effectiveTier,
|
|
3358
|
+
alias: validation.alias,
|
|
3359
|
+
});
|
|
2988
3360
|
}
|
|
2989
|
-
out
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
|
|
2994
|
-
|
|
2995
|
-
|
|
3361
|
+
out = loopOut;
|
|
3362
|
+
}
|
|
3363
|
+
// BAPI-1224: a degraded posture read attaches ONE sanitized warning +
|
|
3364
|
+
// invocation-scoped diagnostic to every eligible row, without altering any
|
|
3365
|
+
// row's already-resolved tier/alias. A row that already carries a stronger
|
|
3366
|
+
// routing diagnostic (tier-fetch/no-tier/etc.) keeps it — the posture
|
|
3367
|
+
// diagnostic never overwrites a stronger one, so `selectPrimaryInvocation-
|
|
3368
|
+
// ModelRoutingDiagnostic` still surfaces the more actionable finding. Applies
|
|
3369
|
+
// uniformly to `out` regardless of whether the automatic-snapshot or the
|
|
3370
|
+
// ordinary difficulty-lookup branch produced it.
|
|
3371
|
+
if (postureResolution.degraded) {
|
|
3372
|
+
return out.map((r) => {
|
|
3373
|
+
if (!isEligible(r))
|
|
3374
|
+
return r;
|
|
3375
|
+
const withWarning = appendSummaryRowWarning(r, postureResolution.warning);
|
|
3376
|
+
return r.modelRoutingDiagnostic
|
|
3377
|
+
? withWarning
|
|
3378
|
+
: { ...withWarning, modelRoutingDiagnostic: postureResolution.diagnostic };
|
|
2996
3379
|
});
|
|
2997
3380
|
}
|
|
2998
3381
|
return out;
|
|
@@ -3002,8 +3385,8 @@ async function resolveModelRoutingForEligible(deps, rows, options, agent, isElig
|
|
|
3002
3385
|
* entrypoint for the real spawn path. ALWAYS fail-open (see
|
|
3003
3386
|
* {@link resolveModelRoutingForEligible}).
|
|
3004
3387
|
*/
|
|
3005
|
-
export async function resolveModelRoutingForRows(deps, rows, options, agent) {
|
|
3006
|
-
return resolveModelRoutingForEligible(deps, rows, options, agent, isCreatedRoutingEligible);
|
|
3388
|
+
export async function resolveModelRoutingForRows(deps, rows, options, agent, errorLog) {
|
|
3389
|
+
return resolveModelRoutingForEligible(deps, rows, options, agent, isCreatedRoutingEligible, errorLog);
|
|
3007
3390
|
}
|
|
3008
3391
|
/**
|
|
3009
3392
|
* Dry-run variant: resolve routing for `dry-run` rows so `--dry-run` can preview
|
|
@@ -3013,8 +3396,8 @@ export async function resolveModelRoutingForRows(deps, rows, options, agent) {
|
|
|
3013
3396
|
* difficulty the backend may compute and cache it (one LLM call) — exactly what
|
|
3014
3397
|
* the subsequent real run would have done.
|
|
3015
3398
|
*/
|
|
3016
|
-
export async function resolveModelRoutingForDryRun(deps, rows, options, agent) {
|
|
3017
|
-
return resolveModelRoutingForEligible(deps, rows, options, agent, isDryRunRoutingEligible);
|
|
3399
|
+
export async function resolveModelRoutingForDryRun(deps, rows, options, agent, errorLog) {
|
|
3400
|
+
return resolveModelRoutingForEligible(deps, rows, options, agent, isDryRunRoutingEligible, errorLog);
|
|
3018
3401
|
}
|
|
3019
3402
|
/**
|
|
3020
3403
|
* Format one concise routing decision line per ticket:
|
|
@@ -3026,6 +3409,11 @@ export async function resolveModelRoutingForDryRun(deps, rows, options, agent) {
|
|
|
3026
3409
|
* `--tier`-driven implementation handoff. The ORDINARY implementation launch (a
|
|
3027
3410
|
* plain `--workflow implement` with no `--tier`) appends NO label, so its line
|
|
3028
3411
|
* stays byte-for-byte identical to the pre-feature format.
|
|
3412
|
+
*
|
|
3413
|
+
* BAPI-1236: `routing=auto-snapshot` is exclusive to a stored
|
|
3414
|
+
* `modelRoutingSource: "automatic_snapshot"` row (a `--auto-tier` machine
|
|
3415
|
+
* handoff) and is never applied to an `injected` row, so an automatic snapshot
|
|
3416
|
+
* can never be misread as a manual `--tier` override in the diagnostics.
|
|
3029
3417
|
*/
|
|
3030
3418
|
export function formatModelRoutingLine(row, agent, context) {
|
|
3031
3419
|
const difficulty = typeof row.difficulty === "number" ? String(row.difficulty) : "?";
|
|
@@ -3037,6 +3425,8 @@ export function formatModelRoutingLine(row, agent, context) {
|
|
|
3037
3425
|
labels.push("phase=review");
|
|
3038
3426
|
if (row.modelRoutingSource === "injected")
|
|
3039
3427
|
labels.push("routing=injected");
|
|
3428
|
+
if (row.modelRoutingSource === "automatic_snapshot")
|
|
3429
|
+
labels.push("routing=auto-snapshot");
|
|
3040
3430
|
return labels.length > 0 ? `${base} ${labels.join(" ")}` : base;
|
|
3041
3431
|
}
|
|
3042
3432
|
/** Stable de-dup key for an invocation-level routing diagnostic. */
|
|
@@ -3406,7 +3796,7 @@ export async function orchestrateStartTickets(deps, options, overrides = {}) {
|
|
|
3406
3796
|
// BEFORE building the spawn command. Always fail-open: routing never aborts a
|
|
3407
3797
|
// spawn — at worst a ticket runs on the agent's default model.
|
|
3408
3798
|
const resolveRoutingFn = overrides.resolveModelRoutingForRows ?? resolveModelRoutingForRows;
|
|
3409
|
-
const routed = await resolveRoutingFn(deps, conductorReady, options, agent);
|
|
3799
|
+
const routed = await resolveRoutingFn(deps, conductorReady, options, agent, overrides.modelRoutingWarningLog);
|
|
3410
3800
|
// Per-ticket routing DECISION lines stay (one per spawnable ticket). The
|
|
3411
3801
|
// noisy per-row routing WARNING emission is intentionally gone (BAPI-377): the
|
|
3412
3802
|
// CLI now emits a single de-duplicated, actionable routing diagnostic per
|
|
@@ -3427,14 +3817,20 @@ export async function orchestrateStartTickets(deps, options, overrides = {}) {
|
|
|
3427
3817
|
let scoped = stampIndexScopeOnRows(routed, indexScope);
|
|
3428
3818
|
if (agent.name === "codex") {
|
|
3429
3819
|
const mcpDeps = buildMcpProvisioningDeps(deps);
|
|
3430
|
-
|
|
3431
|
-
|
|
3432
|
-
|
|
3820
|
+
// Scope resolution can refresh one shared Codex profile. Serialize these
|
|
3821
|
+
// reads and writes so sibling worktrees cannot observe a half-written update.
|
|
3822
|
+
const resolved = [];
|
|
3823
|
+
for (const row of scoped) {
|
|
3824
|
+
if (row.status !== "created" || !row.path) {
|
|
3825
|
+
resolved.push(row);
|
|
3826
|
+
continue;
|
|
3827
|
+
}
|
|
3433
3828
|
const scope = await (overrides.resolveCodexScope ?? resolveCodexLaunchScope)(row.path, mcpDeps);
|
|
3434
|
-
|
|
3829
|
+
resolved.push(scope.ok
|
|
3435
3830
|
? { ...row, codexLaunchArgs: scope.argv }
|
|
3436
|
-
: { ...row, status: "spawn-failed", error: scope.error };
|
|
3437
|
-
}
|
|
3831
|
+
: { ...row, status: "spawn-failed", error: scope.error });
|
|
3832
|
+
}
|
|
3833
|
+
scoped = resolved;
|
|
3438
3834
|
}
|
|
3439
3835
|
const rows = await spawnTabsFn(deps, scoped, terminal, platformConfig.config.buildAgentShellCommand);
|
|
3440
3836
|
return { ok: true, rows };
|
|
@@ -3513,7 +3909,7 @@ export async function runStartTicketsCli(argv, overrides = {}) {
|
|
|
3513
3909
|
// per-ticket routing decision line.
|
|
3514
3910
|
const resolveDryRunRoutingFn = overrides.resolveModelRoutingForDryRun ?? resolveModelRoutingForDryRun;
|
|
3515
3911
|
const dryRunRows = buildDryRunResults(options.keys, options.branchOverrides);
|
|
3516
|
-
const routedDryRunRows = await resolveDryRunRoutingFn(deps, dryRunRows, options, agent);
|
|
3912
|
+
const routedDryRunRows = await resolveDryRunRoutingFn(deps, dryRunRows, options, agent, errorLog);
|
|
3517
3913
|
const routedByKey = new Map(routedDryRunRows.map((r) => [r.key, r]));
|
|
3518
3914
|
// Resolve the SAME worker MCP shim invocation the real provisioning would use
|
|
3519
3915
|
// so the dry-run preview matches the `.mcp.json` that would actually be written.
|