@bridge_gpt/mcp-server 0.2.58 → 0.2.60
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 +231 -113
- package/build/agent-guidance.generated.js +1 -1
- package/build/agent-notes.js +7 -1
- 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 +1 -1
- package/build/commands.generated.js +7 -8
- package/build/conductor/bridge-api-client.js +202 -2
- package/build/conductor/bring-up-facts.js +1 -1
- package/build/conductor/claude-hook.js +16 -16
- package/build/conductor/doctor.js +279 -72
- package/build/conductor/epic-implementer-cli.js +38 -5
- package/build/conductor/epic-reconcile.js +1 -1
- package/build/conductor/epic-runtime.js +21 -12
- 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 +28 -6
- package/build/conductor/local-merge.js +319 -26
- package/build/conductor/merge-execution-contract.js +61 -0
- package/build/conductor/merge-execution-eligibility.js +129 -0
- 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 +166 -9
- package/build/conductor/readiness.js +97 -16
- package/build/conductor/recovery-cli.js +243 -12
- package/build/conductor/recovery-operations.js +64 -6
- 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/connect-bitbucket-api.js +6 -1
- package/build/connect-bitbucket.js +398 -88
- package/build/credential-store.js +16 -0
- package/build/docs.generated.js +2 -2
- package/build/doctor.js +199 -303
- package/build/drive-epic.js +88 -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 +184 -0
- package/build/epic-integration-pr.js +2 -2
- package/build/executor/ci-rerun-job.js +269 -0
- package/build/executor/cli.js +31 -0
- package/build/executor/credentials.js +11 -0
- package/build/executor/env.js +79 -8
- package/build/executor/heartbeat.js +1 -1
- package/build/executor/index.js +21 -4
- package/build/executor/job-errors.js +3 -3
- package/build/executor/job-runner.js +127 -1
- package/build/executor/job-types.js +12 -2
- package/build/executor/merge-job.js +46 -5
- 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/executor/service-lifecycle.js +136 -0
- package/build/executor/service-unit.js +427 -9
- package/build/executor/types.js +5 -0
- package/build/index.js +320 -342
- package/build/init.js +20 -28
- package/build/install-bridge-conductor.js +208 -15
- package/build/install-bridge.js +555 -37
- package/build/mcp-host-entry-adapter.js +6 -0
- package/build/mcp-host-targets.js +2 -0
- package/build/mcp-install-state.js +42 -4
- 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 +118 -10
- package/build/merge-workflow-codeowners.js +224 -0
- package/build/merge-workflow-config.js +133 -0
- package/build/merge-workflow-drift.js +66 -0
- package/build/merge-workflow-install.js +638 -0
- package/build/merge-workflow.js +572 -0
- package/build/model-routing-host.js +25 -0
- package/build/model-routing-preferences.js +99 -0
- package/build/pipelines.generated.js +10 -10
- package/build/{plan-epic-conductor-eligibility.js → plan-epic-implementer-eligibility.js} +18 -16
- package/build/plan-execution-ledger.js +8 -0
- package/build/plane/cli.js +24 -2
- package/build/plane/member-roster.js +5 -5
- package/build/plane/preflight.js +68 -0
- 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/run-unit-tests-launcher.js +5 -0
- package/build/setup-epic.js +171 -17
- package/build/start-tickets-conductor.js +32 -32
- package/build/start-tickets-prereqs.js +4 -9
- package/build/start-tickets.js +475 -91
- 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 +4 -4
- package/pipelines/implement-ticket.json +1 -1
- package/pipelines/plan-epic.json +2 -2
- package/build/conduct-epic/bridge-client.js +0 -773
- package/build/conduct-epic/checkpoint-store.js +0 -542
- package/build/conduct-epic/cli.js +0 -3145
- package/build/conduct-epic/cut-protocol.js +0 -392
- package/build/conduct-epic/lock.js +0 -302
- package/build/conduct-epic/pr-state.js +0 -286
- package/build/conduct-epic/spawn.js +0 -113
- package/build/conductor/cli.js +0 -1278
- package/build/conductor-bundle-cli.js +0 -256
- package/build/direct-ticket-tools.js +0 -70
- package/build/executor/reconcile-overlap-governance.js +0 -129
- package/build/implement-epic/checkpoint-store.js +0 -542
- package/build/implement-epic/cli.js +0 -3158
- package/build/implement-epic/lock.js +0 -302
- package/build/implement-epic/pr-state.js +0 -286
- package/build/implement-epic/spawn.js +0 -113
- /package/build/{implement-epic → epic-shared}/bridge-client.js +0 -0
- /package/build/{implement-epic → epic-shared}/cut-protocol.js +0 -0
package/build/start-tickets.js
CHANGED
|
@@ -77,7 +77,7 @@ import { TICKET_KEY_PATTERN as SHARED_TICKET_KEY_PATTERN, TICKET_KEY_USAGE_SUMMA
|
|
|
77
77
|
// can never drift. `start-tickets.ts` imports VALUES from there; the prereqs
|
|
78
78
|
// module imports only TYPES back, so the runtime graph stays acyclic.
|
|
79
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";
|
|
80
|
-
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";
|
|
81
81
|
import { agentForPlatform, getHostTarget, getWorktreeHostTargets, isRunnableHostTarget, isHostPlatformId } from "./mcp-host-targets.js";
|
|
82
82
|
import { resolveCodexLaunchScope } from "./codex-launch-scope.js";
|
|
83
83
|
import { tmpdir } from "node:os";
|
|
@@ -135,7 +135,8 @@ export function getStartTicketsUsage() {
|
|
|
135
135
|
" --host claude-code|cursor|codex|copilot-vscode|windsurf Current host when --agent is omitted; Claude Code, Cursor, and Codex can launch sessions",
|
|
136
136
|
" Cursor with --auto launches headlessly after a bounded readiness check; without --auto it stays interactive for the review gate.",
|
|
137
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.",
|
|
138
|
-
" --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.",
|
|
139
140
|
" --rounds 1|2 Review round count forwarded to the review phase; review-only, valid only with --workflow review-and-implement",
|
|
140
141
|
" --terminal terminal|iterm Override the macOS terminal app (default: auto-detect via $TERM_PROGRAM); honored on macOS only",
|
|
141
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",
|
|
@@ -143,16 +144,16 @@ export function getStartTicketsUsage() {
|
|
|
143
144
|
" --base-branch BRANCH Cut new worktrees from BRANCH and refresh origin/BRANCH (default: main)",
|
|
144
145
|
" --no-refresh-main Skip refresh of the configured base branch (default main); historical name retained for backward compatibility",
|
|
145
146
|
" --max-parallel N Max worktrees to create concurrently (default: 3)",
|
|
146
|
-
" --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.",
|
|
147
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.",
|
|
148
149
|
" -h, --help Show this help",
|
|
149
150
|
"",
|
|
150
151
|
"Environment:",
|
|
151
152
|
` ${WORKTRUNK_BINARY_OVERRIDE_ENV} Override the Worktrunk executable name/path for nonstandard installs`,
|
|
152
153
|
` ${TMUX_SESSION_OVERRIDE_ENV} Override the tmux session-name prefix on Linux (default: ${DEFAULT_TMUX_SESSION_PREFIX})`,
|
|
153
|
-
"
|
|
154
|
-
"
|
|
155
|
-
"
|
|
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",
|
|
156
157
|
"",
|
|
157
158
|
"Conductor observability (opt-in via --conductor):",
|
|
158
159
|
" With --conductor, real Claude Code workers launched by start-tickets receive",
|
|
@@ -160,7 +161,7 @@ export function getStartTicketsUsage() {
|
|
|
160
161
|
" local lifecycle events into the conductor ledger. Each such run mints one run_id",
|
|
161
162
|
" and attributes worker events by worker_id, ticket key, and worktree path, and a",
|
|
162
163
|
" supervisor peer tab is opened. Without --conductor none of this happens. Inspect",
|
|
163
|
-
" the ledger with the `conductor` CLI. The
|
|
164
|
+
" the ledger with the `conductor` CLI. The BAPI_EPIC_IMPLEMENTER_* env vars above apply",
|
|
164
165
|
" only when --conductor is set.",
|
|
165
166
|
"",
|
|
166
167
|
"Prerequisites:",
|
|
@@ -207,6 +208,13 @@ export function parseStartTicketsArgs(argv) {
|
|
|
207
208
|
// the parsed options); a `ModelTier` = usable override; INJECTED_TIER_UNRESOLVED
|
|
208
209
|
// = supplied-but-malformed (fail open to premium). NEVER an error/throw.
|
|
209
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;
|
|
210
218
|
const branchEntries = [];
|
|
211
219
|
const keys = [];
|
|
212
220
|
for (let i = 0; i < argv.length; i++) {
|
|
@@ -310,6 +318,39 @@ export function parseStartTicketsArgs(argv) {
|
|
|
310
318
|
injectedTier = isModelTier(value) ? value : INJECTED_TIER_UNRESOLVED;
|
|
311
319
|
continue;
|
|
312
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
|
+
}
|
|
313
354
|
if (arg === "--terminal" || arg.startsWith("--terminal=")) {
|
|
314
355
|
let value;
|
|
315
356
|
if (arg.startsWith("--terminal=")) {
|
|
@@ -404,6 +445,18 @@ export function parseStartTicketsArgs(argv) {
|
|
|
404
445
|
}
|
|
405
446
|
keys.push(arg);
|
|
406
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
|
+
}
|
|
407
460
|
// --- key validation ---
|
|
408
461
|
if (keys.length === 0) {
|
|
409
462
|
return {
|
|
@@ -508,6 +561,9 @@ export function parseStartTicketsArgs(argv) {
|
|
|
508
561
|
// supplied, so an omitted flag leaves the legacy option shape untouched
|
|
509
562
|
// (no own `injectedTier` key, not even `undefined`).
|
|
510
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 } : {}),
|
|
511
567
|
},
|
|
512
568
|
};
|
|
513
569
|
}
|
|
@@ -2203,7 +2259,7 @@ export function buildMcpProvisioningDeps(deps, bridgeApiBaseUrlOverride) {
|
|
|
2203
2259
|
// conductor's own on-disk build (absolute-build-path primary, npm-channel
|
|
2204
2260
|
// fallback) — never from process.cwd(). `nodeExecutable` is kept as "node"
|
|
2205
2261
|
// here: Ticket 3 (P2) can later replace this single field with
|
|
2206
|
-
//
|
|
2262
|
+
// EPIC_IMPLEMENTER_NODE_PATH without re-plumbing argv construction.
|
|
2207
2263
|
mcpServerInvocation: resolveMcpShimInvocationForRuntime({
|
|
2208
2264
|
moduleUrl: import.meta.url,
|
|
2209
2265
|
nodeExecutable: "node",
|
|
@@ -2323,6 +2379,9 @@ export function formatModelRoutingDiagnosticLine(d) {
|
|
|
2323
2379
|
return `model routing degraded: Codex model catalog failed validation. ${failOpen}`;
|
|
2324
2380
|
case "codex-entitlement-unverified":
|
|
2325
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).");
|
|
2326
2385
|
default:
|
|
2327
2386
|
return `model routing degraded: ${d.message} ${failOpen}`;
|
|
2328
2387
|
}
|
|
@@ -2568,6 +2627,77 @@ export async function fetchDifficultyModelRoutingConfig(access, includeOverrides
|
|
|
2568
2627
|
};
|
|
2569
2628
|
}
|
|
2570
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
|
+
}
|
|
2571
2701
|
/** One bounded authenticated fetch; reject partial, unsafe, or stale wire data. */
|
|
2572
2702
|
export async function fetchCodexModelCatalog(access) {
|
|
2573
2703
|
let body;
|
|
@@ -2816,6 +2946,67 @@ async function applyPremiumFallbackToEligibleRows(deps, rows, isEligible, agent,
|
|
|
2816
2946
|
const isCreatedRoutingEligible = (r) => r.status === "created" && !!r.path;
|
|
2817
2947
|
/** Dry-run path: rows have no worktree path yet but are eligible for a preview. */
|
|
2818
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
|
+
}
|
|
2819
3010
|
/**
|
|
2820
3011
|
* BAPI-642 — apply a single explicitly injected tier to every eligible row,
|
|
2821
3012
|
* bypassing the per-ticket difficulty/tier HTTP lookup. The tier is still mapped
|
|
@@ -2864,7 +3055,15 @@ async function applyInjectedTierRoutingToEligibleRows(deps, rows, isEligible, ag
|
|
|
2864
3055
|
* `--model`; Claude/Cursor retain their premium fallback. Non-eligible rows
|
|
2865
3056
|
* pass through untouched, and routing failures never abort worktree creation.
|
|
2866
3057
|
*/
|
|
2867
|
-
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)) {
|
|
2868
3067
|
const eligible = rows.filter(isEligible);
|
|
2869
3068
|
if (eligible.length === 0)
|
|
2870
3069
|
return rows;
|
|
@@ -2907,98 +3106,276 @@ async function resolveModelRoutingForEligible(deps, rows, options, agent, isElig
|
|
|
2907
3106
|
// difficulty/tier lookup — `fetchTicketModelTiersForRows` is NEVER called for
|
|
2908
3107
|
// any injected value. A recognized tier is applied to every eligible row; a
|
|
2909
3108
|
// malformed value (INJECTED_TIER_UNRESOLVED) fails open to the premium
|
|
2910
|
-
// fallback. When the flag is absent (`injected === undefined`) the
|
|
2911
|
-
// 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).
|
|
2912
3117
|
const injected = options.injectedTier;
|
|
2913
3118
|
if (injected === INJECTED_TIER_UNRESOLVED) {
|
|
2914
3119
|
const warning = "model routing: --tier was not one of cheap|basic|premium; defaulting to premium routing";
|
|
2915
3120
|
const apply = await makePremiumFallbackApplier(deps, agent, overrides, catalog, access.repoName);
|
|
2916
|
-
|
|
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;
|
|
2917
3126
|
}
|
|
2918
3127
|
if (injected) {
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
//
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
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`;
|
|
2935
3177
|
}
|
|
2936
|
-
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
|
|
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
|
+
}
|
|
3186
|
+
}
|
|
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",
|
|
2947
3274
|
repoName: access.repoName,
|
|
2948
3275
|
storeTarget: `bapi:${access.repoName}`,
|
|
2949
3276
|
ticketKey: row.key,
|
|
2950
|
-
message:
|
|
3277
|
+
message: `${row.key} resolved no tier (source=${source})`,
|
|
2951
3278
|
});
|
|
2952
|
-
|
|
2953
|
-
|
|
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));
|
|
2954
3284
|
continue;
|
|
2955
3285
|
}
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
|
|
2959
|
-
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
//
|
|
2968
|
-
//
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
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
|
+
});
|
|
2979
3329
|
continue;
|
|
2980
3330
|
}
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
|
|
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
|
+
});
|
|
2994
3360
|
}
|
|
2995
|
-
out
|
|
2996
|
-
|
|
2997
|
-
|
|
2998
|
-
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
|
|
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 };
|
|
3002
3379
|
});
|
|
3003
3380
|
}
|
|
3004
3381
|
return out;
|
|
@@ -3008,8 +3385,8 @@ async function resolveModelRoutingForEligible(deps, rows, options, agent, isElig
|
|
|
3008
3385
|
* entrypoint for the real spawn path. ALWAYS fail-open (see
|
|
3009
3386
|
* {@link resolveModelRoutingForEligible}).
|
|
3010
3387
|
*/
|
|
3011
|
-
export async function resolveModelRoutingForRows(deps, rows, options, agent) {
|
|
3012
|
-
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);
|
|
3013
3390
|
}
|
|
3014
3391
|
/**
|
|
3015
3392
|
* Dry-run variant: resolve routing for `dry-run` rows so `--dry-run` can preview
|
|
@@ -3019,8 +3396,8 @@ export async function resolveModelRoutingForRows(deps, rows, options, agent) {
|
|
|
3019
3396
|
* difficulty the backend may compute and cache it (one LLM call) — exactly what
|
|
3020
3397
|
* the subsequent real run would have done.
|
|
3021
3398
|
*/
|
|
3022
|
-
export async function resolveModelRoutingForDryRun(deps, rows, options, agent) {
|
|
3023
|
-
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);
|
|
3024
3401
|
}
|
|
3025
3402
|
/**
|
|
3026
3403
|
* Format one concise routing decision line per ticket:
|
|
@@ -3032,6 +3409,11 @@ export async function resolveModelRoutingForDryRun(deps, rows, options, agent) {
|
|
|
3032
3409
|
* `--tier`-driven implementation handoff. The ORDINARY implementation launch (a
|
|
3033
3410
|
* plain `--workflow implement` with no `--tier`) appends NO label, so its line
|
|
3034
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.
|
|
3035
3417
|
*/
|
|
3036
3418
|
export function formatModelRoutingLine(row, agent, context) {
|
|
3037
3419
|
const difficulty = typeof row.difficulty === "number" ? String(row.difficulty) : "?";
|
|
@@ -3043,6 +3425,8 @@ export function formatModelRoutingLine(row, agent, context) {
|
|
|
3043
3425
|
labels.push("phase=review");
|
|
3044
3426
|
if (row.modelRoutingSource === "injected")
|
|
3045
3427
|
labels.push("routing=injected");
|
|
3428
|
+
if (row.modelRoutingSource === "automatic_snapshot")
|
|
3429
|
+
labels.push("routing=auto-snapshot");
|
|
3046
3430
|
return labels.length > 0 ? `${base} ${labels.join(" ")}` : base;
|
|
3047
3431
|
}
|
|
3048
3432
|
/** Stable de-dup key for an invocation-level routing diagnostic. */
|
|
@@ -3412,7 +3796,7 @@ export async function orchestrateStartTickets(deps, options, overrides = {}) {
|
|
|
3412
3796
|
// BEFORE building the spawn command. Always fail-open: routing never aborts a
|
|
3413
3797
|
// spawn — at worst a ticket runs on the agent's default model.
|
|
3414
3798
|
const resolveRoutingFn = overrides.resolveModelRoutingForRows ?? resolveModelRoutingForRows;
|
|
3415
|
-
const routed = await resolveRoutingFn(deps, conductorReady, options, agent);
|
|
3799
|
+
const routed = await resolveRoutingFn(deps, conductorReady, options, agent, overrides.modelRoutingWarningLog);
|
|
3416
3800
|
// Per-ticket routing DECISION lines stay (one per spawnable ticket). The
|
|
3417
3801
|
// noisy per-row routing WARNING emission is intentionally gone (BAPI-377): the
|
|
3418
3802
|
// CLI now emits a single de-duplicated, actionable routing diagnostic per
|
|
@@ -3525,7 +3909,7 @@ export async function runStartTicketsCli(argv, overrides = {}) {
|
|
|
3525
3909
|
// per-ticket routing decision line.
|
|
3526
3910
|
const resolveDryRunRoutingFn = overrides.resolveModelRoutingForDryRun ?? resolveModelRoutingForDryRun;
|
|
3527
3911
|
const dryRunRows = buildDryRunResults(options.keys, options.branchOverrides);
|
|
3528
|
-
const routedDryRunRows = await resolveDryRunRoutingFn(deps, dryRunRows, options, agent);
|
|
3912
|
+
const routedDryRunRows = await resolveDryRunRoutingFn(deps, dryRunRows, options, agent, errorLog);
|
|
3529
3913
|
const routedByKey = new Map(routedDryRunRows.map((r) => [r.key, r]));
|
|
3530
3914
|
// Resolve the SAME worker MCP shim invocation the real provisioning would use
|
|
3531
3915
|
// so the dry-run preview matches the `.mcp.json` that would actually be written.
|