@kynver-app/runtime 0.1.17 → 0.1.19

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/dist/index.js CHANGED
@@ -10,6 +10,10 @@ function fail(message) {
10
10
  console.error(message);
11
11
  process.exit(1);
12
12
  }
13
+ function hiddenSpawnOptions(opts) {
14
+ if (process.platform !== "win32") return opts;
15
+ return { windowsHide: true, ...opts };
16
+ }
13
17
  function required(value, name) {
14
18
  if (!value) fail(`missing ${name}`);
15
19
  return value;
@@ -393,12 +397,12 @@ var DEFAULT_CRITICAL_FREE_BYTES = 15 * 1024 * 1024 * 1024;
393
397
  var DEFAULT_MAX_USED_PERCENT = 80;
394
398
  var DEFAULT_HARD_MAX_USED_PERCENT = 90;
395
399
  function observeRunnerDiskGate(input = {}) {
396
- const path16 = input.diskPath?.trim() || "/";
400
+ const path17 = input.diskPath?.trim() || "/";
397
401
  const warnBelowBytes = input.diskFreeWarnBytes ?? DEFAULT_WARN_FREE_BYTES;
398
402
  const criticalBelowBytes = input.diskFreeCriticalBytes ?? DEFAULT_CRITICAL_FREE_BYTES;
399
403
  const maxUsedPercent = input.diskMaxUsedPercent ?? DEFAULT_MAX_USED_PERCENT;
400
404
  const hardMaxUsedPercent = input.diskHardMaxUsedPercent ?? DEFAULT_HARD_MAX_USED_PERCENT;
401
- const stats = statfsSync(path16);
405
+ const stats = statfsSync(path17);
402
406
  const freeBytes = Number(stats.bavail) * Number(stats.bsize);
403
407
  const totalBytes = Number(stats.blocks) * Number(stats.bsize);
404
408
  const usedPercent = totalBytes > 0 ? (totalBytes - freeBytes) / totalBytes * 100 : 100;
@@ -418,7 +422,7 @@ function observeRunnerDiskGate(input = {}) {
418
422
  }
419
423
  return {
420
424
  ok,
421
- path: path16,
425
+ path: path17,
422
426
  freeBytes,
423
427
  totalBytes,
424
428
  usedPercent,
@@ -605,6 +609,52 @@ function summarizeEvent(event) {
605
609
  return void 0;
606
610
  }
607
611
 
612
+ // src/exit-classify.ts
613
+ var FAILURE_PATTERNS = [
614
+ {
615
+ test: /\b(?:invalid|unknown|unsupported|unrecognized)\b[^.\n]*\bmodel\b/i,
616
+ label: "provider rejected the requested model"
617
+ },
618
+ {
619
+ test: /\bmodel\b[^.\n]*\b(?:not\s+(?:found|supported|available|recognized|valid)|is\s+not\s+valid|does\s+not\s+exist)/i,
620
+ label: "provider rejected the requested model"
621
+ },
622
+ {
623
+ test: /\b(?:did you mean|available models|choose (?:a|one of)|supported models)\b/i,
624
+ label: "provider rejected the requested model"
625
+ },
626
+ {
627
+ test: /model preflight failed/i,
628
+ label: "model/provider preflight failed"
629
+ },
630
+ {
631
+ test: /\b(?:command not found|ENOENT|is the .*CLI on PATH|executable not found|no such file or directory)\b/i,
632
+ label: "provider CLI is missing or not on PATH"
633
+ },
634
+ {
635
+ test: /\bfailed to spawn\b/i,
636
+ label: "provider failed to spawn the worker process"
637
+ },
638
+ {
639
+ test: /\b(?:not logged in|unauthorized|authentication (?:failed|required)|invalid api key|missing api key|401)\b/i,
640
+ label: "provider authentication failed"
641
+ }
642
+ ];
643
+ function tidy(errorText, max = 240) {
644
+ const oneLine2 = errorText.replace(/\s+/g, " ").trim();
645
+ return oneLine2.length > max ? `${oneLine2.slice(0, max - 1)}\u2026` : oneLine2;
646
+ }
647
+ function classifyExitFailure(errorText) {
648
+ const text = (errorText ?? "").trim();
649
+ if (!text) return null;
650
+ for (const pattern of FAILURE_PATTERNS) {
651
+ if (pattern.test.test(text)) {
652
+ return { blocked: true, reason: `${pattern.label}: ${tidy(text)}` };
653
+ }
654
+ }
655
+ return null;
656
+ }
657
+
608
658
  // src/git.ts
609
659
  import { spawnSync } from "node:child_process";
610
660
  function git(cwd, args, options = {}) {
@@ -720,7 +770,15 @@ var STALE_MS = 6e5;
720
770
  function computeAttention(input) {
721
771
  const now = Date.now();
722
772
  if (input.finalResult) return { state: "done", reason: "final result recorded" };
723
- if (!input.alive) return { state: "needs_attention", reason: "process exited without a final result" };
773
+ if (!input.alive) {
774
+ const classified = classifyExitFailure(input.error);
775
+ if (classified) return { state: "blocked", reason: classified.reason };
776
+ const tail = input.error?.trim();
777
+ return {
778
+ state: "needs_attention",
779
+ reason: tail ? `process exited without a final result: ${tail}` : "process exited without a final result"
780
+ };
781
+ }
724
782
  if (input.heartbeatBlocker) {
725
783
  return { state: "blocked", reason: `worker heartbeat reported blocker: ${input.heartbeatBlocker}` };
726
784
  }
@@ -750,6 +808,7 @@ function computeWorkerStatus(worker, options = {}) {
750
808
  fileMtime(worker.stderrPath),
751
809
  fileMtime(worker.heartbeatPath)
752
810
  ]);
811
+ const error = parsed.error || (!alive && !parsed.finalResult ? tailFile(worker.stderrPath, 10).trim() || void 0 : void 0);
753
812
  const attention = computeAttention({
754
813
  alive,
755
814
  finalResult: parsed.finalResult,
@@ -758,7 +817,8 @@ function computeWorkerStatus(worker, options = {}) {
758
817
  heartbeatBytes,
759
818
  lastActivityAt,
760
819
  heartbeatBlocker: heartbeat.heartbeatBlocker,
761
- startedAt: worker.startedAt
820
+ startedAt: worker.startedAt,
821
+ error
762
822
  });
763
823
  return {
764
824
  runId: worker.runId,
@@ -783,7 +843,7 @@ function computeWorkerStatus(worker, options = {}) {
783
843
  lastHeartbeatSummary: heartbeat.lastHeartbeatSummary,
784
844
  heartbeatBlocker: heartbeat.heartbeatBlocker,
785
845
  finalResult: parsed.finalResult,
786
- error: parsed.error || (!alive && !parsed.finalResult ? tailFile(worker.stderrPath, 10).trim() || void 0 : void 0),
846
+ error,
787
847
  changedFiles,
788
848
  gitAncestry
789
849
  };
@@ -908,8 +968,8 @@ function observeRunnerResourceGate(input) {
908
968
  }
909
969
 
910
970
  // src/supervisor.ts
911
- import { existsSync as existsSync9, mkdirSync as mkdirSync3 } from "node:fs";
912
- import path9 from "node:path";
971
+ import { existsSync as existsSync10, mkdirSync as mkdirSync3 } from "node:fs";
972
+ import path10 from "node:path";
913
973
 
914
974
  // src/prompt.ts
915
975
  function buildPrompt(input) {
@@ -943,10 +1003,74 @@ function buildPrompt(input) {
943
1003
  // src/providers/claude.ts
944
1004
  import { closeSync, openSync } from "node:fs";
945
1005
  import { spawn } from "node:child_process";
1006
+
1007
+ // src/providers/model-preflight.ts
1008
+ var REASONING_SUFFIX_RE = /-(?:thinking(?:-(?:high|medium|low|minimal|max|none))?|high|medium|low|minimal)$/i;
1009
+ function stripReasoningSuffix(model) {
1010
+ return model.replace(REASONING_SUFFIX_RE, "");
1011
+ }
1012
+ var FOREIGN_MODEL_RE = /^(?:gpt-|gpt5|o1|o3|o4|gemini-|grok-|composer|deepseek|llama|mistral|qwen|command-)/i;
1013
+ function looksLikeClaudeModel(model) {
1014
+ return /^claude[-_]/i.test(model) || /^(?:opus|sonnet|haiku)\b/i.test(model);
1015
+ }
1016
+ function preflightClaudeModel(model, defaultModel) {
1017
+ const requested = (model ?? "").trim();
1018
+ if (!requested) {
1019
+ return { ok: true, model: defaultModel, normalized: false };
1020
+ }
1021
+ const stripped = stripReasoningSuffix(requested).trim();
1022
+ const launch = stripped || defaultModel;
1023
+ if (FOREIGN_MODEL_RE.test(launch) || !looksLikeClaudeModel(launch) && launch !== defaultModel) {
1024
+ return {
1025
+ ok: false,
1026
+ model: requested,
1027
+ normalized: false,
1028
+ requested,
1029
+ note: `model "${requested}" is not a Claude model \u2014 the "claude" provider drives the Claude CLI, which only accepts claude-* model ids (got "${launch}"). Pick a Claude model or switch the worker provider.`
1030
+ };
1031
+ }
1032
+ if (launch !== requested) {
1033
+ return {
1034
+ ok: true,
1035
+ model: launch,
1036
+ normalized: true,
1037
+ requested,
1038
+ note: `normalized model "${requested}" \u2192 "${launch}" (the Claude CLI rejects reasoning-effort suffixes)`
1039
+ };
1040
+ }
1041
+ return { ok: true, model: launch, normalized: false };
1042
+ }
1043
+ function preflightCursorModel(model, defaultModel) {
1044
+ const requested = (model ?? "").trim();
1045
+ if (!requested) {
1046
+ return { ok: true, model: defaultModel, normalized: false };
1047
+ }
1048
+ if (looksLikeClaudeModel(requested)) {
1049
+ return {
1050
+ ok: false,
1051
+ model: requested,
1052
+ normalized: false,
1053
+ requested,
1054
+ note: `model "${requested}" is a Claude model but the worker provider is "cursor". Switch the provider to "claude" or pick a Cursor model.`
1055
+ };
1056
+ }
1057
+ return { ok: true, model: requested, normalized: false };
1058
+ }
1059
+
1060
+ // src/providers/claude.ts
1061
+ var CLAUDE_DEFAULT_MODEL = "claude-opus-4-7";
946
1062
  var claudeProvider = {
947
1063
  name: "claude",
1064
+ defaultModel: CLAUDE_DEFAULT_MODEL,
1065
+ preflightModel(model) {
1066
+ return preflightClaudeModel(model, CLAUDE_DEFAULT_MODEL);
1067
+ },
948
1068
  start(opts) {
949
- const model = opts.model || "claude-opus-4-7";
1069
+ const preflight = preflightClaudeModel(opts.model, CLAUDE_DEFAULT_MODEL);
1070
+ if (!preflight.ok) {
1071
+ throw new Error(`claude provider model preflight failed: ${preflight.note}`);
1072
+ }
1073
+ const model = preflight.model;
950
1074
  const stdoutFd = openSync(opts.stdoutPath, "a");
951
1075
  const stderrFd = openSync(opts.stderrPath, "a");
952
1076
  const child = spawn(
@@ -963,12 +1087,12 @@ var claudeProvider = {
963
1087
  "--include-partial-messages",
964
1088
  opts.prompt
965
1089
  ],
966
- {
1090
+ hiddenSpawnOptions({
967
1091
  cwd: opts.worktreePath,
968
1092
  detached: true,
969
1093
  stdio: ["ignore", stdoutFd, stderrFd],
970
1094
  env: scrubClaudeEnv(process.env)
971
- }
1095
+ })
972
1096
  );
973
1097
  closeSync(stdoutFd);
974
1098
  closeSync(stderrFd);
@@ -981,33 +1105,80 @@ var claudeProvider = {
981
1105
  };
982
1106
 
983
1107
  // src/providers/cursor.ts
984
- import { closeSync as closeSync2, existsSync as existsSync7, openSync as openSync2, readdirSync as readdirSync3 } from "node:fs";
1108
+ import { closeSync as closeSync2, existsSync as existsSync8, openSync as openSync2 } from "node:fs";
985
1109
  import { spawn as spawn2 } from "node:child_process";
1110
+ import path7 from "node:path";
1111
+
1112
+ // src/providers/cursor-windows.ts
1113
+ import { existsSync as existsSync7, readdirSync as readdirSync3 } from "node:fs";
986
1114
  import path6 from "node:path";
987
- var DEFAULT_CURSOR_MODEL = "composer-2.5";
988
- function latestVersionDir(versionsRoot) {
1115
+ var CURSOR_VERSION_DIR = /^\d{4}\.\d{1,2}\.\d{1,2}-[a-f0-9]+$/i;
1116
+ function parseCursorVersionSortKey(versionName) {
1117
+ const datePart = versionName.split("-")[0];
1118
+ const parts = datePart.split(".");
1119
+ if (parts.length !== 3) return null;
1120
+ const [year, month, day] = parts;
1121
+ if (!year || !month || !day) return null;
1122
+ return Number(`${year}${month.padStart(2, "0")}${day.padStart(2, "0")}`);
1123
+ }
1124
+ function pickLatestCursorVersionDir(agentRoot) {
1125
+ const versionsRoot = path6.join(agentRoot, "versions");
989
1126
  if (!existsSync7(versionsRoot)) return null;
990
- const versions = readdirSync3(versionsRoot, { withFileTypes: true }).filter((entry) => entry.isDirectory() && /^\d{4}\.\d/.test(entry.name)).map((entry) => entry.name).sort((a, b) => b.localeCompare(a));
991
- return versions[0] ? path6.join(versionsRoot, versions[0]) : null;
992
- }
993
- function resolveBundledCursor(versionDir) {
1127
+ let bestDir = null;
1128
+ let bestKey = -1;
1129
+ for (const entry of readdirSync3(versionsRoot, { withFileTypes: true })) {
1130
+ if (!entry.isDirectory() || !CURSOR_VERSION_DIR.test(entry.name)) continue;
1131
+ const key = parseCursorVersionSortKey(entry.name);
1132
+ if (key == null || key <= bestKey) continue;
1133
+ bestKey = key;
1134
+ bestDir = path6.join(versionsRoot, entry.name);
1135
+ }
1136
+ return bestDir;
1137
+ }
1138
+ function resolveWindowsCursorBundled(agentRoot) {
1139
+ const root = agentRoot?.trim() || path6.join(process.env.LOCALAPPDATA || "", "cursor-agent");
1140
+ const directNode = path6.join(root, "node.exe");
1141
+ const directIndex = path6.join(root, "index.js");
1142
+ if (existsSync7(directNode) && existsSync7(directIndex)) {
1143
+ return { nodeExe: directNode, indexJs: directIndex, versionDir: root };
1144
+ }
1145
+ const versionDir = pickLatestCursorVersionDir(root);
1146
+ if (!versionDir) return null;
994
1147
  const nodeExe = path6.join(versionDir, "node.exe");
995
1148
  const indexJs = path6.join(versionDir, "index.js");
996
1149
  if (!existsSync7(nodeExe) || !existsSync7(indexJs)) return null;
997
- return { executable: nodeExe, prefixArgs: [indexJs], shell: false, detached: true };
1150
+ return { nodeExe, indexJs, versionDir };
998
1151
  }
999
- function resolveWindowsCursorSpawn(agentBin) {
1000
- const agentRoot = path6.dirname(agentBin);
1001
- const direct = resolveBundledCursor(agentRoot);
1002
- if (direct) return direct;
1003
- const versionDir = latestVersionDir(path6.join(agentRoot, "versions"));
1004
- return versionDir ? resolveBundledCursor(versionDir) : null;
1152
+
1153
+ // src/providers/cursor.ts
1154
+ var DEFAULT_CURSOR_MODEL = "composer-2.5";
1155
+ function bundledSpawnTarget(nodeExe, indexJs, versionDir) {
1156
+ return {
1157
+ executable: nodeExe,
1158
+ prefixArgs: [indexJs],
1159
+ shell: false,
1160
+ detached: true,
1161
+ bundledVersionDir: versionDir
1162
+ };
1005
1163
  }
1006
1164
  function resolveCursorSpawn(agentBin) {
1007
- if (process.platform === "win32" && /\.(cmd|bat)$/i.test(agentBin)) {
1008
- const bundled = resolveWindowsCursorSpawn(agentBin);
1009
- if (bundled) return bundled;
1010
- return { executable: agentBin, prefixArgs: [], shell: true, detached: false };
1165
+ if (process.platform === "win32") {
1166
+ const isCursorWrapper = /\.(cmd|bat)$/i.test(agentBin);
1167
+ const isBundledNode = /node\.exe$/i.test(agentBin) && existsSync8(path7.join(path7.dirname(agentBin), "index.js"));
1168
+ const isDefaultShim = agentBin === "agent";
1169
+ if (isCursorWrapper || isBundledNode || isDefaultShim) {
1170
+ const bundled = isCursorWrapper ? resolveWindowsCursorBundled(path7.dirname(agentBin)) : isBundledNode ? {
1171
+ nodeExe: agentBin,
1172
+ indexJs: path7.join(path7.dirname(agentBin), "index.js"),
1173
+ versionDir: path7.dirname(agentBin)
1174
+ } : resolveWindowsCursorBundled();
1175
+ if (bundled) {
1176
+ return bundledSpawnTarget(bundled.nodeExe, bundled.indexJs, bundled.versionDir);
1177
+ }
1178
+ throw new Error(
1179
+ "Cursor Agent on Windows has no headless bundled node.exe under %LOCALAPPDATA%\\cursor-agent\\versions\\\u2026. Run `agent login` / update Cursor Agent CLI, use `--provider claude`, or set KYNVER_CURSOR_AGENT_ROOT to the cursor-agent folder."
1180
+ );
1181
+ }
1011
1182
  }
1012
1183
  return { executable: agentBin, prefixArgs: [], shell: false, detached: true };
1013
1184
  }
@@ -1015,15 +1186,35 @@ function resolveAgentBin() {
1015
1186
  const configured = process.env.KYNVER_CURSOR_AGENT_BIN?.trim() || process.env.CURSOR_AGENT_BIN?.trim();
1016
1187
  if (configured) return configured;
1017
1188
  if (process.platform === "win32") {
1018
- const localAgent = path6.join(process.env.LOCALAPPDATA || "", "cursor-agent", "agent.cmd");
1019
- if (existsSync7(localAgent)) return localAgent;
1189
+ const bundled = resolveWindowsCursorBundled(
1190
+ process.env.KYNVER_CURSOR_AGENT_ROOT?.trim() || void 0
1191
+ );
1192
+ if (bundled) return bundled.nodeExe;
1193
+ const localAgent = path7.join(process.env.LOCALAPPDATA || "", "cursor-agent", "agent.cmd");
1194
+ if (existsSync8(localAgent)) return localAgent;
1020
1195
  }
1021
1196
  return "agent";
1022
1197
  }
1198
+ function cursorWorkerEnv(agentBin, spawnTarget) {
1199
+ return {
1200
+ ...process.env,
1201
+ CI: "1",
1202
+ NO_COLOR: "1",
1203
+ ...spawnTarget.bundledVersionDir ? { CURSOR_INVOKED_AS: path7.basename(agentBin) || "agent.cmd" } : {}
1204
+ };
1205
+ }
1023
1206
  var cursorProvider = {
1024
1207
  name: "cursor",
1208
+ defaultModel: DEFAULT_CURSOR_MODEL,
1209
+ preflightModel(model) {
1210
+ return preflightCursorModel(model, DEFAULT_CURSOR_MODEL);
1211
+ },
1025
1212
  start(opts) {
1026
- const model = opts.model || DEFAULT_CURSOR_MODEL;
1213
+ const preflight = preflightCursorModel(opts.model, DEFAULT_CURSOR_MODEL);
1214
+ if (!preflight.ok) {
1215
+ throw new Error(`cursor provider model preflight failed: ${preflight.note}`);
1216
+ }
1217
+ const model = preflight.model;
1027
1218
  const stdoutFd = openSync2(opts.stdoutPath, "a");
1028
1219
  const stderrFd = openSync2(opts.stderrPath, "a");
1029
1220
  const agentBin = resolveAgentBin();
@@ -1044,16 +1235,13 @@ var cursorProvider = {
1044
1235
  model,
1045
1236
  opts.prompt
1046
1237
  ],
1047
- {
1238
+ hiddenSpawnOptions({
1048
1239
  cwd: opts.worktreePath,
1049
1240
  detached: spawnTarget.detached,
1050
1241
  shell: spawnTarget.shell,
1051
1242
  stdio: ["ignore", stdoutFd, stderrFd],
1052
- env: {
1053
- ...process.env,
1054
- ...spawnTarget.prefixArgs.length > 0 ? { CURSOR_INVOKED_AS: path6.basename(agentBin) } : {}
1055
- }
1056
- }
1243
+ env: cursorWorkerEnv(agentBin, spawnTarget)
1244
+ })
1057
1245
  );
1058
1246
  closeSync2(stdoutFd);
1059
1247
  closeSync2(stderrFd);
@@ -1085,12 +1273,12 @@ function resolveWorkerProvider(name) {
1085
1273
 
1086
1274
  // src/auto-complete.ts
1087
1275
  import { spawn as spawn3 } from "node:child_process";
1088
- import { existsSync as existsSync8, openSync as openSync3, closeSync as closeSync3 } from "node:fs";
1089
- import path8 from "node:path";
1276
+ import { existsSync as existsSync9, openSync as openSync3, closeSync as closeSync3 } from "node:fs";
1277
+ import path9 from "node:path";
1090
1278
  import { fileURLToPath } from "node:url";
1091
1279
 
1092
1280
  // src/worker-ops.ts
1093
- import path7 from "node:path";
1281
+ import path8 from "node:path";
1094
1282
  async function postCompletion(url, secret, body) {
1095
1283
  const res = await fetch(url, {
1096
1284
  method: "POST",
@@ -1213,7 +1401,7 @@ async function completeWorker(args) {
1213
1401
  function workerStatus(args) {
1214
1402
  const worker = loadWorker(String(args.run), String(args.name));
1215
1403
  const status = computeWorkerStatus(worker);
1216
- writeJson(path7.join(worker.workerDir, "last-status.json"), status);
1404
+ writeJson(path8.join(worker.workerDir, "last-status.json"), status);
1217
1405
  console.log(JSON.stringify(status, null, 2));
1218
1406
  }
1219
1407
  function runStatus(args) {
@@ -1221,7 +1409,7 @@ function runStatus(args) {
1221
1409
  const names = Object.keys(run.workers || {});
1222
1410
  const workers = names.map((name) => {
1223
1411
  const worker = readJson(
1224
- path7.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
1412
+ path8.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
1225
1413
  void 0
1226
1414
  );
1227
1415
  if (!worker) {
@@ -1257,7 +1445,7 @@ function runStatus(args) {
1257
1445
  needsAttention: workers.filter((w) => w.attention && w.attention !== "ok" && w.attention !== "done").map((w) => w.worker),
1258
1446
  workers
1259
1447
  };
1260
- writeJson(path7.join(runDirectory(run.id), "last-board.json"), board);
1448
+ writeJson(path8.join(runDirectory(run.id), "last-board.json"), board);
1261
1449
  console.log(JSON.stringify(board, null, 2));
1262
1450
  }
1263
1451
  function tailWorker(args) {
@@ -1396,12 +1584,12 @@ async function autoCompleteWorkerCli(raw) {
1396
1584
  }
1397
1585
  }
1398
1586
  function resolveDefaultCliPath() {
1399
- return path8.join(fileURLToPath(new URL(".", import.meta.url)), "cli.js");
1587
+ return path9.join(fileURLToPath(new URL(".", import.meta.url)), "cli.js");
1400
1588
  }
1401
1589
  function spawnCompletionSidecar(opts) {
1402
1590
  const cliPath = opts.cliPath ?? resolveDefaultCliPath();
1403
- if (!existsSync8(cliPath)) return void 0;
1404
- const logPath = path8.join(opts.workerDir, "auto-complete.log");
1591
+ if (!existsSync9(cliPath)) return void 0;
1592
+ const logPath = path9.join(opts.workerDir, "auto-complete.log");
1405
1593
  let logFd;
1406
1594
  try {
1407
1595
  logFd = openSync3(logPath, "a");
@@ -1427,11 +1615,15 @@ function spawnCompletionSidecar(opts) {
1427
1615
  if (opts.baseUrl) args.push("--base-url", opts.baseUrl);
1428
1616
  if (opts.secret) args.push("--secret", opts.secret);
1429
1617
  try {
1430
- const child = spawn3(nodeExecutable, args, {
1431
- detached: true,
1432
- stdio,
1433
- env: process.env
1434
- });
1618
+ const child = spawn3(
1619
+ nodeExecutable,
1620
+ args,
1621
+ hiddenSpawnOptions({
1622
+ detached: true,
1623
+ stdio,
1624
+ env: process.env
1625
+ })
1626
+ );
1435
1627
  if (logFd !== void 0) closeSync3(logFd);
1436
1628
  child.unref();
1437
1629
  return { pid: child.pid, logPath, cliPath };
@@ -1455,31 +1647,44 @@ function spawnWorkerProcess(run, opts) {
1455
1647
  const name = safeSlug(rawName);
1456
1648
  if (run.workers?.[name]) throw new Error(`worker already exists in run ${run.id}: ${name}`);
1457
1649
  if (!opts.task) throw new Error(`missing task text for worker ${name}`);
1650
+ const provider = resolveWorkerProvider(opts.provider);
1651
+ let launchModel = opts.model;
1652
+ if (provider.preflightModel) {
1653
+ const preflight = provider.preflightModel(opts.model);
1654
+ if (!preflight.ok) {
1655
+ throw new Error(
1656
+ `model preflight failed for provider "${provider.name}": ${preflight.note ?? "invalid model/provider combination"}`
1657
+ );
1658
+ }
1659
+ if (preflight.normalized) {
1660
+ console.error(`[supervisor] ${name}: ${preflight.note}`);
1661
+ }
1662
+ launchModel = preflight.model;
1663
+ }
1458
1664
  const { worktreesDir } = getPaths();
1459
- const workerDir = path9.join(runDirectory(run.id), "workers", name);
1665
+ const workerDir = path10.join(runDirectory(run.id), "workers", name);
1460
1666
  mkdirSync3(workerDir, { recursive: true });
1461
- const worktreePath = path9.join(worktreesDir, run.id, name);
1667
+ const worktreePath = path10.join(worktreesDir, run.id, name);
1462
1668
  const branch = opts.branch || `agent/${run.id}/${name}`;
1463
- if (existsSync9(worktreePath)) throw new Error(`worktree path already exists: ${worktreePath}`);
1669
+ if (existsSync10(worktreePath)) throw new Error(`worktree path already exists: ${worktreePath}`);
1464
1670
  git(run.repo, ["fetch", "origin", "--prune"], { allowFailure: true });
1465
1671
  git(run.repo, ["worktree", "add", "-b", branch, worktreePath, run.baseCommit], { throwError: true });
1466
- const stdoutPath = path9.join(workerDir, "stdout.jsonl");
1467
- const stderrPath = path9.join(workerDir, "stderr.log");
1468
- const heartbeatPath = path9.join(workerDir, "heartbeat.jsonl");
1672
+ const stdoutPath = path10.join(workerDir, "stdout.jsonl");
1673
+ const stderrPath = path10.join(workerDir, "stderr.log");
1674
+ const heartbeatPath = path10.join(workerDir, "heartbeat.jsonl");
1469
1675
  const prompt = buildPrompt({
1470
1676
  task: opts.task,
1471
1677
  ownedPaths: opts.ownedPaths || [],
1472
1678
  worktreePath,
1473
1679
  heartbeatPath
1474
1680
  });
1475
- const provider = resolveWorkerProvider(opts.provider);
1476
1681
  let started;
1477
1682
  try {
1478
1683
  started = provider.start({
1479
1684
  name,
1480
1685
  task: opts.task,
1481
1686
  ownedPaths: opts.ownedPaths,
1482
- model: opts.model,
1687
+ model: launchModel,
1483
1688
  branch,
1484
1689
  worktreePath,
1485
1690
  workerDir,
@@ -1493,7 +1698,7 @@ function spawnWorkerProcess(run, opts) {
1493
1698
  git(run.repo, ["branch", "-D", branch], { allowFailure: true });
1494
1699
  throw error;
1495
1700
  }
1496
- const model = started.model || opts.model || "claude-opus-4-7";
1701
+ const model = started.model || launchModel || provider.defaultModel || "claude-opus-4-7";
1497
1702
  const worker = {
1498
1703
  name,
1499
1704
  runId: run.id,
@@ -1515,7 +1720,7 @@ function spawnWorkerProcess(run, opts) {
1515
1720
  startedAt: (/* @__PURE__ */ new Date()).toISOString()
1516
1721
  };
1517
1722
  saveWorker(run.id, worker);
1518
- run.workers = { ...run.workers || {}, [name]: { workerDir, statusPath: path9.join(workerDir, "worker.json") } };
1723
+ run.workers = { ...run.workers || {}, [name]: { workerDir, statusPath: path10.join(workerDir, "worker.json") } };
1519
1724
  run.status = "running";
1520
1725
  saveRun(run);
1521
1726
  if (worker.agentOsId && worker.taskId) {
@@ -1738,7 +1943,7 @@ function redactHarness(text, secret) {
1738
1943
  }
1739
1944
 
1740
1945
  // src/validate.ts
1741
- import path10 from "node:path";
1946
+ import path11 from "node:path";
1742
1947
  var RUN_ID_RE = /^[a-z0-9][a-z0-9._-]{0,127}$/i;
1743
1948
  var WORKER_NAME_RE = /^[a-z0-9][a-z0-9._-]{0,63}$/i;
1744
1949
  function validateRunId(runId) {
@@ -1752,15 +1957,15 @@ function validateWorkerName(name) {
1752
1957
  return trimmed;
1753
1958
  }
1754
1959
  function validateRepo(repo) {
1755
- const resolved = path10.resolve(repo);
1960
+ const resolved = path11.resolve(repo);
1756
1961
  if (resolved.includes("..")) throw new Error("repo path must not contain .. segments");
1757
1962
  return resolved;
1758
1963
  }
1759
1964
  function validateOwnedPaths(repoRoot, ownedPaths) {
1760
1965
  return ownedPaths.map((owned) => {
1761
- const resolved = path10.resolve(repoRoot, owned);
1762
- const rel = path10.relative(repoRoot, resolved);
1763
- if (rel.startsWith("..") || path10.isAbsolute(rel)) {
1966
+ const resolved = path11.resolve(repoRoot, owned);
1967
+ const rel = path11.relative(repoRoot, resolved);
1968
+ if (rel.startsWith("..") || path11.isAbsolute(rel)) {
1764
1969
  throw new Error(`owned path escapes repo: ${owned}`);
1765
1970
  }
1766
1971
  return resolved;
@@ -1772,14 +1977,14 @@ function validateTailLines(lines) {
1772
1977
  }
1773
1978
 
1774
1979
  // src/worktree.ts
1775
- import { existsSync as existsSync10, mkdirSync as mkdirSync4 } from "node:fs";
1776
- import path11 from "node:path";
1980
+ import { existsSync as existsSync11, mkdirSync as mkdirSync4 } from "node:fs";
1981
+ import path12 from "node:path";
1777
1982
  function createRun(args) {
1778
1983
  const repo = validateRepo(required(String(args.repo || ""), "--repo"));
1779
1984
  ensureGitRepo(repo);
1780
1985
  const id = args.id ? validateRunId(String(args.id)) : timestampSlug(String(args.name || "run"));
1781
1986
  const dir = runDirectory(id);
1782
- if (existsSync10(dir)) failExists(`run already exists: ${id}`);
1987
+ if (existsSync11(dir)) failExists(`run already exists: ${id}`);
1783
1988
  mkdirSync4(dir, { recursive: true });
1784
1989
  const base = String(args.base || "origin/main");
1785
1990
  const baseCommit = git(repo, ["rev-parse", base]).trim();
@@ -1793,12 +1998,12 @@ function createRun(args) {
1793
1998
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
1794
1999
  workers: {}
1795
2000
  };
1796
- writeJson(path11.join(dir, "run.json"), run);
2001
+ writeJson(path12.join(dir, "run.json"), run);
1797
2002
  console.log(JSON.stringify({ runId: id, runDir: dir, repo, base, baseCommit }, null, 2));
1798
2003
  }
1799
2004
  function listRuns() {
1800
2005
  const { runsDir } = getPaths();
1801
- const rows = listRunIds(runsDir).map((id) => readJson(path11.join(runDirectory(id), "run.json"), void 0)).filter(Boolean).map((run) => ({
2006
+ const rows = listRunIds(runsDir).map((id) => readJson(path12.join(runDirectory(id), "run.json"), void 0)).filter(Boolean).map((run) => ({
1802
2007
  id: run.id,
1803
2008
  name: run.name,
1804
2009
  status: run.status,
@@ -1813,7 +2018,7 @@ function failExists(message) {
1813
2018
  }
1814
2019
 
1815
2020
  // src/sweep.ts
1816
- import path12 from "node:path";
2021
+ import path13 from "node:path";
1817
2022
  async function sweepRun(args) {
1818
2023
  const pipeline = args.pipeline === true || args.pipeline === "true";
1819
2024
  try {
@@ -1825,7 +2030,7 @@ async function sweepRun(args) {
1825
2030
  const releasedLocalOrphans = [];
1826
2031
  for (const name of Object.keys(run.workers || {})) {
1827
2032
  const worker = readJson(
1828
- path12.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
2033
+ path13.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
1829
2034
  void 0
1830
2035
  );
1831
2036
  if (!worker || !worker.dispatched || !worker.taskId) continue;
@@ -1872,10 +2077,10 @@ import { mkdirSync as mkdirSync5, realpathSync } from "node:fs";
1872
2077
  import { fileURLToPath as fileURLToPath2 } from "node:url";
1873
2078
 
1874
2079
  // src/pipeline-tick.ts
1875
- import path15 from "node:path";
2080
+ import path16 from "node:path";
1876
2081
 
1877
2082
  // src/finalize.ts
1878
- import path13 from "node:path";
2083
+ import path14 from "node:path";
1879
2084
  var ACTIVE_RUN_STATUSES = /* @__PURE__ */ new Set(["running", "dispatching", "pending", "queued"]);
1880
2085
  function terminalStatusFor(run) {
1881
2086
  const names = Object.keys(run.workers || {});
@@ -1885,7 +2090,7 @@ function terminalStatusFor(run) {
1885
2090
  let anyCompletionBlocked = false;
1886
2091
  for (const name of names) {
1887
2092
  const worker = readJson(
1888
- path13.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
2093
+ path14.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
1889
2094
  void 0
1890
2095
  );
1891
2096
  if (!worker) continue;
@@ -1918,7 +2123,7 @@ function finalizeStaleRuns() {
1918
2123
  }
1919
2124
 
1920
2125
  // src/plan-progress-daemon-sync.ts
1921
- import path14 from "node:path";
2126
+ import path15 from "node:path";
1922
2127
 
1923
2128
  // src/plan-progress-sync.ts
1924
2129
  async function syncPlanProgress(args) {
@@ -1942,7 +2147,7 @@ async function syncActiveWorkerPlanProgress(runId, args) {
1942
2147
  const outcomes = [];
1943
2148
  for (const name of Object.keys(run.workers || {})) {
1944
2149
  const worker = readJson(
1945
- path14.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
2150
+ path15.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
1946
2151
  void 0
1947
2152
  );
1948
2153
  if (!worker?.dispatched || !worker.taskId) continue;
@@ -1996,7 +2201,7 @@ async function completeFinishedWorkers(runId, args) {
1996
2201
  const outcomes = [];
1997
2202
  for (const name of Object.keys(run.workers || {})) {
1998
2203
  const worker = readJson(
1999
- path15.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
2204
+ path16.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
2000
2205
  void 0
2001
2206
  );
2002
2207
  if (!worker?.taskId) continue;