@bridge_gpt/mcp-server 0.2.55 → 0.2.57
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 +26 -10
- package/build/agent-capabilities/types.js +4 -0
- package/build/agent-registry.js +20 -3
- package/build/codex-bridge-profile.js +151 -0
- package/build/codex-home.js +7 -0
- package/build/codex-launch-scope.js +54 -0
- package/build/codex-skill-adapter.js +8 -5
- package/build/command-host.js +9 -0
- package/build/command-provisioning.js +35 -4
- package/build/commands.generated.js +5 -5
- package/build/doctor.js +9 -1
- package/build/epic-implementer-bin.js +3 -3
- package/build/index.js +121 -116
- package/build/init.js +2 -1
- package/build/install-bridge.js +69 -10
- package/build/mcp-host-config.js +4 -0
- package/build/mcp-host-targets.js +21 -11
- package/build/mcp-provisioning.js +1 -1
- package/build/mcp-registration-doctor.js +1 -1
- package/build/readme.generated.js +1 -1
- package/build/review-tickets.js +50 -35
- package/build/start-tickets.js +335 -58
- package/build/version.generated.js +3 -3
- package/package.json +1 -1
package/build/start-tickets.js
CHANGED
|
@@ -56,10 +56,12 @@
|
|
|
56
56
|
import { execFile } from "child_process";
|
|
57
57
|
import { readFile, writeFile, mkdir, mkdtemp, stat, lstat, symlink, unlink, copyFile, readdir, rm, } from "fs/promises";
|
|
58
58
|
import os from "node:os";
|
|
59
|
+
import { createHash } from "node:crypto";
|
|
59
60
|
import path from "path";
|
|
61
|
+
import { validateHttpBaseUrl } from "./base-url.js";
|
|
60
62
|
import { VERSION } from "./version.generated.js";
|
|
61
63
|
import { resolveBapiCredentials, getPrimaryCredentialStorePath, } from "./credential-store.js";
|
|
62
|
-
import { MCP_PACKAGE_NAME } from "./mcp-identity.js";
|
|
64
|
+
import { LEGACY_SERVER_NAMES, MCP_PACKAGE_NAME, MCP_SERVER_NAME } from "./mcp-identity.js";
|
|
63
65
|
// BAPI-941: the shared drift diagnostic formatter. The leaf drift module imports
|
|
64
66
|
// nothing from here, so this direction stays acyclic.
|
|
65
67
|
import { formatClaudeReviewWorkflowDriftDiagnostic } from "./claude-review-workflow-drift.js";
|
|
@@ -74,7 +76,10 @@ import { TICKET_KEY_PATTERN as SHARED_TICKET_KEY_PATTERN, TICKET_KEY_USAGE_SUMMA
|
|
|
74
76
|
// can never drift. `start-tickets.ts` imports VALUES from there; the prereqs
|
|
75
77
|
// module imports only TYPES back, so the runtime graph stays acyclic.
|
|
76
78
|
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";
|
|
77
|
-
import { DEFAULT_AGENT_NAME, resolveAgentSpec, isAgentName,
|
|
79
|
+
import { DEFAULT_AGENT_NAME, resolveAgentSpec, isAgentName, resolveModelAlias, isValidModelAlias, isModelTier, } from "./agent-registry.js";
|
|
80
|
+
import { agentForPlatform, getHostTarget, getWorktreeHostTargets, isRunnableHostTarget, isHostPlatformId } from "./mcp-host-targets.js";
|
|
81
|
+
import { resolveCodexLaunchScope } from "./codex-launch-scope.js";
|
|
82
|
+
import { tmpdir } from "node:os";
|
|
78
83
|
import { createStartTicketsConductorContext, provisionConductorHooksForRows, emitStartTicketsRunStarted, injectConductorEnvIntoShellCommand, buildSupervisorTabCommand, isSupervisorLaunchEnabled, supervisorSpawnKey, } from "./start-tickets-conductor.js";
|
|
79
84
|
import { transitionEpicDispatch, resolveConductorBridgeApiAccess, } from "./conductor/bridge-api-client.js";
|
|
80
85
|
import { PR_BASE_BRANCH_ENV_VAR, buildPrBaseContractLaunchInstruction, } from "./pr-base-contract.js";
|
|
@@ -125,9 +130,11 @@ export function getStartTicketsUsage() {
|
|
|
125
130
|
` npx -y ${MCP_PACKAGE_NAME} start-tickets [flags] KEY [KEY ...]`,
|
|
126
131
|
"",
|
|
127
132
|
"Flags:",
|
|
128
|
-
" --agent claude|cursor-agent Agent command to launch in each worktree (default: claude)",
|
|
129
|
-
" --
|
|
130
|
-
"
|
|
133
|
+
" --agent claude|cursor-agent|codex Agent command to launch in each worktree (overrides --host; default: claude)",
|
|
134
|
+
" --host claude-code|cursor|codex|copilot-vscode|windsurf Current host when --agent is omitted; Claude Code, Cursor, and Codex can launch sessions",
|
|
135
|
+
" Cursor with --auto launches headlessly after a bounded readiness check; without --auto it stays interactive for the review gate.",
|
|
136
|
+
" --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 Coarse model-routing override: bypasses the per-ticket difficulty/tier lookup and applies this tier to every ticket. 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.",
|
|
131
138
|
" --rounds 1|2 Review round count forwarded to the review phase; review-only, valid only with --workflow review-and-implement",
|
|
132
139
|
" --terminal terminal|iterm Override the macOS terminal app (default: auto-detect via $TERM_PROGRAM); honored on macOS only",
|
|
133
140
|
" --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",
|
|
@@ -163,6 +170,13 @@ export function getStartTicketsUsage() {
|
|
|
163
170
|
TICKET_KEY_USAGE_SUMMARY,
|
|
164
171
|
].join("\n");
|
|
165
172
|
}
|
|
173
|
+
/** Agent names backed by a runnable, worktree-scoped host registration. */
|
|
174
|
+
function formatStartTicketsAgentNames() {
|
|
175
|
+
return getWorktreeHostTargets()
|
|
176
|
+
.filter(isRunnableHostTarget)
|
|
177
|
+
.map((target) => target.launchAgent)
|
|
178
|
+
.join(", ");
|
|
179
|
+
}
|
|
166
180
|
/**
|
|
167
181
|
* Parse argv strictly and without an arg-parser dependency. Returns a
|
|
168
182
|
* discriminated result: help, a validation error, or validated options.
|
|
@@ -177,7 +191,8 @@ export function parseStartTicketsArgs(argv) {
|
|
|
177
191
|
let autoApprove = false;
|
|
178
192
|
let refreshMain = true;
|
|
179
193
|
let maxParallelRaw;
|
|
180
|
-
let agentName
|
|
194
|
+
let agentName;
|
|
195
|
+
let hostPlatform;
|
|
181
196
|
let baseBranch = "main";
|
|
182
197
|
let conductorEnabled = false;
|
|
183
198
|
// BAPI-948: `--guard-stale-branch` (and `--conductor`, which implies it) opt the
|
|
@@ -217,12 +232,20 @@ export function parseStartTicketsArgs(argv) {
|
|
|
217
232
|
if (!isAgentName(value)) {
|
|
218
233
|
return {
|
|
219
234
|
status: "error",
|
|
220
|
-
message: `Invalid --agent value: '${value}' (
|
|
235
|
+
message: `Invalid --agent value: '${value}' (launchable agents: ${formatStartTicketsAgentNames()}).`,
|
|
221
236
|
};
|
|
222
237
|
}
|
|
223
238
|
agentName = value;
|
|
224
239
|
continue;
|
|
225
240
|
}
|
|
241
|
+
if (arg === "--host" || arg.startsWith("--host=")) {
|
|
242
|
+
const value = arg.startsWith("--host=") ? arg.slice("--host=".length) : takeValue();
|
|
243
|
+
if (!value || !isHostPlatformId(value)) {
|
|
244
|
+
return { status: "error", message: `Invalid --host value: '${value ?? ""}'.` };
|
|
245
|
+
}
|
|
246
|
+
hostPlatform = value;
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
226
249
|
if (arg === "--workflow" || arg.startsWith("--workflow=")) {
|
|
227
250
|
let value;
|
|
228
251
|
if (arg.startsWith("--workflow=")) {
|
|
@@ -452,6 +475,14 @@ export function parseStartTicketsArgs(argv) {
|
|
|
452
475
|
}
|
|
453
476
|
reviewRounds = reviewRoundsRaw === "1" ? 1 : 2;
|
|
454
477
|
}
|
|
478
|
+
const target = hostPlatform ? getHostTarget(hostPlatform) : undefined;
|
|
479
|
+
if (!agentName && target && !isRunnableHostTarget(target)) {
|
|
480
|
+
return {
|
|
481
|
+
status: "error",
|
|
482
|
+
message: `${target.label} supports MCP configuration but cannot spawn a Bridge session. Continue the workflow manually or pass an explicit --agent override.`,
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
const resolvedAgentName = agentName ?? (hostPlatform ? agentForPlatform(hostPlatform) : DEFAULT_AGENT_NAME);
|
|
455
486
|
return {
|
|
456
487
|
status: "ok",
|
|
457
488
|
options: {
|
|
@@ -462,7 +493,7 @@ export function parseStartTicketsArgs(argv) {
|
|
|
462
493
|
refreshMain,
|
|
463
494
|
maxParallel,
|
|
464
495
|
branchOverrides,
|
|
465
|
-
agentName,
|
|
496
|
+
agentName: resolvedAgentName,
|
|
466
497
|
baseBranch,
|
|
467
498
|
conductorEnabled,
|
|
468
499
|
workflow,
|
|
@@ -530,7 +561,7 @@ resumeMode = false, workflow = "implement", reviewRounds,
|
|
|
530
561
|
// conductor runs always, and (BAPI-801/BAPI-847) for every plain run whose base
|
|
531
562
|
// resolved, `main` included — and threaded (BAPI-593) into the spawned workflow
|
|
532
563
|
// command.
|
|
533
|
-
baseBranch) {
|
|
564
|
+
baseBranch, bridgeApiBaseUrlOverride) {
|
|
534
565
|
if (!isSupportedStartTicketsPlatform(deps.platform)) {
|
|
535
566
|
return { ok: false, error: unsupportedPlatformMessage(deps.platform) };
|
|
536
567
|
}
|
|
@@ -541,6 +572,7 @@ baseBranch) {
|
|
|
541
572
|
// conductor-only — a plain worker's prompt stays bare and learns the base from
|
|
542
573
|
// the env plus the create-PR instruction files.
|
|
543
574
|
const prBaseBranch = resolvePrBaseBranchEnvValue(conductorEnabled, baseBranch);
|
|
575
|
+
const bridgeApiBaseUrl = bridgeApiBaseUrlOverride ?? resolveSpawnBridgeApiBaseUrl(deps.env);
|
|
544
576
|
return {
|
|
545
577
|
ok: true,
|
|
546
578
|
config: {
|
|
@@ -550,7 +582,7 @@ baseBranch) {
|
|
|
550
582
|
// falls back to the basename-derived repo name (the 403 root cause), and
|
|
551
583
|
// (BAPI-586/BAPI-801) the run base so the spawned worker opens its PR
|
|
552
584
|
// against it. The base assignment stays outermost.
|
|
553
|
-
buildAgentShellCommand: (key, worktreePath, modelAlias) => prependBaseBranchEnvAssignment(prependRepoNameEnvAssignment(buildAgentShellCommand(agent, key, worktreePath, platform, autoApprove, modelAlias, conductorEnabled, resumeMode, workflow, reviewRounds, baseBranch), repoName, platform), prBaseBranch, platform),
|
|
585
|
+
buildAgentShellCommand: (key, worktreePath, modelAlias, launchArgs) => prependBaseBranchEnvAssignment(prependRepoNameEnvAssignment(buildAgentShellCommand(agent, key, worktreePath, platform, autoApprove, modelAlias, conductorEnabled, resumeMode, workflow, reviewRounds, baseBranch, launchArgs, bridgeApiBaseUrl), repoName, platform), prBaseBranch, platform),
|
|
554
586
|
spawnTerminalTab: deps.spawnTerminalTab,
|
|
555
587
|
},
|
|
556
588
|
};
|
|
@@ -580,6 +612,51 @@ export function prependRepoNameEnvAssignment(command, repoName, platform = "darw
|
|
|
580
612
|
}
|
|
581
613
|
return `export BAPI_REPO_NAME='${shSquoteInner(repoName)}' && ${command}`;
|
|
582
614
|
}
|
|
615
|
+
/** Carry the parent's non-secret Bridge endpoint into a newly opened terminal. */
|
|
616
|
+
export function prependBridgeApiBaseUrlEnvAssignment(command, baseUrl, platform = "darwin") {
|
|
617
|
+
if (!baseUrl)
|
|
618
|
+
return command;
|
|
619
|
+
if (platform === "win32") {
|
|
620
|
+
return `$env:BAPI_BASE_URL = ${powershellSquote(baseUrl)}; ${command}`;
|
|
621
|
+
}
|
|
622
|
+
return `export BAPI_BASE_URL='${shSquoteInner(baseUrl)}' && ${command}`;
|
|
623
|
+
}
|
|
624
|
+
/** Validate before placing an endpoint in a launch script or MCP registration. */
|
|
625
|
+
export function resolveSpawnBridgeApiBaseUrl(env) {
|
|
626
|
+
const raw = env.BAPI_BASE_URL;
|
|
627
|
+
if (!raw || !raw.trim())
|
|
628
|
+
return undefined;
|
|
629
|
+
const result = validateHttpBaseUrl(raw);
|
|
630
|
+
return result.ok ? result.baseUrl : undefined;
|
|
631
|
+
}
|
|
632
|
+
/**
|
|
633
|
+
* A shell-spawned CLI cannot see the parent MCP process's environment. Read only
|
|
634
|
+
* the non-secret endpoint from the selected host's project registration when the
|
|
635
|
+
* shell did not inherit BAPI_BASE_URL (common for Cursor launched from the GUI).
|
|
636
|
+
*/
|
|
637
|
+
export async function resolveStartTicketsBridgeApiBaseUrl(deps, agent) {
|
|
638
|
+
const inherited = resolveSpawnBridgeApiBaseUrl(deps.env);
|
|
639
|
+
if (inherited)
|
|
640
|
+
return inherited;
|
|
641
|
+
const configPath = agent.name === "cursor-agent"
|
|
642
|
+
? path.join(deps.cwd, ".cursor", "mcp.json")
|
|
643
|
+
: path.join(deps.cwd, ".mcp.json");
|
|
644
|
+
try {
|
|
645
|
+
const raw = await (deps.readFile ?? ((filePath) => readFile(filePath, "utf-8")))(configPath);
|
|
646
|
+
const parsed = JSON.parse(raw);
|
|
647
|
+
const bridge = parsed.mcpServers?.[MCP_SERVER_NAME] ?? parsed.mcpServers?.[LEGACY_SERVER_NAMES[0]];
|
|
648
|
+
if (!bridge)
|
|
649
|
+
return undefined;
|
|
650
|
+
const args = Array.isArray(bridge.args) ? bridge.args : [];
|
|
651
|
+
const flagIndex = args.indexOf("--base-url");
|
|
652
|
+
const candidate = bridge.env?.BAPI_BASE_URL ?? (flagIndex >= 0 ? args[flagIndex + 1] : undefined);
|
|
653
|
+
const validated = validateHttpBaseUrl(candidate);
|
|
654
|
+
return validated.ok ? validated.baseUrl : undefined;
|
|
655
|
+
}
|
|
656
|
+
catch {
|
|
657
|
+
return undefined;
|
|
658
|
+
}
|
|
659
|
+
}
|
|
583
660
|
/**
|
|
584
661
|
* BAPI-801/BAPI-847: decide the `BAPI_BASE_BRANCH` value a spawned worker's shell
|
|
585
662
|
* should carry — the single rule shared by the real spawn builder
|
|
@@ -775,6 +852,11 @@ export async function runPreflight(deps, options, warn = (message) => console.wa
|
|
|
775
852
|
const enforceLiveSourceGuard = options.nonMutatingBase === true || options.epic !== undefined;
|
|
776
853
|
const result = await enforcePreflightPrerequisites(deps, { enforceLiveSourceGuard });
|
|
777
854
|
if (result.ok) {
|
|
855
|
+
if (options.agentName === "cursor-agent" && options.autoApprove) {
|
|
856
|
+
const cursorReady = await preflightHeadlessCursor(deps);
|
|
857
|
+
if (!cursorReady.ok)
|
|
858
|
+
return cursorReady;
|
|
859
|
+
}
|
|
778
860
|
// Overridden collision: not silent — surface the warning through the channel.
|
|
779
861
|
if (result.warning)
|
|
780
862
|
warn(result.warning);
|
|
@@ -786,6 +868,52 @@ export async function runPreflight(deps, options, warn = (message) => console.wa
|
|
|
786
868
|
// Both missing-prerequisite and live-source-collision are doctor-diagnosable.
|
|
787
869
|
return { ok: false, error: appendDoctorHint(result.error) };
|
|
788
870
|
}
|
|
871
|
+
/** Validate the actual headless path before creating any ticket worktree. */
|
|
872
|
+
async function preflightHeadlessCursor(deps) {
|
|
873
|
+
let help;
|
|
874
|
+
try {
|
|
875
|
+
help = await deps.runCommand("cursor-agent", ["--help"], { timeoutMs: 5000 });
|
|
876
|
+
}
|
|
877
|
+
catch {
|
|
878
|
+
return { ok: false, error: "Cursor Agent is unavailable. Install cursor-agent and ensure it is on PATH." };
|
|
879
|
+
}
|
|
880
|
+
if (help.exitCode !== 0) {
|
|
881
|
+
return { ok: false, error: "Cursor Agent is unavailable. Install cursor-agent and ensure it is on PATH." };
|
|
882
|
+
}
|
|
883
|
+
for (const flag of ["--print", "--output-format", "--trust", "--workspace", "--force", "--approve-mcps"]) {
|
|
884
|
+
if (!help.stdout.includes(flag)) {
|
|
885
|
+
return { ok: false, error: `Cursor Agent does not support ${flag}. Update cursor-agent before using --auto.` };
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
let probeDir;
|
|
889
|
+
try {
|
|
890
|
+
probeDir = await mkdtemp(path.join(tmpdir(), "bridge-cursor-ready-"));
|
|
891
|
+
}
|
|
892
|
+
catch {
|
|
893
|
+
return { ok: false, error: "Could not create a temporary workspace for the Cursor Agent readiness check." };
|
|
894
|
+
}
|
|
895
|
+
try {
|
|
896
|
+
const probe = await deps.runCommand("cursor-agent", [
|
|
897
|
+
"-p", "--output-format", "text", "--trust", "--workspace", probeDir,
|
|
898
|
+
"Reply with exactly BAPI_CURSOR_READY. Do not use tools.",
|
|
899
|
+
], { cwd: probeDir, timeoutMs: 60000 });
|
|
900
|
+
if (probe.exitCode !== 0 || !probe.stdout.includes("BAPI_CURSOR_READY")) {
|
|
901
|
+
return { ok: false, error: "Cursor Agent could not complete a headless authentication check. Run cursor-agent login or configure CURSOR_API_KEY, then retry." };
|
|
902
|
+
}
|
|
903
|
+
return { ok: true };
|
|
904
|
+
}
|
|
905
|
+
catch {
|
|
906
|
+
return { ok: false, error: "Cursor Agent headless readiness check failed. Verify authentication and retry." };
|
|
907
|
+
}
|
|
908
|
+
finally {
|
|
909
|
+
try {
|
|
910
|
+
await rm(probeDir, { recursive: true, force: true });
|
|
911
|
+
}
|
|
912
|
+
catch {
|
|
913
|
+
// Cleanup failure must not turn a bounded readiness result into a raw error.
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
}
|
|
789
917
|
// ---------------------------------------------------------------------------
|
|
790
918
|
// main refresh + worktree parsing
|
|
791
919
|
// ---------------------------------------------------------------------------
|
|
@@ -1192,17 +1320,23 @@ export function buildAgentPrompt(key, opts = {}) {
|
|
|
1192
1320
|
// injected as a `--model` flag (see buildAgentInvocationArgv), never embedded
|
|
1193
1321
|
// in the prompt text.
|
|
1194
1322
|
const workflow = opts.workflow ?? "implement";
|
|
1195
|
-
const
|
|
1323
|
+
const commandName = workflow === "review-and-implement" ? "review-and-implement" : "implement-ticket";
|
|
1324
|
+
// Codex skills use explicit invocation names; Claude and Cursor use commands.
|
|
1325
|
+
const head = opts.agentName === "codex" ? `$bridge-${commandName}` : `/${commandName}`;
|
|
1196
1326
|
let command = `${head} ${key}${opts.autoApprove ? " --auto" : ""}`;
|
|
1197
1327
|
// Review-only arguments (`--rounds`, non-default `--base-branch`) must never
|
|
1198
1328
|
// leak into the legacy `/implement-ticket` prompt.
|
|
1199
1329
|
if (workflow === "review-and-implement") {
|
|
1330
|
+
if (opts.agentName === "cursor-agent")
|
|
1331
|
+
command += " --agent=cursor-agent";
|
|
1200
1332
|
if (opts.reviewRounds !== undefined) {
|
|
1201
1333
|
command += ` --rounds=${opts.reviewRounds}`;
|
|
1202
1334
|
}
|
|
1203
1335
|
if (opts.baseBranch !== undefined && opts.baseBranch !== "main") {
|
|
1204
1336
|
command += ` --base-branch='${shSquoteInner(opts.baseBranch)}'`;
|
|
1205
1337
|
}
|
|
1338
|
+
if (opts.agentName === "codex")
|
|
1339
|
+
command += " --agent codex --host codex";
|
|
1206
1340
|
}
|
|
1207
1341
|
const parts = [command];
|
|
1208
1342
|
if (opts.conductorEnabled) {
|
|
@@ -1224,8 +1358,13 @@ export function buildAgentPrompt(key, opts = {}) {
|
|
|
1224
1358
|
* otherwise they are omitted entirely (fail-open). The prompt is always the
|
|
1225
1359
|
* final argument. Pure and never throws on an invalid alias.
|
|
1226
1360
|
*/
|
|
1227
|
-
export function buildAgentInvocationArgv(agent, prompt, modelAlias) {
|
|
1361
|
+
export function buildAgentInvocationArgv(agent, prompt, modelAlias, launchArgsOrCursorWorkspace = []) {
|
|
1228
1362
|
const argv = [agent.command];
|
|
1363
|
+
if (agent.name === "codex" && Array.isArray(launchArgsOrCursorWorkspace))
|
|
1364
|
+
argv.push(...launchArgsOrCursorWorkspace);
|
|
1365
|
+
if (agent.name === "cursor-agent" && typeof launchArgsOrCursorWorkspace === "string" && launchArgsOrCursorWorkspace) {
|
|
1366
|
+
argv.push("-p", "--output-format", "text", "--trust", "--approve-mcps", "--force", "--workspace", launchArgsOrCursorWorkspace);
|
|
1367
|
+
}
|
|
1229
1368
|
if (agent.supportsModelOverride &&
|
|
1230
1369
|
typeof modelAlias === "string" &&
|
|
1231
1370
|
isValidModelAlias(modelAlias)) {
|
|
@@ -1241,10 +1380,10 @@ export function buildAgentInvocationArgv(agent, prompt, modelAlias) {
|
|
|
1241
1380
|
* `--model <alias>` pair and the prompt — is run through the platform-correct
|
|
1242
1381
|
* `quote`.
|
|
1243
1382
|
*/
|
|
1244
|
-
export function buildAgentInvocation(agent, prompt, quote, modelAlias) {
|
|
1383
|
+
export function buildAgentInvocation(agent, prompt, quote, modelAlias, launchArgsOrCursorWorkspace = []) {
|
|
1245
1384
|
switch (agent.promptArgStyle) {
|
|
1246
1385
|
case "positional": {
|
|
1247
|
-
const [command, ...rest] = buildAgentInvocationArgv(agent, prompt, modelAlias);
|
|
1386
|
+
const [command, ...rest] = buildAgentInvocationArgv(agent, prompt, modelAlias, launchArgsOrCursorWorkspace);
|
|
1248
1387
|
const quotedRest = rest.map(quote);
|
|
1249
1388
|
return [command, ...quotedRest].join(" ");
|
|
1250
1389
|
}
|
|
@@ -1255,14 +1394,14 @@ export function buildAgentInvocation(agent, prompt, quote, modelAlias) {
|
|
|
1255
1394
|
}
|
|
1256
1395
|
}
|
|
1257
1396
|
/** POSIX agent shell command: `cd '<path>' && <agent> [--model '<alias>'] '<prompt>'`. */
|
|
1258
|
-
export function buildPosixAgentShellCommand(agent, key, worktreePath, autoApprove = false, modelAlias, conductorEnabled = false, resumeMode = false, workflow = "implement", reviewRounds, baseBranch) {
|
|
1259
|
-
const invocation = buildAgentInvocation(agent, buildAgentPrompt(key, { autoApprove, conductorEnabled, resumeMode, workflow, reviewRounds, baseBranch }), (p) => `'${shSquoteInner(p)}'`, modelAlias);
|
|
1260
|
-
return `cd '${shSquoteInner(worktreePath)}' && ${invocation}`;
|
|
1397
|
+
export function buildPosixAgentShellCommand(agent, key, worktreePath, autoApprove = false, modelAlias, conductorEnabled = false, resumeMode = false, workflow = "implement", reviewRounds, baseBranch, launchArgs = [], bridgeApiBaseUrl) {
|
|
1398
|
+
const invocation = buildAgentInvocation(agent, buildAgentPrompt(key, { autoApprove, conductorEnabled, resumeMode, workflow, reviewRounds, baseBranch, agentName: agent.name }), (p) => `'${shSquoteInner(p)}'`, modelAlias, agent.name === "cursor-agent" && autoApprove ? worktreePath : launchArgs);
|
|
1399
|
+
return `cd '${shSquoteInner(worktreePath)}' && ${prependBridgeApiBaseUrlEnvAssignment(invocation, bridgeApiBaseUrl, "darwin")}`;
|
|
1261
1400
|
}
|
|
1262
1401
|
/** PowerShell agent shell command: `Set-Location -LiteralPath '<path>'; <agent> [--model '<alias>'] '<prompt>'`. */
|
|
1263
|
-
export function buildPowerShellAgentShellCommand(agent, key, worktreePath, autoApprove = false, modelAlias, conductorEnabled = false, resumeMode = false, workflow = "implement", reviewRounds, baseBranch) {
|
|
1264
|
-
const invocation = buildAgentInvocation(agent, buildAgentPrompt(key, { autoApprove, conductorEnabled, resumeMode, workflow, reviewRounds, baseBranch }), powershellSquote, modelAlias);
|
|
1265
|
-
return `Set-Location -LiteralPath ${powershellSquote(worktreePath)}; ${invocation}`;
|
|
1402
|
+
export function buildPowerShellAgentShellCommand(agent, key, worktreePath, autoApprove = false, modelAlias, conductorEnabled = false, resumeMode = false, workflow = "implement", reviewRounds, baseBranch, launchArgs = [], bridgeApiBaseUrl) {
|
|
1403
|
+
const invocation = buildAgentInvocation(agent, buildAgentPrompt(key, { autoApprove, conductorEnabled, resumeMode, workflow, reviewRounds, baseBranch, agentName: agent.name }), powershellSquote, modelAlias, agent.name === "cursor-agent" && autoApprove ? worktreePath : launchArgs);
|
|
1404
|
+
return `Set-Location -LiteralPath ${powershellSquote(worktreePath)}; ${prependBridgeApiBaseUrlEnvAssignment(invocation, bridgeApiBaseUrl, "win32")}`;
|
|
1266
1405
|
}
|
|
1267
1406
|
/**
|
|
1268
1407
|
* Build the shell command run inside each spawned tab/session, dispatched by
|
|
@@ -1272,10 +1411,10 @@ export function buildPowerShellAgentShellCommand(agent, key, worktreePath, autoA
|
|
|
1272
1411
|
* injected as `--model` at the spawn boundary. `conductorEnabled` appends the
|
|
1273
1412
|
* BAPI-586 PR-base contract instruction to the prompt (opt-in via `--conductor`).
|
|
1274
1413
|
*/
|
|
1275
|
-
export function buildAgentShellCommand(agent, key, worktreePath, platform = "darwin", autoApprove = false, modelAlias, conductorEnabled = false, resumeMode = false, workflow = "implement", reviewRounds, baseBranch) {
|
|
1414
|
+
export function buildAgentShellCommand(agent, key, worktreePath, platform = "darwin", autoApprove = false, modelAlias, conductorEnabled = false, resumeMode = false, workflow = "implement", reviewRounds, baseBranch, launchArgs = [], bridgeApiBaseUrl) {
|
|
1276
1415
|
if (platform === "win32")
|
|
1277
|
-
return buildPowerShellAgentShellCommand(agent, key, worktreePath, autoApprove, modelAlias, conductorEnabled, resumeMode, workflow, reviewRounds, baseBranch);
|
|
1278
|
-
return buildPosixAgentShellCommand(agent, key, worktreePath, autoApprove, modelAlias, conductorEnabled, resumeMode, workflow, reviewRounds, baseBranch);
|
|
1416
|
+
return buildPowerShellAgentShellCommand(agent, key, worktreePath, autoApprove, modelAlias, conductorEnabled, resumeMode, workflow, reviewRounds, baseBranch, launchArgs, bridgeApiBaseUrl);
|
|
1417
|
+
return buildPosixAgentShellCommand(agent, key, worktreePath, autoApprove, modelAlias, conductorEnabled, resumeMode, workflow, reviewRounds, baseBranch, launchArgs, bridgeApiBaseUrl);
|
|
1279
1418
|
}
|
|
1280
1419
|
/**
|
|
1281
1420
|
* Build the shell command run inside a spawned tab/session for an ARBITRARY
|
|
@@ -1857,7 +1996,7 @@ export async function spawnTabsForCreatedWorktrees(deps, rows, terminal, buildSh
|
|
|
1857
1996
|
out.push(row);
|
|
1858
1997
|
continue;
|
|
1859
1998
|
}
|
|
1860
|
-
const baseShellCommand = buildShellCommand(row.key, row.path, row.modelAlias ?? null);
|
|
1999
|
+
const baseShellCommand = buildShellCommand(row.key, row.path, row.modelAlias ?? null, row.codexLaunchArgs);
|
|
1861
2000
|
// Scope the row's conductor identity env to this terminal/tab/session only
|
|
1862
2001
|
// (BAPI-394). No-op when the row carries no conductorEnv (dry-run, non-Claude
|
|
1863
2002
|
// agents, or conductor disabled). Never mutates process/global env.
|
|
@@ -1912,9 +2051,11 @@ export function getDryRunPlatformDetails(agent, platform = process.platform, env
|
|
|
1912
2051
|
// and (BAPI-801/BAPI-847) the PR base as a BAPI_BASE_BRANCH prefix through the
|
|
1913
2052
|
// SAME resolvePrBaseBranchEnvValue rule — including the ordinary `main` case —
|
|
1914
2053
|
// and the same outermost ordering the real spawn builder uses; dry-run shares
|
|
1915
|
-
// both environment renderings with real spawning. Reuses the same
|
|
1916
|
-
//
|
|
1917
|
-
|
|
2054
|
+
// both environment renderings with real spawning. Reuses the same
|
|
2055
|
+
// buildAgentShellCommand/buildAgentPrompt path as a real spawn. A Codex
|
|
2056
|
+
// dry-run has no created worktree to bind, so its preview omits the
|
|
2057
|
+
// process-local -C/-c MCP scope that the real spawn resolves later.
|
|
2058
|
+
buildAgentShellCommand: (key, worktreePath, modelAlias, launchArgs) => prependBaseBranchEnvAssignment(prependRepoNameEnvAssignment(buildAgentShellCommand(agent, key, worktreePath, platform, autoApprove, modelAlias, conductorEnabled, false, workflow, reviewRounds, baseBranch, launchArgs, resolveSpawnBridgeApiBaseUrl(env)), repoName, platform), resolvePrBaseBranchEnvValue(conductorEnabled, baseBranch), platform),
|
|
1918
2059
|
};
|
|
1919
2060
|
}
|
|
1920
2061
|
/**
|
|
@@ -1923,7 +2064,7 @@ export function getDryRunPlatformDetails(agent, platform = process.platform, env
|
|
|
1923
2064
|
* the `bapi` target. Lists both registration files and the version-pinned shim
|
|
1924
2065
|
* command. Pure formatting — implies no credentials and writes no files.
|
|
1925
2066
|
*/
|
|
1926
|
-
export function buildDryRunMcpProvisioningLines(worktreePath, platform = process.platform, mcpServerInvocation) {
|
|
2067
|
+
export function buildDryRunMcpProvisioningLines(worktreePath, platform = process.platform, mcpServerInvocation, bridgeApiBaseUrl) {
|
|
1927
2068
|
const api = platform === "win32" ? path.win32 : path.posix;
|
|
1928
2069
|
const mcpJson = api.join(worktreePath, ".mcp.json");
|
|
1929
2070
|
const cursorJson = api.join(worktreePath, ".cursor", "mcp.json");
|
|
@@ -1944,6 +2085,7 @@ export function buildDryRunMcpProvisioningLines(worktreePath, platform = process
|
|
|
1944
2085
|
`DRY-RUN: ${mcpJson}`,
|
|
1945
2086
|
`DRY-RUN: ${cursorJson}`,
|
|
1946
2087
|
`DRY-RUN: ${shim}`,
|
|
2088
|
+
...(bridgeApiBaseUrl ? [`DRY-RUN: bapi shim adds --base-url ${bridgeApiBaseUrl}`] : []),
|
|
1947
2089
|
];
|
|
1948
2090
|
}
|
|
1949
2091
|
/**
|
|
@@ -1968,7 +2110,7 @@ export function buildDryRunDetailLines(agent, key, branch, platform = process.pl
|
|
|
1968
2110
|
`DRY-RUN: ${worktrunkBinary} ${wtArgs.join(" ")}`,
|
|
1969
2111
|
`DRY-RUN: ${agentInvocation}`,
|
|
1970
2112
|
...(prBaseBranch ? [`DRY-RUN: PR base: ${prBaseBranch}`] : []),
|
|
1971
|
-
...buildDryRunMcpProvisioningLines("<worktree-path>", platform, mcpServerInvocation),
|
|
2113
|
+
...buildDryRunMcpProvisioningLines("<worktree-path>", platform, mcpServerInvocation, resolveSpawnBridgeApiBaseUrl(env)),
|
|
1972
2114
|
];
|
|
1973
2115
|
}
|
|
1974
2116
|
/**
|
|
@@ -2042,13 +2184,15 @@ export function formatSummaryReport(rows) {
|
|
|
2042
2184
|
* Provisioning needs real `fs/promises` ops (not part of the command-runner DI
|
|
2043
2185
|
* boundary), but inherits the platform and cwd from the orchestration deps.
|
|
2044
2186
|
*/
|
|
2045
|
-
export function buildMcpProvisioningDeps(deps) {
|
|
2187
|
+
export function buildMcpProvisioningDeps(deps, bridgeApiBaseUrlOverride) {
|
|
2046
2188
|
return {
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2189
|
+
env: deps.env,
|
|
2190
|
+
readFile: deps.readFile ?? ((filePath) => readFile(filePath, "utf-8")),
|
|
2191
|
+
writeFile: deps.writeFile ?? ((filePath, data) => writeFile(filePath, data, "utf-8")),
|
|
2192
|
+
mkdir: deps.mkdir ?? ((dirPath, options) => mkdir(dirPath, options)),
|
|
2050
2193
|
platform: deps.platform,
|
|
2051
2194
|
cwd: deps.cwd,
|
|
2195
|
+
bridgeApiBaseUrl: bridgeApiBaseUrlOverride ?? resolveSpawnBridgeApiBaseUrl(deps.env),
|
|
2052
2196
|
// Resolve how the worker MCP shim launches at dispatch time from the running
|
|
2053
2197
|
// conductor's own on-disk build (absolute-build-path primary, npm-channel
|
|
2054
2198
|
// fallback) — never from process.cwd(). `nodeExecutable` is kept as "node"
|
|
@@ -2127,7 +2271,9 @@ export function formatModelRoutingDiagnosticLine(d) {
|
|
|
2127
2271
|
const repo = d.repoName ?? "<unknown>";
|
|
2128
2272
|
const target = d.storeTarget ?? (d.repoName ? `bapi:${d.repoName}` : "bapi:<repo>");
|
|
2129
2273
|
const storePath = d.storePath ?? "~/.config/bridge/credentials.json";
|
|
2130
|
-
const failOpen =
|
|
2274
|
+
const failOpen = d.agentName === "codex"
|
|
2275
|
+
? "Codex continues with its configured default model."
|
|
2276
|
+
: "Assuming hard ticket; defaulting to premium/Opus when available.";
|
|
2131
2277
|
const ticketSuffix = d.ticketKey ? ` for ${d.ticketKey}` : "";
|
|
2132
2278
|
const statusSuffix = typeof d.status === "number" ? ` (HTTP ${d.status})` : "";
|
|
2133
2279
|
switch (d.kind) {
|
|
@@ -2165,6 +2311,12 @@ export function formatModelRoutingDiagnosticLine(d) {
|
|
|
2165
2311
|
case "no-tier":
|
|
2166
2312
|
return (`model routing: Bridge API returned no usable tier${ticketSuffix}. Ticket spawning ` +
|
|
2167
2313
|
`continues fail-open. ${failOpen}`);
|
|
2314
|
+
case "catalog-unavailable":
|
|
2315
|
+
return `model routing degraded: Codex model catalog unavailable. ${failOpen}`;
|
|
2316
|
+
case "catalog-invalid":
|
|
2317
|
+
return `model routing degraded: Codex model catalog failed validation. ${failOpen}`;
|
|
2318
|
+
case "codex-entitlement-unverified":
|
|
2319
|
+
return "model routing: Bridge permits the selected model; local Codex account entitlement cannot be verified non-interactively.";
|
|
2168
2320
|
default:
|
|
2169
2321
|
return `model routing degraded: ${d.message} ${failOpen}`;
|
|
2170
2322
|
}
|
|
@@ -2176,6 +2328,7 @@ export function formatModelRoutingDiagnosticLine(d) {
|
|
|
2176
2328
|
export const START_TICKETS_DEFAULT_BASE_URL = "https://bridgegpt-api.com";
|
|
2177
2329
|
const TICKET_MODEL_TIER_FETCH_TIMEOUT_MS = 75_000;
|
|
2178
2330
|
const CONFIG_FIELD_FETCH_TIMEOUT_MS = 30_000;
|
|
2331
|
+
const CODEX_CATALOG_FETCH_TIMEOUT_MS = 15_000;
|
|
2179
2332
|
const CURSOR_MODEL_LIST_TIMEOUT_MS = 15_000;
|
|
2180
2333
|
/** GET auth headers for the CLI's Bridge API calls. */
|
|
2181
2334
|
function startTicketsGetHeaders(access) {
|
|
@@ -2388,10 +2541,12 @@ export async function fetchStartTicketsConfigField(access, fieldName) {
|
|
|
2388
2541
|
* Any fetch failure returns a structured failure (with a classified diagnostic)
|
|
2389
2542
|
* so the caller fails open to premium/Opus.
|
|
2390
2543
|
*/
|
|
2391
|
-
export async function fetchDifficultyModelRoutingConfig(access) {
|
|
2544
|
+
export async function fetchDifficultyModelRoutingConfig(access, includeOverrides = true) {
|
|
2392
2545
|
try {
|
|
2393
2546
|
const enabledValue = await fetchStartTicketsConfigField(access, "difficulty_model_routing_enabled");
|
|
2394
|
-
const overridesValue =
|
|
2547
|
+
const overridesValue = includeOverrides
|
|
2548
|
+
? await fetchStartTicketsConfigField(access, "difficulty_model_tier_overrides")
|
|
2549
|
+
: undefined;
|
|
2395
2550
|
const enabled = enabledValue === false ? false : true;
|
|
2396
2551
|
return { ok: true, config: { enabled, overrides: normalizeDifficultyModelTierOverrides(overridesValue) } };
|
|
2397
2552
|
}
|
|
@@ -2407,6 +2562,57 @@ export async function fetchDifficultyModelRoutingConfig(access) {
|
|
|
2407
2562
|
};
|
|
2408
2563
|
}
|
|
2409
2564
|
}
|
|
2565
|
+
/** One bounded authenticated fetch; reject partial, unsafe, or stale wire data. */
|
|
2566
|
+
export async function fetchCodexModelCatalog(access) {
|
|
2567
|
+
let body;
|
|
2568
|
+
try {
|
|
2569
|
+
const url = buildStartTicketsJiraUrl(access.baseUrl, "/codex-model-catalog", {
|
|
2570
|
+
repo_name: access.repoName,
|
|
2571
|
+
});
|
|
2572
|
+
body = await fetchJsonWithTimeout(url, startTicketsGetHeaders(access), CODEX_CATALOG_FETCH_TIMEOUT_MS);
|
|
2573
|
+
}
|
|
2574
|
+
catch (err) {
|
|
2575
|
+
return {
|
|
2576
|
+
ok: false,
|
|
2577
|
+
warning: "model routing: Codex catalog unavailable; using configured default",
|
|
2578
|
+
diagnostic: {
|
|
2579
|
+
...diagnosticFromFetchError(err, {
|
|
2580
|
+
repoName: access.repoName,
|
|
2581
|
+
fallbackKind: "catalog-unavailable",
|
|
2582
|
+
message: "Codex model catalog unavailable",
|
|
2583
|
+
}),
|
|
2584
|
+
agentName: "codex",
|
|
2585
|
+
},
|
|
2586
|
+
};
|
|
2587
|
+
}
|
|
2588
|
+
const invalid = () => ({
|
|
2589
|
+
ok: false,
|
|
2590
|
+
warning: "model routing: Codex catalog invalid; using configured default",
|
|
2591
|
+
diagnostic: makeModelRoutingDiagnostic({
|
|
2592
|
+
kind: "catalog-invalid",
|
|
2593
|
+
repoName: access.repoName,
|
|
2594
|
+
agentName: "codex",
|
|
2595
|
+
message: "Codex model catalog failed validation",
|
|
2596
|
+
}),
|
|
2597
|
+
});
|
|
2598
|
+
if (!body || typeof body !== "object" || Array.isArray(body))
|
|
2599
|
+
return invalid();
|
|
2600
|
+
const obj = body;
|
|
2601
|
+
if (!obj.models || typeof obj.models !== "object" || Array.isArray(obj.models))
|
|
2602
|
+
return invalid();
|
|
2603
|
+
const models = obj.models;
|
|
2604
|
+
const tiers = ["cheap", "basic", "premium"];
|
|
2605
|
+
if (Object.keys(models).length !== tiers.length)
|
|
2606
|
+
return invalid();
|
|
2607
|
+
if (!tiers.every((tier) => typeof models[tier] === "string" && isValidModelAlias(models[tier]))) {
|
|
2608
|
+
return invalid();
|
|
2609
|
+
}
|
|
2610
|
+
const canonical = tiers.map((tier) => `${tier}=${models[tier]}`).join("\n");
|
|
2611
|
+
const revision = `sha256:${createHash("sha256").update(canonical, "utf8").digest("hex")}`;
|
|
2612
|
+
if (obj.revision !== revision)
|
|
2613
|
+
return invalid();
|
|
2614
|
+
return { ok: true, catalog: { models: models, revision } };
|
|
2615
|
+
}
|
|
2410
2616
|
/**
|
|
2411
2617
|
* Fetch the coarse tier for one ticket with a bounded timeout. Never throws to
|
|
2412
2618
|
* the batch orchestrator: failures/timeouts return a structured warning plus a
|
|
@@ -2532,6 +2738,24 @@ export function applyDefaultModelRoutingMetadata(row, reason, diagnostic) {
|
|
|
2532
2738
|
...(diagnostic ? { modelRoutingDiagnostic: diagnostic } : {}),
|
|
2533
2739
|
};
|
|
2534
2740
|
}
|
|
2741
|
+
function resolveRoutingAlias(agent, tier, overrides, catalog) {
|
|
2742
|
+
return agent.name === "codex"
|
|
2743
|
+
? catalog?.models[tier] ?? null
|
|
2744
|
+
: resolveModelAlias(agent, tier, overrides);
|
|
2745
|
+
}
|
|
2746
|
+
function codexEntitlementDiagnostic(repoName) {
|
|
2747
|
+
return makeModelRoutingDiagnostic({
|
|
2748
|
+
kind: "codex-entitlement-unverified",
|
|
2749
|
+
agentName: "codex",
|
|
2750
|
+
repoName,
|
|
2751
|
+
message: "Local Codex account entitlement cannot be verified non-interactively",
|
|
2752
|
+
});
|
|
2753
|
+
}
|
|
2754
|
+
function codexDefaultRows(rows, isEligible, warning, diagnostic) {
|
|
2755
|
+
return rows.map((row) => isEligible(row)
|
|
2756
|
+
? appendSummaryRowWarning(applyDefaultModelRoutingMetadata(row, warning, diagnostic ? { ...diagnostic, agentName: "codex" } : undefined), warning)
|
|
2757
|
+
: row);
|
|
2758
|
+
}
|
|
2535
2759
|
/**
|
|
2536
2760
|
* Resolve + validate the premium (Opus) alias ONCE and return a reusable applier.
|
|
2537
2761
|
*
|
|
@@ -2547,12 +2771,14 @@ export function applyDefaultModelRoutingMetadata(row, reason, diagnostic) {
|
|
|
2547
2771
|
* (e.g. it doesn't support --model, or verification fails) the applier falls back
|
|
2548
2772
|
* to the true no-routing default so we never emit an invalid --model.
|
|
2549
2773
|
*/
|
|
2550
|
-
async function makePremiumFallbackApplier(deps, agent, overrides) {
|
|
2551
|
-
const alias =
|
|
2774
|
+
async function makePremiumFallbackApplier(deps, agent, overrides, catalog = null, repoName) {
|
|
2775
|
+
const alias = resolveRoutingAlias(agent, "premium", overrides, catalog);
|
|
2552
2776
|
const validation = alias ? await validateResolvedModelAliasForAgent(deps, agent, alias) : null;
|
|
2553
2777
|
return (row, warning, diagnostic) => {
|
|
2554
2778
|
if (validation && validation.ok) {
|
|
2555
|
-
const reason =
|
|
2779
|
+
const reason = agent.name === "codex"
|
|
2780
|
+
? `${warning}; defaulting to premium (model=${validation.alias})`
|
|
2781
|
+
: `${warning}; defaulting to premium/Opus (model=${validation.alias})`;
|
|
2556
2782
|
return appendSummaryRowWarning({
|
|
2557
2783
|
...row,
|
|
2558
2784
|
difficulty: row.difficulty ?? null,
|
|
@@ -2560,7 +2786,11 @@ async function makePremiumFallbackApplier(deps, agent, overrides) {
|
|
|
2560
2786
|
modelAlias: validation.alias,
|
|
2561
2787
|
modelRoutingSource: "fallback",
|
|
2562
2788
|
modelRoutingReason: reason,
|
|
2563
|
-
...(diagnostic
|
|
2789
|
+
...(diagnostic
|
|
2790
|
+
? { modelRoutingDiagnostic: agent.name === "codex" ? { ...diagnostic, agentName: "codex" } : diagnostic }
|
|
2791
|
+
: agent.name === "codex" && repoName
|
|
2792
|
+
? { modelRoutingDiagnostic: codexEntitlementDiagnostic(repoName) }
|
|
2793
|
+
: {}),
|
|
2564
2794
|
}, warning);
|
|
2565
2795
|
}
|
|
2566
2796
|
// Premium alias unavailable for this agent — fall back to agent default.
|
|
@@ -2591,14 +2821,14 @@ const isDryRunRoutingEligible = (r) => r.status === "dry-run";
|
|
|
2591
2821
|
* validated ONCE (identical for every row) so a cursor-agent batch fires at most
|
|
2592
2822
|
* one advertised-model probe.
|
|
2593
2823
|
*/
|
|
2594
|
-
async function applyInjectedTierRoutingToEligibleRows(deps, rows, isEligible, agent, overrides, tier) {
|
|
2824
|
+
async function applyInjectedTierRoutingToEligibleRows(deps, rows, isEligible, agent, overrides, tier, catalog = null, repoName) {
|
|
2595
2825
|
const stampInjected = (row) => ({
|
|
2596
2826
|
...row,
|
|
2597
2827
|
difficulty: null,
|
|
2598
2828
|
modelTier: tier,
|
|
2599
2829
|
modelRoutingSource: "injected",
|
|
2600
2830
|
});
|
|
2601
|
-
const alias =
|
|
2831
|
+
const alias = resolveRoutingAlias(agent, tier, overrides, catalog);
|
|
2602
2832
|
if (!alias) {
|
|
2603
2833
|
const warning = `model routing: no valid alias for injected tier=${tier}; using agent default`;
|
|
2604
2834
|
return rows.map((r) => isEligible(r)
|
|
@@ -2616,15 +2846,17 @@ async function applyInjectedTierRoutingToEligibleRows(deps, rows, isEligible, ag
|
|
|
2616
2846
|
...stampInjected(r),
|
|
2617
2847
|
modelAlias: validation.alias,
|
|
2618
2848
|
modelRoutingReason: `injected tier=${tier} model=${validation.alias}`,
|
|
2849
|
+
...(agent.name === "codex" && repoName
|
|
2850
|
+
? { modelRoutingDiagnostic: codexEntitlementDiagnostic(repoName) }
|
|
2851
|
+
: {}),
|
|
2619
2852
|
}
|
|
2620
2853
|
: r);
|
|
2621
2854
|
}
|
|
2622
2855
|
/**
|
|
2623
2856
|
* Shared fail-open routing resolution. `isEligible` selects which rows get a
|
|
2624
|
-
* resolved tier/alias
|
|
2625
|
-
*
|
|
2626
|
-
*
|
|
2627
|
-
* pass through untouched.
|
|
2857
|
+
* resolved tier/alias. Codex fails open to its configured default with no
|
|
2858
|
+
* `--model`; Claude/Cursor retain their premium fallback. Non-eligible rows
|
|
2859
|
+
* pass through untouched, and routing failures never abort worktree creation.
|
|
2628
2860
|
*/
|
|
2629
2861
|
async function resolveModelRoutingForEligible(deps, rows, options, agent, isEligible) {
|
|
2630
2862
|
const eligible = rows.filter(isEligible);
|
|
@@ -2636,13 +2868,19 @@ async function resolveModelRoutingForEligible(deps, rows, options, agent, isElig
|
|
|
2636
2868
|
}
|
|
2637
2869
|
const accessResult = await resolveStartTicketsBridgeApiAccess(deps);
|
|
2638
2870
|
if (!accessResult.ok) {
|
|
2871
|
+
if (agent.name === "codex") {
|
|
2872
|
+
return codexDefaultRows(rows, isEligible, accessResult.warning, accessResult.diagnostic);
|
|
2873
|
+
}
|
|
2639
2874
|
// Genuine error reaching the backend — default unknown difficulty to premium/Opus,
|
|
2640
2875
|
// stamping the access diagnostic onto every eligible row.
|
|
2641
2876
|
return applyPremiumFallbackToEligibleRows(deps, rows, isEligible, agent, null, accessResult.warning, accessResult.diagnostic);
|
|
2642
2877
|
}
|
|
2643
2878
|
const access = accessResult.access;
|
|
2644
|
-
const configResult = await fetchDifficultyModelRoutingConfig(access);
|
|
2879
|
+
const configResult = await fetchDifficultyModelRoutingConfig(access, agent.name !== "codex");
|
|
2645
2880
|
if (!configResult.ok) {
|
|
2881
|
+
if (agent.name === "codex") {
|
|
2882
|
+
return codexDefaultRows(rows, isEligible, configResult.warning, configResult.diagnostic);
|
|
2883
|
+
}
|
|
2646
2884
|
// Can't read routing config — treat as error and default to premium/Opus.
|
|
2647
2885
|
return applyPremiumFallbackToEligibleRows(deps, rows, isEligible, agent, null, configResult.warning, configResult.diagnostic);
|
|
2648
2886
|
}
|
|
@@ -2651,6 +2889,14 @@ async function resolveModelRoutingForEligible(deps, rows, options, agent, isElig
|
|
|
2651
2889
|
const reason = "model routing: disabled for this repo; using agent default";
|
|
2652
2890
|
return rows.map((r) => (isEligible(r) ? applyDefaultModelRoutingMetadata(r, reason) : r));
|
|
2653
2891
|
}
|
|
2892
|
+
let catalog = null;
|
|
2893
|
+
if (agent.name === "codex") {
|
|
2894
|
+
const result = await fetchCodexModelCatalog(access);
|
|
2895
|
+
if (!result.ok) {
|
|
2896
|
+
return codexDefaultRows(rows, isEligible, result.warning, result.diagnostic);
|
|
2897
|
+
}
|
|
2898
|
+
catalog = result.catalog;
|
|
2899
|
+
}
|
|
2654
2900
|
// BAPI-642: an explicitly injected `--tier` short-circuits the per-ticket
|
|
2655
2901
|
// difficulty/tier lookup — `fetchTicketModelTiersForRows` is NEVER called for
|
|
2656
2902
|
// any injected value. A recognized tier is applied to every eligible row; a
|
|
@@ -2660,11 +2906,11 @@ async function resolveModelRoutingForEligible(deps, rows, options, agent, isElig
|
|
|
2660
2906
|
const injected = options.injectedTier;
|
|
2661
2907
|
if (injected === INJECTED_TIER_UNRESOLVED) {
|
|
2662
2908
|
const warning = "model routing: --tier was not one of cheap|basic|premium; defaulting to premium routing";
|
|
2663
|
-
const apply = await makePremiumFallbackApplier(deps, agent, overrides);
|
|
2909
|
+
const apply = await makePremiumFallbackApplier(deps, agent, overrides, catalog, access.repoName);
|
|
2664
2910
|
return rows.map((r) => (isEligible(r) ? apply(r, warning) : r));
|
|
2665
2911
|
}
|
|
2666
2912
|
if (injected) {
|
|
2667
|
-
return applyInjectedTierRoutingToEligibleRows(deps, rows, isEligible, agent, overrides, injected);
|
|
2913
|
+
return applyInjectedTierRoutingToEligibleRows(deps, rows, isEligible, agent, overrides, injected, catalog, access.repoName);
|
|
2668
2914
|
}
|
|
2669
2915
|
const tierMap = await fetchTicketModelTiersForRows(access, eligible, options.maxParallel, isEligible);
|
|
2670
2916
|
// Lazily resolve+validate the premium fallback alias at most once for this run
|
|
@@ -2672,7 +2918,7 @@ async function resolveModelRoutingForEligible(deps, rows, options, agent, isElig
|
|
|
2672
2918
|
let premiumApplier = null;
|
|
2673
2919
|
const applyPremiumFallback = async (row, warning, diagnostic) => {
|
|
2674
2920
|
if (!premiumApplier)
|
|
2675
|
-
premiumApplier = await makePremiumFallbackApplier(deps, agent, overrides);
|
|
2921
|
+
premiumApplier = await makePremiumFallbackApplier(deps, agent, overrides, catalog, access.repoName);
|
|
2676
2922
|
return premiumApplier(row, warning, diagnostic);
|
|
2677
2923
|
};
|
|
2678
2924
|
const out = [];
|
|
@@ -2697,6 +2943,10 @@ async function resolveModelRoutingForEligible(deps, rows, options, agent, isElig
|
|
|
2697
2943
|
ticketKey: row.key,
|
|
2698
2944
|
message: `no tier resolved for ${row.key}`,
|
|
2699
2945
|
});
|
|
2946
|
+
if (agent.name === "codex") {
|
|
2947
|
+
out.push(codexDefaultRows([row], () => true, warning, diagnostic)[0]);
|
|
2948
|
+
continue;
|
|
2949
|
+
}
|
|
2700
2950
|
out.push(await applyPremiumFallback(row, warning, diagnostic));
|
|
2701
2951
|
continue;
|
|
2702
2952
|
}
|
|
@@ -2718,10 +2968,14 @@ async function resolveModelRoutingForEligible(deps, rows, options, agent, isElig
|
|
|
2718
2968
|
ticketKey: row.key,
|
|
2719
2969
|
message: `${row.key} resolved no tier (source=${source})`,
|
|
2720
2970
|
});
|
|
2971
|
+
if (agent.name === "codex") {
|
|
2972
|
+
out.push(codexDefaultRows([baseRow], () => true, warning, diagnostic)[0]);
|
|
2973
|
+
continue;
|
|
2974
|
+
}
|
|
2721
2975
|
out.push(await applyPremiumFallback(baseRow, warning, diagnostic));
|
|
2722
2976
|
continue;
|
|
2723
2977
|
}
|
|
2724
|
-
const alias =
|
|
2978
|
+
const alias = resolveRoutingAlias(agent, tier, overrides, catalog);
|
|
2725
2979
|
if (!alias) {
|
|
2726
2980
|
const warning = `model routing: no valid alias for tier=${tier}; using agent default`;
|
|
2727
2981
|
out.push(appendSummaryRowWarning({ ...baseRow, modelAlias: null, modelRoutingReason: warning }, warning));
|
|
@@ -2736,6 +2990,9 @@ async function resolveModelRoutingForEligible(deps, rows, options, agent, isElig
|
|
|
2736
2990
|
...baseRow,
|
|
2737
2991
|
modelAlias: validation.alias,
|
|
2738
2992
|
modelRoutingReason: `tier=${tier} model=${validation.alias} (source=${source})`,
|
|
2993
|
+
...(agent.name === "codex"
|
|
2994
|
+
? { modelRoutingDiagnostic: codexEntitlementDiagnostic(access.repoName) }
|
|
2995
|
+
: {}),
|
|
2739
2996
|
});
|
|
2740
2997
|
}
|
|
2741
2998
|
return out;
|
|
@@ -2978,7 +3235,7 @@ export async function orchestrateStartTickets(deps, options, overrides = {}) {
|
|
|
2978
3235
|
if (!agent) {
|
|
2979
3236
|
return {
|
|
2980
3237
|
ok: false,
|
|
2981
|
-
error: `Unknown agent: '${options.agentName}'.
|
|
3238
|
+
error: `Unknown agent: '${options.agentName}'. Launchable agents: ${formatStartTicketsAgentNames()}.`,
|
|
2982
3239
|
};
|
|
2983
3240
|
}
|
|
2984
3241
|
const preflight = await runPreflight(deps, options, overrides.liveSourceWarningLog);
|
|
@@ -2992,6 +3249,7 @@ export async function orchestrateStartTickets(deps, options, overrides = {}) {
|
|
|
2992
3249
|
// Fail-open: an unresolved name is warned about loudly but never aborts the run.
|
|
2993
3250
|
const resolveRepoNameFn = overrides.resolveRepoName ?? resolveStartTicketsRepoName;
|
|
2994
3251
|
const resolvedRepoName = await resolveRepoNameFn(deps);
|
|
3252
|
+
const bridgeApiBaseUrl = await resolveStartTicketsBridgeApiBaseUrl(deps, agent);
|
|
2995
3253
|
if (!resolvedRepoName) {
|
|
2996
3254
|
overrides.repoNameWarningLog?.("Warning: could not resolve the repo name (set BAPI_REPO_NAME or add a valid " +
|
|
2997
3255
|
".bridge/config). Spawned worktrees may fall back to the directory name and " +
|
|
@@ -3004,7 +3262,7 @@ export async function orchestrateStartTickets(deps, options, overrides = {}) {
|
|
|
3004
3262
|
// epic.base_branch override applied above) so the spawned worker gets
|
|
3005
3263
|
// BAPI_BASE_BRANCH — every conductor worker, and a plain worker whose base
|
|
3006
3264
|
// is non-default.
|
|
3007
|
-
options.baseBranch);
|
|
3265
|
+
options.baseBranch, bridgeApiBaseUrl);
|
|
3008
3266
|
if (!platformConfig.ok)
|
|
3009
3267
|
return { ok: false, error: platformConfig.error };
|
|
3010
3268
|
// BAPI-527: resolve the base start point new worktrees are cut from BEFORE any
|
|
@@ -3052,9 +3310,9 @@ export async function orchestrateStartTickets(deps, options, overrides = {}) {
|
|
|
3052
3310
|
await emitClaudeReviewWorkflowDriftAdvisory(deps, options.baseBranch, overrides);
|
|
3053
3311
|
const createWorktreesFn = overrides.createWorktrees ?? createWorktrees;
|
|
3054
3312
|
const provisionCommandsFn = overrides.provisionCommands ??
|
|
3055
|
-
((rows, d) => provisionCommandsForCreatedWorktrees(rows, buildCommandProvisioningDeps(d)));
|
|
3313
|
+
((rows, d) => provisionCommandsForCreatedWorktrees(rows, buildCommandProvisioningDeps(d), agent.name === "codex"));
|
|
3056
3314
|
const provisionFn = overrides.provisionMcpRegistrations ??
|
|
3057
|
-
((rows, d) => provisionMcpRegistrationsForCreatedWorktrees(rows, buildMcpProvisioningDeps(d)));
|
|
3315
|
+
((rows, d) => provisionMcpRegistrationsForCreatedWorktrees(rows, buildMcpProvisioningDeps(d, bridgeApiBaseUrl)));
|
|
3058
3316
|
const materializeFn = overrides.materializeFileCredentials ?? materializeFileCredentialsForCreatedWorktrees;
|
|
3059
3317
|
const detectTerminalFn = overrides.detectTerminal ?? detectTerminal;
|
|
3060
3318
|
const spawnTabsFn = overrides.spawnTabsForCreatedWorktrees ?? spawnTabsForCreatedWorktrees;
|
|
@@ -3166,7 +3424,18 @@ export async function orchestrateStartTickets(deps, options, overrides = {}) {
|
|
|
3166
3424
|
// BAPI-844: stamp the validated scope on the spawnable rows immediately before
|
|
3167
3425
|
// the spawn, so the value the tab is launched with is the one this run
|
|
3168
3426
|
// declared. A run with no scope gets the identical `routed` array back.
|
|
3169
|
-
|
|
3427
|
+
let scoped = stampIndexScopeOnRows(routed, indexScope);
|
|
3428
|
+
if (agent.name === "codex") {
|
|
3429
|
+
const mcpDeps = buildMcpProvisioningDeps(deps);
|
|
3430
|
+
scoped = await Promise.all(scoped.map(async (row) => {
|
|
3431
|
+
if (row.status !== "created" || !row.path)
|
|
3432
|
+
return row;
|
|
3433
|
+
const scope = await (overrides.resolveCodexScope ?? resolveCodexLaunchScope)(row.path, mcpDeps);
|
|
3434
|
+
return scope.ok
|
|
3435
|
+
? { ...row, codexLaunchArgs: scope.argv }
|
|
3436
|
+
: { ...row, status: "spawn-failed", error: scope.error };
|
|
3437
|
+
}));
|
|
3438
|
+
}
|
|
3170
3439
|
const rows = await spawnTabsFn(deps, scoped, terminal, platformConfig.config.buildAgentShellCommand);
|
|
3171
3440
|
return { ok: true, rows };
|
|
3172
3441
|
}
|
|
@@ -3209,15 +3478,23 @@ export async function runStartTicketsCli(argv, overrides = {}) {
|
|
|
3209
3478
|
return 1;
|
|
3210
3479
|
}
|
|
3211
3480
|
const options = parsed.options;
|
|
3212
|
-
const
|
|
3481
|
+
const rawDeps = overrides.deps ?? createDefaultStartTicketsDeps();
|
|
3213
3482
|
const orchestrate = overrides.orchestrate ?? orchestrateStartTickets;
|
|
3214
3483
|
// The parser validates `--agent`, so this resolves to a real spec; the guard
|
|
3215
3484
|
// is defensive only.
|
|
3216
3485
|
const agent = resolveAgentSpec(options.agentName);
|
|
3217
3486
|
if (!agent) {
|
|
3218
|
-
errorLog(`Error: Unknown agent: '${options.agentName}'.
|
|
3487
|
+
errorLog(`Error: Unknown agent: '${options.agentName}'. Launchable agents: ${formatStartTicketsAgentNames()}.`);
|
|
3219
3488
|
return 1;
|
|
3220
3489
|
}
|
|
3490
|
+
// The editor's MCP registration can have a local endpoint that its Bash tool
|
|
3491
|
+
// does not inherit. Use that endpoint for the CLI's own routing as well as for
|
|
3492
|
+
// its generated worktree registration and terminal command. Never mutate the
|
|
3493
|
+
// caller's environment or read/copy its API key from the registration.
|
|
3494
|
+
const bridgeApiBaseUrl = await resolveStartTicketsBridgeApiBaseUrl(rawDeps, agent);
|
|
3495
|
+
const deps = bridgeApiBaseUrl
|
|
3496
|
+
? { ...rawDeps, env: { ...rawDeps.env, BAPI_BASE_URL: bridgeApiBaseUrl } }
|
|
3497
|
+
: rawDeps;
|
|
3221
3498
|
// BAPI-642: a malformed `--tier` value is fail-open — emit ONE concise,
|
|
3222
3499
|
// secret-free warning and continue (routing applies the premium fallback). It
|
|
3223
3500
|
// never aborts the launch or changes the numeric exit code.
|