@bridge_gpt/mcp-server 0.2.0 → 0.2.2
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 +56 -54
- package/build/agent-launchers/claude.js +25 -17
- package/build/agent-launchers/cursor.js +65 -0
- package/build/agent-launchers/index.js +23 -8
- package/build/agent-registry.js +68 -0
- package/build/command-catalog.js +376 -0
- package/build/commands.generated.js +8 -5
- package/build/index.js +406 -120
- package/build/mcp-provisioning.js +94 -1
- package/build/pipeline-utils.js +0 -33
- package/build/pipelines.generated.js +2 -31
- package/build/readme.generated.js +3 -0
- package/build/schedule-run.js +436 -88
- package/build/schedule-store.js +41 -1
- package/build/scheduled-prompt.js +109 -0
- package/build/scheduler-backends/at-fallback.js +5 -10
- package/build/scheduler-backends/escaping.js +40 -10
- package/build/scheduler-backends/launchd.js +23 -14
- package/build/scheduler-backends/systemd-user.js +32 -19
- package/build/scheduler-backends/task-scheduler.js +8 -13
- package/build/start-tickets.js +459 -30
- package/build/version.generated.js +1 -1
- package/package.json +4 -3
- package/pipelines/implement-ticket.json +2 -28
- package/smoke-test/SMOKE-TEST.md +61 -18
package/build/start-tickets.js
CHANGED
|
@@ -40,8 +40,9 @@
|
|
|
40
40
|
* via `; exec $SHELL`); attach with `tmux attach -t <session>`.
|
|
41
41
|
*
|
|
42
42
|
* Any other `process.platform` value fails fast with a clear "unsupported
|
|
43
|
-
* platform" message. `--dry-run`
|
|
44
|
-
*
|
|
43
|
+
* platform" message. `--dry-run` creates no worktrees and opens no tabs, but it
|
|
44
|
+
* DOES resolve model routing read-only (fail-open) so the preview shows the
|
|
45
|
+
* exact platform-correct command form — including the chosen `--model` — on any OS.
|
|
45
46
|
*
|
|
46
47
|
* The single highest-risk Windows detail is the `wt` name collision: Windows
|
|
47
48
|
* Terminal is `wt.exe` (tab launcher) while Worktrunk installs as `git-wt`
|
|
@@ -53,16 +54,19 @@
|
|
|
53
54
|
* unit-testable on Linux CI without spawning real commands or terminals.
|
|
54
55
|
*/
|
|
55
56
|
import { execFile } from "child_process";
|
|
56
|
-
import { readFile, writeFile, mkdir } from "fs/promises";
|
|
57
|
+
import { readFile, writeFile, mkdir, stat } from "fs/promises";
|
|
58
|
+
import os from "node:os";
|
|
57
59
|
import path from "path";
|
|
58
60
|
import { VERSION } from "./version.generated.js";
|
|
61
|
+
import { resolveBapiCredentials } from "./credential-store.js";
|
|
62
|
+
import { readBridgeConfig } from "./bridge-config.js";
|
|
59
63
|
import { provisionMcpRegistrationsForCreatedWorktrees, } from "./mcp-provisioning.js";
|
|
60
64
|
// Per-OS prerequisite knowledge + low-level command probes live in the shared
|
|
61
65
|
// prereqs module so `runPreflight` (enforce) and the read-only `doctor` (render)
|
|
62
66
|
// can never drift. `start-tickets.ts` imports VALUES from there; the prereqs
|
|
63
67
|
// module imports only TYPES back, so the runtime graph stays acyclic.
|
|
64
68
|
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";
|
|
65
|
-
import { DEFAULT_AGENT_NAME, resolveAgentSpec, isAgentName, formatValidAgentNames, } from "./agent-registry.js";
|
|
69
|
+
import { DEFAULT_AGENT_NAME, resolveAgentSpec, isAgentName, formatValidAgentNames, resolveModelAlias, isValidModelAlias, isModelTier, } from "./agent-registry.js";
|
|
66
70
|
// Re-export the shared prereq surface (constants, platform helpers, command
|
|
67
71
|
// probes) so existing import sites that read them from "./start-tickets.js"
|
|
68
72
|
// keep working unchanged.
|
|
@@ -94,7 +98,7 @@ export function getStartTicketsUsage() {
|
|
|
94
98
|
"Flags:",
|
|
95
99
|
" --agent claude|cursor-agent Agent command to launch in each worktree (default: claude)",
|
|
96
100
|
" --terminal terminal|iterm Override the macOS terminal app (default: auto-detect via $TERM_PROGRAM); honored on macOS only",
|
|
97
|
-
" --dry-run Print intended actions;
|
|
101
|
+
" --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",
|
|
98
102
|
" --branch KEY=BRANCH Use BRANCH instead of feature/KEY for that ticket (repeatable)",
|
|
99
103
|
" --base-branch BRANCH Cut new worktrees from BRANCH and refresh origin/BRANCH (default: main)",
|
|
100
104
|
" --no-refresh-main Skip refresh of the configured base branch (default main); historical name retained for backward compatibility",
|
|
@@ -391,7 +395,7 @@ export function resolveStartTicketsPlatformConfig(deps, agent, autoApprove = fal
|
|
|
391
395
|
config: {
|
|
392
396
|
platform,
|
|
393
397
|
worktrunkBinary: resolveWorktrunkBinary(platform, deps.env),
|
|
394
|
-
buildAgentShellCommand: (key, worktreePath) => buildAgentShellCommand(agent, key, worktreePath, platform, autoApprove),
|
|
398
|
+
buildAgentShellCommand: (key, worktreePath, modelAlias) => buildAgentShellCommand(agent, key, worktreePath, platform, autoApprove, modelAlias),
|
|
395
399
|
spawnTerminalTab: deps.spawnTerminalTab,
|
|
396
400
|
},
|
|
397
401
|
};
|
|
@@ -465,6 +469,8 @@ export function createDefaultStartTicketsDeps() {
|
|
|
465
469
|
// Worktrunk JSON / git porcelain output can be large; give it room.
|
|
466
470
|
maxBuffer: 64 * 1024 * 1024,
|
|
467
471
|
encoding: "utf-8",
|
|
472
|
+
// Optional bounded timeout (e.g. cursor-agent --list-models probes).
|
|
473
|
+
timeout: options?.timeoutMs,
|
|
468
474
|
}, (error, stdout, stderr) => {
|
|
469
475
|
const exitCode = error && typeof error.code === "number"
|
|
470
476
|
? (error.code)
|
|
@@ -799,44 +805,68 @@ export async function createWorktrees(deps, options, worktrunkBinary) {
|
|
|
799
805
|
* (`/implement-ticket <KEY> --auto`) — used by full-automation chains.
|
|
800
806
|
*/
|
|
801
807
|
export function buildAgentPrompt(key, opts = {}) {
|
|
808
|
+
// `modelAlias` is accepted for signature consistency only — the model is
|
|
809
|
+
// injected as a `--model` flag (see buildAgentInvocationArgv), never embedded
|
|
810
|
+
// in the prompt text.
|
|
802
811
|
return `/implement-ticket ${key}${opts.autoApprove ? " --auto" : ""}`;
|
|
803
812
|
}
|
|
804
813
|
/**
|
|
805
|
-
* Build the
|
|
806
|
-
*
|
|
807
|
-
* agent a
|
|
808
|
-
*
|
|
814
|
+
* Build the ordered argv for an agent invocation:
|
|
815
|
+
* `[command, (--model, alias)?, prompt]`. The model flag+alias are appended ONLY
|
|
816
|
+
* when the agent supports a model override AND `modelAlias` is a non-empty valid
|
|
817
|
+
* alias; otherwise they are omitted entirely (fail-open). The prompt is always
|
|
818
|
+
* the final argument. Pure and never throws on an invalid alias.
|
|
809
819
|
*/
|
|
810
|
-
export function
|
|
820
|
+
export function buildAgentInvocationArgv(agent, prompt, modelAlias) {
|
|
821
|
+
const argv = [agent.command];
|
|
822
|
+
if (agent.supportsModelOverride &&
|
|
823
|
+
typeof modelAlias === "string" &&
|
|
824
|
+
isValidModelAlias(modelAlias)) {
|
|
825
|
+
argv.push(agent.modelFlag, modelAlias);
|
|
826
|
+
}
|
|
827
|
+
argv.push(prompt);
|
|
828
|
+
return argv;
|
|
829
|
+
}
|
|
830
|
+
/**
|
|
831
|
+
* Build the agent invocation string for the agent's prompt style, from the
|
|
832
|
+
* validated argv array. The registry-controlled command head stays unquoted
|
|
833
|
+
* (it is never untrusted input); every following argument (the optional
|
|
834
|
+
* `--model <alias>` and the prompt) is run through the platform-correct `quote`.
|
|
835
|
+
*/
|
|
836
|
+
export function buildAgentInvocation(agent, prompt, quote, modelAlias) {
|
|
811
837
|
switch (agent.promptArgStyle) {
|
|
812
|
-
case "positional":
|
|
813
|
-
|
|
838
|
+
case "positional": {
|
|
839
|
+
const [command, ...rest] = buildAgentInvocationArgv(agent, prompt, modelAlias);
|
|
840
|
+
const quotedRest = rest.map(quote);
|
|
841
|
+
return [command, ...quotedRest].join(" ");
|
|
842
|
+
}
|
|
814
843
|
default: {
|
|
815
844
|
const exhaustive = agent.promptArgStyle;
|
|
816
845
|
throw new Error(`Unsupported agent promptArgStyle: ${String(exhaustive)}`);
|
|
817
846
|
}
|
|
818
847
|
}
|
|
819
848
|
}
|
|
820
|
-
/** POSIX agent shell command: `cd '<path>' && <agent> '<prompt>'`. */
|
|
821
|
-
export function buildPosixAgentShellCommand(agent, key, worktreePath, autoApprove = false) {
|
|
822
|
-
const invocation = buildAgentInvocation(agent, buildAgentPrompt(key, { autoApprove }), (p) => `'${shSquoteInner(p)}'
|
|
849
|
+
/** POSIX agent shell command: `cd '<path>' && <agent> [--model '<alias>'] '<prompt>'`. */
|
|
850
|
+
export function buildPosixAgentShellCommand(agent, key, worktreePath, autoApprove = false, modelAlias) {
|
|
851
|
+
const invocation = buildAgentInvocation(agent, buildAgentPrompt(key, { autoApprove }), (p) => `'${shSquoteInner(p)}'`, modelAlias);
|
|
823
852
|
return `cd '${shSquoteInner(worktreePath)}' && ${invocation}`;
|
|
824
853
|
}
|
|
825
|
-
/** PowerShell agent shell command: `Set-Location -LiteralPath '<path>'; <agent> '<prompt>'`. */
|
|
826
|
-
export function buildPowerShellAgentShellCommand(agent, key, worktreePath, autoApprove = false) {
|
|
827
|
-
const invocation = buildAgentInvocation(agent, buildAgentPrompt(key, { autoApprove }), powershellSquote);
|
|
854
|
+
/** PowerShell agent shell command: `Set-Location -LiteralPath '<path>'; <agent> [--model '<alias>'] '<prompt>'`. */
|
|
855
|
+
export function buildPowerShellAgentShellCommand(agent, key, worktreePath, autoApprove = false, modelAlias) {
|
|
856
|
+
const invocation = buildAgentInvocation(agent, buildAgentPrompt(key, { autoApprove }), powershellSquote, modelAlias);
|
|
828
857
|
return `Set-Location -LiteralPath ${powershellSquote(worktreePath)}; ${invocation}`;
|
|
829
858
|
}
|
|
830
859
|
/**
|
|
831
860
|
* Build the shell command run inside each spawned tab/session, dispatched by
|
|
832
861
|
* platform. PowerShell on Windows; POSIX everywhere else (incl. the unsupported
|
|
833
862
|
* dry-run fallback). The selected `agent` (never a module-level constant)
|
|
834
|
-
* determines the launched command.
|
|
863
|
+
* determines the launched command. An optional validated `modelAlias` is
|
|
864
|
+
* injected as `--model` at the spawn boundary.
|
|
835
865
|
*/
|
|
836
|
-
export function buildAgentShellCommand(agent, key, worktreePath, platform = "darwin", autoApprove = false) {
|
|
866
|
+
export function buildAgentShellCommand(agent, key, worktreePath, platform = "darwin", autoApprove = false, modelAlias) {
|
|
837
867
|
if (platform === "win32")
|
|
838
|
-
return buildPowerShellAgentShellCommand(agent, key, worktreePath, autoApprove);
|
|
839
|
-
return buildPosixAgentShellCommand(agent, key, worktreePath, autoApprove);
|
|
868
|
+
return buildPowerShellAgentShellCommand(agent, key, worktreePath, autoApprove, modelAlias);
|
|
869
|
+
return buildPosixAgentShellCommand(agent, key, worktreePath, autoApprove, modelAlias);
|
|
840
870
|
}
|
|
841
871
|
// ---------------------------------------------------------------------------
|
|
842
872
|
// macOS terminal spawning (behind the injected boundary)
|
|
@@ -1081,7 +1111,7 @@ export async function spawnTabsForCreatedWorktrees(deps, rows, terminal, buildSh
|
|
|
1081
1111
|
out.push(row);
|
|
1082
1112
|
continue;
|
|
1083
1113
|
}
|
|
1084
|
-
const shellCommand = buildShellCommand(row.key, row.path);
|
|
1114
|
+
const shellCommand = buildShellCommand(row.key, row.path, row.modelAlias ?? null);
|
|
1085
1115
|
const result = await deps.spawnTerminalTab(deps, terminal, shellCommand, {
|
|
1086
1116
|
key: row.key,
|
|
1087
1117
|
worktreePath: row.path,
|
|
@@ -1115,7 +1145,9 @@ export function buildDryRunResults(keys, overrides) {
|
|
|
1115
1145
|
export function getDryRunPlatformDetails(agent, platform = process.platform, env = process.env, autoApprove = false) {
|
|
1116
1146
|
return {
|
|
1117
1147
|
worktrunkBinary: resolveWorktrunkBinary(platform, env),
|
|
1118
|
-
|
|
1148
|
+
// The builder accepts an optional resolved modelAlias; the dry-run caller
|
|
1149
|
+
// now passes the previewed tier's alias so `--model` shows in the preview.
|
|
1150
|
+
buildAgentShellCommand: (key, worktreePath, modelAlias) => buildAgentShellCommand(agent, key, worktreePath, platform, autoApprove, modelAlias),
|
|
1119
1151
|
};
|
|
1120
1152
|
}
|
|
1121
1153
|
/**
|
|
@@ -1144,10 +1176,10 @@ export function buildDryRunMcpProvisioningLines(worktreePath, platform = process
|
|
|
1144
1176
|
* the secret-free MCP provisioning preview. Pure platform formatting only — no
|
|
1145
1177
|
* preflight, no routing failures.
|
|
1146
1178
|
*/
|
|
1147
|
-
export function buildDryRunDetailLines(agent, key, branch, platform = process.platform, env = process.env, baseBranch = "main", autoApprove = false) {
|
|
1179
|
+
export function buildDryRunDetailLines(agent, key, branch, platform = process.platform, env = process.env, baseBranch = "main", autoApprove = false, modelAlias = null) {
|
|
1148
1180
|
const { worktrunkBinary, buildAgentShellCommand: build } = getDryRunPlatformDetails(agent, platform, env, autoApprove);
|
|
1149
1181
|
const wtArgs = buildWtSwitchArgs(branch, false, baseBranch);
|
|
1150
|
-
const agentInvocation = build(key, "<worktree-path>");
|
|
1182
|
+
const agentInvocation = build(key, "<worktree-path>", modelAlias);
|
|
1151
1183
|
return [
|
|
1152
1184
|
`DRY-RUN: ${key} -> branch=${branch}`,
|
|
1153
1185
|
`DRY-RUN: ${worktrunkBinary} ${wtArgs.join(" ")}`,
|
|
@@ -1235,6 +1267,371 @@ export function buildMcpProvisioningDeps(deps) {
|
|
|
1235
1267
|
export async function materializeFileCredentialsForCreatedWorktrees(rows, _deps) {
|
|
1236
1268
|
return rows;
|
|
1237
1269
|
}
|
|
1270
|
+
// ---------------------------------------------------------------------------
|
|
1271
|
+
// BAPI-365: difficulty-based model routing (fail-open at the spawn boundary)
|
|
1272
|
+
// ---------------------------------------------------------------------------
|
|
1273
|
+
/** Default Bridge API base URL when BAPI_BASE_URL is unset. */
|
|
1274
|
+
export const START_TICKETS_DEFAULT_BASE_URL = "https://bridgegpt-api.com";
|
|
1275
|
+
const TICKET_MODEL_TIER_FETCH_TIMEOUT_MS = 75_000;
|
|
1276
|
+
const CONFIG_FIELD_FETCH_TIMEOUT_MS = 30_000;
|
|
1277
|
+
const CURSOR_MODEL_LIST_TIMEOUT_MS = 15_000;
|
|
1278
|
+
/** GET auth headers for the CLI's Bridge API calls. */
|
|
1279
|
+
function startTicketsGetHeaders(access) {
|
|
1280
|
+
return {
|
|
1281
|
+
"X-API-Key": access.apiKey,
|
|
1282
|
+
"X-Bridge-MCP-Version": VERSION,
|
|
1283
|
+
};
|
|
1284
|
+
}
|
|
1285
|
+
/**
|
|
1286
|
+
* Resolve the repo name for routing: prefer `BAPI_REPO_NAME`, then `.bridge/config`.
|
|
1287
|
+
* Returns `null` when neither is available.
|
|
1288
|
+
*/
|
|
1289
|
+
export async function resolveStartTicketsRepoName(deps) {
|
|
1290
|
+
const fromEnv = deps.env.BAPI_REPO_NAME;
|
|
1291
|
+
if (typeof fromEnv === "string" && fromEnv.trim().length > 0) {
|
|
1292
|
+
return fromEnv.trim();
|
|
1293
|
+
}
|
|
1294
|
+
try {
|
|
1295
|
+
const result = await readBridgeConfig(deps.cwd, {
|
|
1296
|
+
readFile: (filePath) => readFile(filePath, "utf-8"),
|
|
1297
|
+
});
|
|
1298
|
+
if (result.ok && result.manifest.repoName) {
|
|
1299
|
+
return result.manifest.repoName;
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
catch {
|
|
1303
|
+
// fall through to null
|
|
1304
|
+
}
|
|
1305
|
+
return null;
|
|
1306
|
+
}
|
|
1307
|
+
/**
|
|
1308
|
+
* Resolve Bridge API access (repo name + API key + base URL). Returns a
|
|
1309
|
+
* structured failure — never throwing and never embedding secret values — so the
|
|
1310
|
+
* caller can degrade to the agent default model.
|
|
1311
|
+
*/
|
|
1312
|
+
export async function resolveStartTicketsBridgeApiAccess(deps) {
|
|
1313
|
+
const repoName = await resolveStartTicketsRepoName(deps);
|
|
1314
|
+
if (!repoName) {
|
|
1315
|
+
return {
|
|
1316
|
+
ok: false,
|
|
1317
|
+
warning: "model routing: could not resolve repo name (set BAPI_REPO_NAME or .bridge/config); using agent default model",
|
|
1318
|
+
};
|
|
1319
|
+
}
|
|
1320
|
+
let credResult;
|
|
1321
|
+
try {
|
|
1322
|
+
credResult = await resolveBapiCredentials(repoName, {
|
|
1323
|
+
env: deps.env,
|
|
1324
|
+
homedir: os.homedir,
|
|
1325
|
+
platform: deps.platform,
|
|
1326
|
+
readFile: (p) => readFile(p, "utf-8"),
|
|
1327
|
+
stat: (p) => stat(p),
|
|
1328
|
+
});
|
|
1329
|
+
}
|
|
1330
|
+
catch {
|
|
1331
|
+
return { ok: false, warning: "model routing: failed to resolve Bridge API credentials; using agent default model" };
|
|
1332
|
+
}
|
|
1333
|
+
if (!credResult.ok) {
|
|
1334
|
+
return { ok: false, warning: "model routing: Bridge API credentials unavailable; using agent default model" };
|
|
1335
|
+
}
|
|
1336
|
+
const baseUrlRaw = deps.env.BAPI_BASE_URL;
|
|
1337
|
+
const baseUrl = typeof baseUrlRaw === "string" && baseUrlRaw.trim().length > 0
|
|
1338
|
+
? baseUrlRaw.trim()
|
|
1339
|
+
: START_TICKETS_DEFAULT_BASE_URL;
|
|
1340
|
+
return { ok: true, access: { repoName, apiKey: credResult.credentials.apiKey, baseUrl } };
|
|
1341
|
+
}
|
|
1342
|
+
/** Build a `${baseUrl}/jira${apiPath}` URL with query params; trims trailing slashes. */
|
|
1343
|
+
export function buildStartTicketsJiraUrl(baseUrl, apiPath, params = {}) {
|
|
1344
|
+
const trimmed = baseUrl.replace(/\/+$/, "");
|
|
1345
|
+
const url = new URL(`${trimmed}/jira${apiPath}`);
|
|
1346
|
+
for (const [k, v] of Object.entries(params)) {
|
|
1347
|
+
url.searchParams.set(k, v);
|
|
1348
|
+
}
|
|
1349
|
+
return url.toString();
|
|
1350
|
+
}
|
|
1351
|
+
/**
|
|
1352
|
+
* GET JSON with an `AbortController` timeout. Throws a generic error (no secret
|
|
1353
|
+
* material) on a non-2xx response; always clears the timeout.
|
|
1354
|
+
*/
|
|
1355
|
+
export async function fetchJsonWithTimeout(url, headers, timeoutMs) {
|
|
1356
|
+
const controller = new AbortController();
|
|
1357
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
1358
|
+
try {
|
|
1359
|
+
const resp = await fetch(url, { headers, signal: controller.signal });
|
|
1360
|
+
if (!resp.ok) {
|
|
1361
|
+
throw new Error(`request failed with status ${resp.status}`);
|
|
1362
|
+
}
|
|
1363
|
+
return await resp.json();
|
|
1364
|
+
}
|
|
1365
|
+
finally {
|
|
1366
|
+
clearTimeout(timer);
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
/** Normalize an unknown config value into a clean tier->alias override map. */
|
|
1370
|
+
export function normalizeDifficultyModelTierOverrides(value) {
|
|
1371
|
+
if (value === null || value === undefined)
|
|
1372
|
+
return {};
|
|
1373
|
+
if (typeof value !== "object" || Array.isArray(value))
|
|
1374
|
+
return {};
|
|
1375
|
+
const out = {};
|
|
1376
|
+
for (const [key, raw] of Object.entries(value)) {
|
|
1377
|
+
if (!isModelTier(key))
|
|
1378
|
+
continue;
|
|
1379
|
+
if (typeof raw !== "string")
|
|
1380
|
+
continue;
|
|
1381
|
+
const trimmed = raw.trim();
|
|
1382
|
+
if (trimmed.length === 0)
|
|
1383
|
+
continue;
|
|
1384
|
+
out[key] = trimmed;
|
|
1385
|
+
}
|
|
1386
|
+
return out;
|
|
1387
|
+
}
|
|
1388
|
+
/** GET /jira/config-field/{fieldName}; returns the parsed `value` (or undefined). */
|
|
1389
|
+
export async function fetchStartTicketsConfigField(access, fieldName) {
|
|
1390
|
+
const url = buildStartTicketsJiraUrl(access.baseUrl, `/config-field/${encodeURIComponent(fieldName)}`, { repo_name: access.repoName });
|
|
1391
|
+
const body = await fetchJsonWithTimeout(url, startTicketsGetHeaders(access), CONFIG_FIELD_FETCH_TIMEOUT_MS);
|
|
1392
|
+
if (body && typeof body === "object" && "value" in body) {
|
|
1393
|
+
return body.value;
|
|
1394
|
+
}
|
|
1395
|
+
return undefined;
|
|
1396
|
+
}
|
|
1397
|
+
/**
|
|
1398
|
+
* Fetch the repo's routing config once. A missing/null enable value defaults to
|
|
1399
|
+
* enabled (the feature ships ON); an explicit boolean `false` disables routing.
|
|
1400
|
+
* Any fetch failure returns a structured failure so the caller omits `--model`.
|
|
1401
|
+
*/
|
|
1402
|
+
export async function fetchDifficultyModelRoutingConfig(access) {
|
|
1403
|
+
try {
|
|
1404
|
+
const enabledValue = await fetchStartTicketsConfigField(access, "difficulty_model_routing_enabled");
|
|
1405
|
+
const overridesValue = await fetchStartTicketsConfigField(access, "difficulty_model_tier_overrides");
|
|
1406
|
+
const enabled = enabledValue === false ? false : true;
|
|
1407
|
+
return { ok: true, config: { enabled, overrides: normalizeDifficultyModelTierOverrides(overridesValue) } };
|
|
1408
|
+
}
|
|
1409
|
+
catch {
|
|
1410
|
+
return { ok: false, warning: "model routing: failed to fetch routing config; using agent default model" };
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
/**
|
|
1414
|
+
* Fetch the coarse tier for one ticket with a bounded timeout. Never throws to
|
|
1415
|
+
* the batch orchestrator: failures/timeouts return a structured warning.
|
|
1416
|
+
*/
|
|
1417
|
+
export async function fetchTicketModelTierForStartTickets(access, ticket) {
|
|
1418
|
+
try {
|
|
1419
|
+
const url = buildStartTicketsJiraUrl(access.baseUrl, `/tickets/${encodeURIComponent(ticket)}/model-tier`, { repo_name: access.repoName });
|
|
1420
|
+
const body = await fetchJsonWithTimeout(url, startTicketsGetHeaders(access), TICKET_MODEL_TIER_FETCH_TIMEOUT_MS);
|
|
1421
|
+
const obj = (body ?? {});
|
|
1422
|
+
const tier = isModelTier(obj.tier) ? obj.tier : null;
|
|
1423
|
+
const difficulty = typeof obj.difficulty === "number" ? obj.difficulty : null;
|
|
1424
|
+
const source = obj.source === "cached" || obj.source === "computed" || obj.source === "fallback"
|
|
1425
|
+
? obj.source
|
|
1426
|
+
: "fallback";
|
|
1427
|
+
return { ok: true, value: { difficulty, tier, source } };
|
|
1428
|
+
}
|
|
1429
|
+
catch {
|
|
1430
|
+
return { ok: false, warning: `model routing: tier lookup failed for ${ticket}; using agent default model` };
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
/**
|
|
1434
|
+
* Fetch tiers for the still-spawnable rows with bounded parallelism (capped at
|
|
1435
|
+
* 3). One ticket's failure never aborts the others; results are keyed by ticket.
|
|
1436
|
+
*/
|
|
1437
|
+
export async function fetchTicketModelTiersForRows(access, rows, maxParallel, isEligible = (r) => r.status === "created" && !!r.path) {
|
|
1438
|
+
const eligible = rows.filter(isEligible);
|
|
1439
|
+
const limit = Math.min(maxParallel, 3);
|
|
1440
|
+
const results = await runWithConcurrency(eligible, limit, (row) => fetchTicketModelTierForStartTickets(access, row.key).then((res) => [row.key, res]));
|
|
1441
|
+
return new Map(results);
|
|
1442
|
+
}
|
|
1443
|
+
/**
|
|
1444
|
+
* Parse a model-list command's stdout into a set of advertised model aliases.
|
|
1445
|
+
* Tokens are split on whitespace/commas, stripped of surrounding bullets and
|
|
1446
|
+
* punctuation, and kept only when they match the alias allowlist pattern.
|
|
1447
|
+
*/
|
|
1448
|
+
export function parseAdvertisedAgentModels(stdout) {
|
|
1449
|
+
const out = new Set();
|
|
1450
|
+
for (const token of stdout.split(/[\s,]+/)) {
|
|
1451
|
+
const cleaned = token.replace(/^[^A-Za-z0-9._:-]+/, "").replace(/[^A-Za-z0-9._:-]+$/, "");
|
|
1452
|
+
// Require at least one alphanumeric char so bare punctuation (e.g. a "-"
|
|
1453
|
+
// bullet) is not mistaken for an alias even though it matches the pattern.
|
|
1454
|
+
if (cleaned.length > 0 && /[A-Za-z0-9]/.test(cleaned) && isValidModelAlias(cleaned)) {
|
|
1455
|
+
out.add(cleaned);
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1458
|
+
return out;
|
|
1459
|
+
}
|
|
1460
|
+
/**
|
|
1461
|
+
* Discover the cursor-agent advertised model set by running
|
|
1462
|
+
* `cursor-agent --list-models` (then `cursor-agent models` as a fallback)
|
|
1463
|
+
* non-interactively with a bounded timeout. Returns `null` on failure, timeout,
|
|
1464
|
+
* empty output, or parse uncertainty.
|
|
1465
|
+
*/
|
|
1466
|
+
export async function fetchCursorAgentAdvertisedModels(deps, agent) {
|
|
1467
|
+
const tryCommand = async (args) => {
|
|
1468
|
+
try {
|
|
1469
|
+
const result = await deps.runCommand(agent.command, args, {
|
|
1470
|
+
cwd: deps.cwd,
|
|
1471
|
+
timeoutMs: CURSOR_MODEL_LIST_TIMEOUT_MS,
|
|
1472
|
+
});
|
|
1473
|
+
if (!commandSucceeded(result))
|
|
1474
|
+
return null;
|
|
1475
|
+
const models = parseAdvertisedAgentModels(result.stdout);
|
|
1476
|
+
return models.size > 0 ? models : null;
|
|
1477
|
+
}
|
|
1478
|
+
catch {
|
|
1479
|
+
return null;
|
|
1480
|
+
}
|
|
1481
|
+
};
|
|
1482
|
+
const primary = await tryCommand(["--list-models"]);
|
|
1483
|
+
if (primary)
|
|
1484
|
+
return primary;
|
|
1485
|
+
return tryCommand(["models"]);
|
|
1486
|
+
}
|
|
1487
|
+
/**
|
|
1488
|
+
* Validate a resolved alias for the agent before it reaches shell construction.
|
|
1489
|
+
* For non-cursor agents the candidate is returned unchanged (resolveModelAlias
|
|
1490
|
+
* already statically validated claude aliases). For cursor-agent the candidate
|
|
1491
|
+
* must appear in the live advertised model set; otherwise it is rejected.
|
|
1492
|
+
*/
|
|
1493
|
+
export async function validateResolvedModelAliasForAgent(deps, agent, candidate) {
|
|
1494
|
+
if (agent.name !== "cursor-agent") {
|
|
1495
|
+
return { ok: true, alias: candidate };
|
|
1496
|
+
}
|
|
1497
|
+
const advertised = await fetchCursorAgentAdvertisedModels(deps, agent);
|
|
1498
|
+
if (!advertised) {
|
|
1499
|
+
return {
|
|
1500
|
+
ok: false,
|
|
1501
|
+
warning: `model routing: could not verify cursor-agent models; omitting --model for '${candidate}'`,
|
|
1502
|
+
};
|
|
1503
|
+
}
|
|
1504
|
+
if (!advertised.has(candidate)) {
|
|
1505
|
+
return {
|
|
1506
|
+
ok: false,
|
|
1507
|
+
warning: `model routing: cursor-agent model '${candidate}' not advertised; omitting --model`,
|
|
1508
|
+
};
|
|
1509
|
+
}
|
|
1510
|
+
return { ok: true, alias: candidate };
|
|
1511
|
+
}
|
|
1512
|
+
/** Return a copy of `row` with default (no-routing) metadata and a reason. */
|
|
1513
|
+
export function applyDefaultModelRoutingMetadata(row, reason) {
|
|
1514
|
+
return {
|
|
1515
|
+
...row,
|
|
1516
|
+
difficulty: row.difficulty ?? null,
|
|
1517
|
+
modelTier: null,
|
|
1518
|
+
modelAlias: null,
|
|
1519
|
+
modelRoutingSource: row.modelRoutingSource ?? "default",
|
|
1520
|
+
modelRoutingReason: reason,
|
|
1521
|
+
};
|
|
1522
|
+
}
|
|
1523
|
+
/** Real spawn path: a worktree was created and has a path. */
|
|
1524
|
+
const isCreatedRoutingEligible = (r) => r.status === "created" && !!r.path;
|
|
1525
|
+
/** Dry-run path: rows have no worktree path yet but are eligible for a preview. */
|
|
1526
|
+
const isDryRunRoutingEligible = (r) => r.status === "dry-run";
|
|
1527
|
+
/**
|
|
1528
|
+
* Shared fail-open routing resolution. `isEligible` selects which rows get a
|
|
1529
|
+
* resolved tier/alias; every failure mode (unsupported agent, credential/config/
|
|
1530
|
+
* tier fetch failure, backend fallback, invalid/unavailable alias) is converted
|
|
1531
|
+
* to `modelAlias: null` plus a row warning, and never throws. Non-eligible rows
|
|
1532
|
+
* pass through untouched.
|
|
1533
|
+
*/
|
|
1534
|
+
async function resolveModelRoutingForEligible(deps, rows, options, agent, isEligible) {
|
|
1535
|
+
const eligible = rows.filter(isEligible);
|
|
1536
|
+
if (eligible.length === 0)
|
|
1537
|
+
return rows;
|
|
1538
|
+
if (!agent.supportsModelOverride) {
|
|
1539
|
+
const reason = `model routing: agent '${agent.name}' does not support --model; using agent default`;
|
|
1540
|
+
return rows.map((r) => (isEligible(r) ? applyDefaultModelRoutingMetadata(r, reason) : r));
|
|
1541
|
+
}
|
|
1542
|
+
const accessResult = await resolveStartTicketsBridgeApiAccess(deps);
|
|
1543
|
+
if (!accessResult.ok) {
|
|
1544
|
+
return rows.map((r) => isEligible(r)
|
|
1545
|
+
? appendSummaryRowWarning(applyDefaultModelRoutingMetadata(r, accessResult.warning), accessResult.warning)
|
|
1546
|
+
: r);
|
|
1547
|
+
}
|
|
1548
|
+
const access = accessResult.access;
|
|
1549
|
+
const configResult = await fetchDifficultyModelRoutingConfig(access);
|
|
1550
|
+
if (!configResult.ok) {
|
|
1551
|
+
return rows.map((r) => isEligible(r)
|
|
1552
|
+
? appendSummaryRowWarning(applyDefaultModelRoutingMetadata(r, configResult.warning), configResult.warning)
|
|
1553
|
+
: r);
|
|
1554
|
+
}
|
|
1555
|
+
const { enabled, overrides } = configResult.config;
|
|
1556
|
+
if (!enabled) {
|
|
1557
|
+
const reason = "model routing: disabled for this repo; using agent default";
|
|
1558
|
+
return rows.map((r) => (isEligible(r) ? applyDefaultModelRoutingMetadata(r, reason) : r));
|
|
1559
|
+
}
|
|
1560
|
+
const tierMap = await fetchTicketModelTiersForRows(access, eligible, options.maxParallel, isEligible);
|
|
1561
|
+
const out = [];
|
|
1562
|
+
for (const row of rows) {
|
|
1563
|
+
if (!isEligible(row)) {
|
|
1564
|
+
out.push(row);
|
|
1565
|
+
continue;
|
|
1566
|
+
}
|
|
1567
|
+
const tierResult = tierMap.get(row.key);
|
|
1568
|
+
if (!tierResult || !tierResult.ok) {
|
|
1569
|
+
const warning = tierResult && !tierResult.ok
|
|
1570
|
+
? tierResult.warning
|
|
1571
|
+
: `model routing: no tier resolved for ${row.key}; using agent default`;
|
|
1572
|
+
out.push(appendSummaryRowWarning(applyDefaultModelRoutingMetadata(row, warning), warning));
|
|
1573
|
+
continue;
|
|
1574
|
+
}
|
|
1575
|
+
const { difficulty, tier, source } = tierResult.value;
|
|
1576
|
+
const baseRow = {
|
|
1577
|
+
...row,
|
|
1578
|
+
difficulty: difficulty ?? null,
|
|
1579
|
+
modelTier: tier ?? null,
|
|
1580
|
+
modelRoutingSource: source,
|
|
1581
|
+
};
|
|
1582
|
+
if (!tier) {
|
|
1583
|
+
const warning = `model routing: ${row.key} resolved no tier (source=${source}); using agent default`;
|
|
1584
|
+
out.push(appendSummaryRowWarning(applyDefaultModelRoutingMetadata(baseRow, warning), warning));
|
|
1585
|
+
continue;
|
|
1586
|
+
}
|
|
1587
|
+
const alias = resolveModelAlias(agent, tier, overrides);
|
|
1588
|
+
if (!alias) {
|
|
1589
|
+
const warning = `model routing: no valid alias for tier=${tier}; using agent default`;
|
|
1590
|
+
out.push(appendSummaryRowWarning({ ...baseRow, modelAlias: null, modelRoutingReason: warning }, warning));
|
|
1591
|
+
continue;
|
|
1592
|
+
}
|
|
1593
|
+
const validation = await validateResolvedModelAliasForAgent(deps, agent, alias);
|
|
1594
|
+
if (!validation.ok) {
|
|
1595
|
+
out.push(appendSummaryRowWarning({ ...baseRow, modelAlias: null, modelRoutingReason: validation.warning }, validation.warning));
|
|
1596
|
+
continue;
|
|
1597
|
+
}
|
|
1598
|
+
out.push({
|
|
1599
|
+
...baseRow,
|
|
1600
|
+
modelAlias: validation.alias,
|
|
1601
|
+
modelRoutingReason: `tier=${tier} model=${validation.alias} (source=${source})`,
|
|
1602
|
+
});
|
|
1603
|
+
}
|
|
1604
|
+
return out;
|
|
1605
|
+
}
|
|
1606
|
+
/**
|
|
1607
|
+
* Resolve per-ticket model routing for the spawnable (created) rows. Public
|
|
1608
|
+
* entrypoint for the real spawn path. ALWAYS fail-open (see
|
|
1609
|
+
* {@link resolveModelRoutingForEligible}).
|
|
1610
|
+
*/
|
|
1611
|
+
export async function resolveModelRoutingForRows(deps, rows, options, agent) {
|
|
1612
|
+
return resolveModelRoutingForEligible(deps, rows, options, agent, isCreatedRoutingEligible);
|
|
1613
|
+
}
|
|
1614
|
+
/**
|
|
1615
|
+
* Dry-run variant: resolve routing for `dry-run` rows so `--dry-run` can preview
|
|
1616
|
+
* the model each tab would launch with. Identical fail-open resolution; only the
|
|
1617
|
+
* eligibility differs (dry-run rows carry no worktree path). NOTE: this performs
|
|
1618
|
+
* the same read-only tier lookup as a real run, so for a ticket with no cached
|
|
1619
|
+
* difficulty the backend may compute and cache it (one LLM call) — exactly what
|
|
1620
|
+
* the subsequent real run would have done.
|
|
1621
|
+
*/
|
|
1622
|
+
export async function resolveModelRoutingForDryRun(deps, rows, options, agent) {
|
|
1623
|
+
return resolveModelRoutingForEligible(deps, rows, options, agent, isDryRunRoutingEligible);
|
|
1624
|
+
}
|
|
1625
|
+
/**
|
|
1626
|
+
* Format one concise routing decision line per ticket:
|
|
1627
|
+
* `KEY difficulty=<n|?> tier=<tier|fallback> agent=<name> model=<alias|default>`.
|
|
1628
|
+
*/
|
|
1629
|
+
export function formatModelRoutingLine(row, agent) {
|
|
1630
|
+
const difficulty = typeof row.difficulty === "number" ? String(row.difficulty) : "?";
|
|
1631
|
+
const tier = row.modelTier ?? "fallback";
|
|
1632
|
+
const model = row.modelAlias ?? "default";
|
|
1633
|
+
return `${row.key} difficulty=${difficulty} tier=${tier} agent=${agent.name} model=${model}`;
|
|
1634
|
+
}
|
|
1238
1635
|
export async function orchestrateStartTickets(deps, options, overrides = {}) {
|
|
1239
1636
|
if (options.dryRun) {
|
|
1240
1637
|
return { ok: true, rows: buildDryRunResults(options.keys, options.branchOverrides) };
|
|
@@ -1276,8 +1673,21 @@ export async function orchestrateStartTickets(deps, options, overrides = {}) {
|
|
|
1276
1673
|
// and BEFORE tab spawning (currently a pass-through seam; see
|
|
1277
1674
|
// materializeFileCredentialsForCreatedWorktrees).
|
|
1278
1675
|
const materialized = await materializeFn(provisioned, deps);
|
|
1676
|
+
// BAPI-365: resolve per-ticket model routing AFTER provisioning/materialization
|
|
1677
|
+
// and BEFORE building the spawn command. Always fail-open: routing never aborts
|
|
1678
|
+
// a spawn — at worst a ticket runs on the agent's default model.
|
|
1679
|
+
const resolveRoutingFn = overrides.resolveModelRoutingForRows ?? resolveModelRoutingForRows;
|
|
1680
|
+
const routed = await resolveRoutingFn(deps, materialized, options, agent);
|
|
1681
|
+
for (const row of routed) {
|
|
1682
|
+
if (row.status !== "created" || !row.path)
|
|
1683
|
+
continue;
|
|
1684
|
+
overrides.modelRoutingLog?.(formatModelRoutingLine(row, agent));
|
|
1685
|
+
if (row.modelAlias == null && row.modelRoutingReason) {
|
|
1686
|
+
overrides.modelRoutingWarningLog?.(`${row.key}: ${row.modelRoutingReason}`);
|
|
1687
|
+
}
|
|
1688
|
+
}
|
|
1279
1689
|
const terminal = detectTerminalFn(options.terminal, deps.env);
|
|
1280
|
-
const rows = await spawnTabsFn(deps,
|
|
1690
|
+
const rows = await spawnTabsFn(deps, routed, terminal, platformConfig.config.buildAgentShellCommand);
|
|
1281
1691
|
return { ok: true, rows };
|
|
1282
1692
|
}
|
|
1283
1693
|
/** Platform-specific guidance printed when one or more tabs fail to spawn. */
|
|
@@ -1329,15 +1739,34 @@ export async function runStartTicketsCli(argv, overrides = {}) {
|
|
|
1329
1739
|
return 1;
|
|
1330
1740
|
}
|
|
1331
1741
|
if (options.dryRun) {
|
|
1742
|
+
// Preview model routing too: resolve tiers read-only (fail-open) so the
|
|
1743
|
+
// dry-run shows the exact `--model` each tab would launch with, plus a
|
|
1744
|
+
// per-ticket routing decision line.
|
|
1745
|
+
const resolveDryRunRoutingFn = overrides.resolveModelRoutingForDryRun ?? resolveModelRoutingForDryRun;
|
|
1746
|
+
const dryRunRows = buildDryRunResults(options.keys, options.branchOverrides);
|
|
1747
|
+
const routedDryRunRows = await resolveDryRunRoutingFn(deps, dryRunRows, options, agent);
|
|
1748
|
+
const routedByKey = new Map(routedDryRunRows.map((r) => [r.key, r]));
|
|
1332
1749
|
for (const key of options.keys) {
|
|
1333
1750
|
const branch = resolveBranchForTicket(key, options.branchOverrides);
|
|
1334
|
-
|
|
1751
|
+
const routedRow = routedByKey.get(key);
|
|
1752
|
+
const modelAlias = routedRow?.modelAlias ?? null;
|
|
1753
|
+
for (const line of buildDryRunDetailLines(agent, key, branch, deps.platform, deps.env, options.baseBranch, options.autoApprove, modelAlias)) {
|
|
1335
1754
|
log(line);
|
|
1336
1755
|
}
|
|
1756
|
+
log(`DRY-RUN: model routing: ${formatModelRoutingLine(routedRow ?? { key, branch, status: "dry-run" }, agent)}`);
|
|
1757
|
+
if (modelAlias == null && routedRow?.modelRoutingReason) {
|
|
1758
|
+
log(`DRY-RUN: ${routedRow.modelRoutingReason}`);
|
|
1759
|
+
}
|
|
1337
1760
|
}
|
|
1338
1761
|
log("");
|
|
1339
1762
|
}
|
|
1340
|
-
|
|
1763
|
+
// Thread the routing decision/warning sinks through to the orchestrator. The
|
|
1764
|
+
// dry-run path short-circuits inside orchestrate before any routing (the
|
|
1765
|
+
// preview above already resolved it), so no backend tiers are fetched twice.
|
|
1766
|
+
const result = await orchestrate(deps, options, {
|
|
1767
|
+
modelRoutingLog: log,
|
|
1768
|
+
modelRoutingWarningLog: errorLog,
|
|
1769
|
+
});
|
|
1341
1770
|
if (!result.ok) {
|
|
1342
1771
|
errorLog(`Error: ${result.error}`);
|
|
1343
1772
|
return 1;
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// AUTO-GENERATED — do not edit manually. Regenerate with: npm run build
|
|
2
|
-
export const VERSION = "0.2.
|
|
2
|
+
export const VERSION = "0.2.2";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bridge_gpt/mcp-server",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "Bridge API MCP server — exposes Jira endpoints as MCP tools for Claude Code agents",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -17,10 +17,11 @@
|
|
|
17
17
|
"LICENSE"
|
|
18
18
|
],
|
|
19
19
|
"scripts": {
|
|
20
|
-
"build": "node scripts/bundle-version.js && node scripts/bundle-pipelines.js && node scripts/bundle-commands.js && node scripts/bundle-agents.js && tsc",
|
|
20
|
+
"build": "node scripts/bundle-version.js && node scripts/bundle-readme.js && node scripts/bundle-pipelines.js && node scripts/bundle-commands.js && node scripts/bundle-agents.js && tsc",
|
|
21
|
+
"check:version-generated": "node scripts/bundle-version.js && node scripts/check-version-generated.js",
|
|
21
22
|
"postbuild": "node scripts/prepend-shebang.cjs",
|
|
22
23
|
"start": "node build/index.js",
|
|
23
|
-
"test": "node --test build/pipeline-utils.test.js build/update-check.test.js build/cli-upgrade.test.js build/decision-page-schema.test.js build/decision-page-template.test.js build/bundle-pipelines.test.js build/instructions-contract.test.js build/pipeline-orchestrator-persistence.test.js build/pipeline-orchestrator-execution.test.js build/pipeline-orchestrator-integration.test.js build/index-static.test.js build/index-resolvers.test.js build/index-project-root.test.js build/index-pipelines.test.js build/index.test.js build/bridge-config.test.js build/credential-store.test.js build/mcp-invoke.test.js build/mcp-provisioning.test.js build/third-party-mcp-targets.test.js build/git-ignore-utils.test.js build/credential-materialization.test.js build/mcp-registration-doctor.test.js build/secret-safety.test.js build/start-tickets.test.js build/start-tickets-base-branch.test.js build/agent-registry.test.js build/start-tickets-prereqs.test.js build/doctor.test.js build/package-static.test.js build/chain-utils.test.js build/chain-orchestrator.test.js build/scheduler-backends/types.test.js build/scheduler-backends/escaping.test.js build/scheduler-backends/launchd.test.js build/scheduler-backends/task-scheduler.test.js build/scheduler-backends/systemd-user.test.js build/scheduler-backends/at-fallback.test.js build/scheduler-backends/index.test.js build/agent-launchers/claude.test.js build/agent-launchers/index.test.js build/schedule-store.test.js build/schedule-run.test.js build/agent-capabilities/cli.test.js build/agent-capabilities/runner.test.js build/agent-capabilities/probes.test.js build/agent-capabilities/reporter.test.js && node --experimental-test-module-mocks --test build/index-heavy-read-truncation.test.js build/index-artifacts.test.js build/index-brainstorm-filenames.test.js build/index-output-path.test.js build/index-generate-decision-page.test.js build/index-generate-decision-page.integration.test.js",
|
|
24
|
+
"test": "node --test build/pipeline-utils.test.js build/update-check.test.js build/cli-upgrade.test.js build/decision-page-schema.test.js build/decision-page-template.test.js build/bundle-pipelines.test.js build/instructions-contract.test.js build/pipeline-orchestrator-persistence.test.js build/pipeline-orchestrator-execution.test.js build/pipeline-orchestrator-integration.test.js build/index-static.test.js build/index-resolvers.test.js build/index-project-root.test.js build/index-pipelines.test.js build/index.test.js build/bridge-config.test.js build/credential-store.test.js build/mcp-invoke.test.js build/mcp-provisioning.test.js build/third-party-mcp-targets.test.js build/git-ignore-utils.test.js build/credential-materialization.test.js build/mcp-registration-doctor.test.js build/secret-safety.test.js build/start-tickets.test.js build/start-tickets-base-branch.test.js build/agent-registry.test.js build/agent-registry.model-routing.test.js build/start-tickets.shell-model-routing.test.js build/start-tickets.bridge-api-model-routing.test.js build/start-tickets.tier-fetch-model-routing.test.js build/start-tickets.resolve-model-routing.test.js build/start-tickets.orchestrate-model-routing.test.js build/start-tickets-prereqs.test.js build/doctor.test.js build/package-static.test.js build/chain-utils.test.js build/chain-orchestrator.test.js build/scheduler-backends/types.test.js build/scheduler-backends/escaping.test.js build/scheduler-backends/launchd.test.js build/scheduler-backends/task-scheduler.test.js build/scheduler-backends/systemd-user.test.js build/scheduler-backends/at-fallback.test.js build/scheduler-backends/index.test.js build/command-catalog.test.js build/scheduled-prompt.test.js build/agent-launchers/claude.test.js build/agent-launchers/cursor.test.js build/agent-launchers/index.test.js build/schedule-store.test.js build/schedule-run.test.js build/agent-capabilities/cli.test.js build/agent-capabilities/runner.test.js build/agent-capabilities/probes.test.js build/agent-capabilities/reporter.test.js && node --experimental-test-module-mocks --test build/index-heavy-read-truncation.test.js build/index-artifacts.test.js build/index-brainstorm-filenames.test.js build/index-output-path.test.js build/index-generate-decision-page.test.js build/index-generate-decision-page.integration.test.js",
|
|
24
25
|
"test:integration": "node --test build/integration/refresh-main.integration.test.js build/integration/start-tickets.integration.test.js build/integration/doctor.integration.test.js build/integration/agent-capabilities.integration.test.js",
|
|
25
26
|
"prepublishOnly": "npm run build && node scripts/verify-shebang.cjs"
|
|
26
27
|
},
|