@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/cli.js CHANGED
@@ -16,6 +16,10 @@ function fail(message) {
16
16
  console.error(message);
17
17
  process.exit(1);
18
18
  }
19
+ function hiddenSpawnOptions(opts) {
20
+ if (process.platform !== "win32") return opts;
21
+ return { windowsHide: true, ...opts };
22
+ }
19
23
  function required(value, name) {
20
24
  if (!value) fail(`missing ${name}`);
21
25
  return value;
@@ -392,12 +396,12 @@ var DEFAULT_CRITICAL_FREE_BYTES = 15 * 1024 * 1024 * 1024;
392
396
  var DEFAULT_MAX_USED_PERCENT = 80;
393
397
  var DEFAULT_HARD_MAX_USED_PERCENT = 90;
394
398
  function observeRunnerDiskGate(input = {}) {
395
- const path16 = input.diskPath?.trim() || "/";
399
+ const path17 = input.diskPath?.trim() || "/";
396
400
  const warnBelowBytes = input.diskFreeWarnBytes ?? DEFAULT_WARN_FREE_BYTES;
397
401
  const criticalBelowBytes = input.diskFreeCriticalBytes ?? DEFAULT_CRITICAL_FREE_BYTES;
398
402
  const maxUsedPercent = input.diskMaxUsedPercent ?? DEFAULT_MAX_USED_PERCENT;
399
403
  const hardMaxUsedPercent = input.diskHardMaxUsedPercent ?? DEFAULT_HARD_MAX_USED_PERCENT;
400
- const stats = statfsSync(path16);
404
+ const stats = statfsSync(path17);
401
405
  const freeBytes = Number(stats.bavail) * Number(stats.bsize);
402
406
  const totalBytes = Number(stats.blocks) * Number(stats.bsize);
403
407
  const usedPercent = totalBytes > 0 ? (totalBytes - freeBytes) / totalBytes * 100 : 100;
@@ -417,7 +421,7 @@ function observeRunnerDiskGate(input = {}) {
417
421
  }
418
422
  return {
419
423
  ok,
420
- path: path16,
424
+ path: path17,
421
425
  freeBytes,
422
426
  totalBytes,
423
427
  usedPercent,
@@ -604,6 +608,52 @@ function summarizeEvent(event) {
604
608
  return void 0;
605
609
  }
606
610
 
611
+ // src/exit-classify.ts
612
+ var FAILURE_PATTERNS = [
613
+ {
614
+ test: /\b(?:invalid|unknown|unsupported|unrecognized)\b[^.\n]*\bmodel\b/i,
615
+ label: "provider rejected the requested model"
616
+ },
617
+ {
618
+ test: /\bmodel\b[^.\n]*\b(?:not\s+(?:found|supported|available|recognized|valid)|is\s+not\s+valid|does\s+not\s+exist)/i,
619
+ label: "provider rejected the requested model"
620
+ },
621
+ {
622
+ test: /\b(?:did you mean|available models|choose (?:a|one of)|supported models)\b/i,
623
+ label: "provider rejected the requested model"
624
+ },
625
+ {
626
+ test: /model preflight failed/i,
627
+ label: "model/provider preflight failed"
628
+ },
629
+ {
630
+ test: /\b(?:command not found|ENOENT|is the .*CLI on PATH|executable not found|no such file or directory)\b/i,
631
+ label: "provider CLI is missing or not on PATH"
632
+ },
633
+ {
634
+ test: /\bfailed to spawn\b/i,
635
+ label: "provider failed to spawn the worker process"
636
+ },
637
+ {
638
+ test: /\b(?:not logged in|unauthorized|authentication (?:failed|required)|invalid api key|missing api key|401)\b/i,
639
+ label: "provider authentication failed"
640
+ }
641
+ ];
642
+ function tidy(errorText, max = 240) {
643
+ const oneLine2 = errorText.replace(/\s+/g, " ").trim();
644
+ return oneLine2.length > max ? `${oneLine2.slice(0, max - 1)}\u2026` : oneLine2;
645
+ }
646
+ function classifyExitFailure(errorText) {
647
+ const text = (errorText ?? "").trim();
648
+ if (!text) return null;
649
+ for (const pattern of FAILURE_PATTERNS) {
650
+ if (pattern.test.test(text)) {
651
+ return { blocked: true, reason: `${pattern.label}: ${tidy(text)}` };
652
+ }
653
+ }
654
+ return null;
655
+ }
656
+
607
657
  // src/git.ts
608
658
  import { spawnSync } from "node:child_process";
609
659
  function git(cwd, args, options = {}) {
@@ -719,7 +769,15 @@ var STALE_MS = 6e5;
719
769
  function computeAttention(input) {
720
770
  const now = Date.now();
721
771
  if (input.finalResult) return { state: "done", reason: "final result recorded" };
722
- if (!input.alive) return { state: "needs_attention", reason: "process exited without a final result" };
772
+ if (!input.alive) {
773
+ const classified = classifyExitFailure(input.error);
774
+ if (classified) return { state: "blocked", reason: classified.reason };
775
+ const tail = input.error?.trim();
776
+ return {
777
+ state: "needs_attention",
778
+ reason: tail ? `process exited without a final result: ${tail}` : "process exited without a final result"
779
+ };
780
+ }
723
781
  if (input.heartbeatBlocker) {
724
782
  return { state: "blocked", reason: `worker heartbeat reported blocker: ${input.heartbeatBlocker}` };
725
783
  }
@@ -749,6 +807,7 @@ function computeWorkerStatus(worker, options = {}) {
749
807
  fileMtime(worker.stderrPath),
750
808
  fileMtime(worker.heartbeatPath)
751
809
  ]);
810
+ const error = parsed.error || (!alive && !parsed.finalResult ? tailFile(worker.stderrPath, 10).trim() || void 0 : void 0);
752
811
  const attention = computeAttention({
753
812
  alive,
754
813
  finalResult: parsed.finalResult,
@@ -757,7 +816,8 @@ function computeWorkerStatus(worker, options = {}) {
757
816
  heartbeatBytes,
758
817
  lastActivityAt,
759
818
  heartbeatBlocker: heartbeat.heartbeatBlocker,
760
- startedAt: worker.startedAt
819
+ startedAt: worker.startedAt,
820
+ error
761
821
  });
762
822
  return {
763
823
  runId: worker.runId,
@@ -782,7 +842,7 @@ function computeWorkerStatus(worker, options = {}) {
782
842
  lastHeartbeatSummary: heartbeat.lastHeartbeatSummary,
783
843
  heartbeatBlocker: heartbeat.heartbeatBlocker,
784
844
  finalResult: parsed.finalResult,
785
- error: parsed.error || (!alive && !parsed.finalResult ? tailFile(worker.stderrPath, 10).trim() || void 0 : void 0),
845
+ error,
786
846
  changedFiles,
787
847
  gitAncestry
788
848
  };
@@ -907,8 +967,8 @@ function observeRunnerResourceGate(input) {
907
967
  }
908
968
 
909
969
  // src/supervisor.ts
910
- import { existsSync as existsSync9, mkdirSync as mkdirSync3 } from "node:fs";
911
- import path9 from "node:path";
970
+ import { existsSync as existsSync10, mkdirSync as mkdirSync3 } from "node:fs";
971
+ import path10 from "node:path";
912
972
 
913
973
  // src/prompt.ts
914
974
  function buildPrompt(input) {
@@ -942,10 +1002,74 @@ function buildPrompt(input) {
942
1002
  // src/providers/claude.ts
943
1003
  import { closeSync, openSync } from "node:fs";
944
1004
  import { spawn } from "node:child_process";
1005
+
1006
+ // src/providers/model-preflight.ts
1007
+ var REASONING_SUFFIX_RE = /-(?:thinking(?:-(?:high|medium|low|minimal|max|none))?|high|medium|low|minimal)$/i;
1008
+ function stripReasoningSuffix(model) {
1009
+ return model.replace(REASONING_SUFFIX_RE, "");
1010
+ }
1011
+ var FOREIGN_MODEL_RE = /^(?:gpt-|gpt5|o1|o3|o4|gemini-|grok-|composer|deepseek|llama|mistral|qwen|command-)/i;
1012
+ function looksLikeClaudeModel(model) {
1013
+ return /^claude[-_]/i.test(model) || /^(?:opus|sonnet|haiku)\b/i.test(model);
1014
+ }
1015
+ function preflightClaudeModel(model, defaultModel) {
1016
+ const requested = (model ?? "").trim();
1017
+ if (!requested) {
1018
+ return { ok: true, model: defaultModel, normalized: false };
1019
+ }
1020
+ const stripped = stripReasoningSuffix(requested).trim();
1021
+ const launch = stripped || defaultModel;
1022
+ if (FOREIGN_MODEL_RE.test(launch) || !looksLikeClaudeModel(launch) && launch !== defaultModel) {
1023
+ return {
1024
+ ok: false,
1025
+ model: requested,
1026
+ normalized: false,
1027
+ requested,
1028
+ 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.`
1029
+ };
1030
+ }
1031
+ if (launch !== requested) {
1032
+ return {
1033
+ ok: true,
1034
+ model: launch,
1035
+ normalized: true,
1036
+ requested,
1037
+ note: `normalized model "${requested}" \u2192 "${launch}" (the Claude CLI rejects reasoning-effort suffixes)`
1038
+ };
1039
+ }
1040
+ return { ok: true, model: launch, normalized: false };
1041
+ }
1042
+ function preflightCursorModel(model, defaultModel) {
1043
+ const requested = (model ?? "").trim();
1044
+ if (!requested) {
1045
+ return { ok: true, model: defaultModel, normalized: false };
1046
+ }
1047
+ if (looksLikeClaudeModel(requested)) {
1048
+ return {
1049
+ ok: false,
1050
+ model: requested,
1051
+ normalized: false,
1052
+ requested,
1053
+ note: `model "${requested}" is a Claude model but the worker provider is "cursor". Switch the provider to "claude" or pick a Cursor model.`
1054
+ };
1055
+ }
1056
+ return { ok: true, model: requested, normalized: false };
1057
+ }
1058
+
1059
+ // src/providers/claude.ts
1060
+ var CLAUDE_DEFAULT_MODEL = "claude-opus-4-7";
945
1061
  var claudeProvider = {
946
1062
  name: "claude",
1063
+ defaultModel: CLAUDE_DEFAULT_MODEL,
1064
+ preflightModel(model) {
1065
+ return preflightClaudeModel(model, CLAUDE_DEFAULT_MODEL);
1066
+ },
947
1067
  start(opts) {
948
- const model = opts.model || "claude-opus-4-7";
1068
+ const preflight = preflightClaudeModel(opts.model, CLAUDE_DEFAULT_MODEL);
1069
+ if (!preflight.ok) {
1070
+ throw new Error(`claude provider model preflight failed: ${preflight.note}`);
1071
+ }
1072
+ const model = preflight.model;
949
1073
  const stdoutFd = openSync(opts.stdoutPath, "a");
950
1074
  const stderrFd = openSync(opts.stderrPath, "a");
951
1075
  const child = spawn(
@@ -962,12 +1086,12 @@ var claudeProvider = {
962
1086
  "--include-partial-messages",
963
1087
  opts.prompt
964
1088
  ],
965
- {
1089
+ hiddenSpawnOptions({
966
1090
  cwd: opts.worktreePath,
967
1091
  detached: true,
968
1092
  stdio: ["ignore", stdoutFd, stderrFd],
969
1093
  env: scrubClaudeEnv(process.env)
970
- }
1094
+ })
971
1095
  );
972
1096
  closeSync(stdoutFd);
973
1097
  closeSync(stderrFd);
@@ -980,33 +1104,80 @@ var claudeProvider = {
980
1104
  };
981
1105
 
982
1106
  // src/providers/cursor.ts
983
- import { closeSync as closeSync2, existsSync as existsSync7, openSync as openSync2, readdirSync as readdirSync3 } from "node:fs";
1107
+ import { closeSync as closeSync2, existsSync as existsSync8, openSync as openSync2 } from "node:fs";
984
1108
  import { spawn as spawn2 } from "node:child_process";
1109
+ import path7 from "node:path";
1110
+
1111
+ // src/providers/cursor-windows.ts
1112
+ import { existsSync as existsSync7, readdirSync as readdirSync3 } from "node:fs";
985
1113
  import path6 from "node:path";
986
- var DEFAULT_CURSOR_MODEL = "composer-2.5";
987
- function latestVersionDir(versionsRoot) {
1114
+ var CURSOR_VERSION_DIR = /^\d{4}\.\d{1,2}\.\d{1,2}-[a-f0-9]+$/i;
1115
+ function parseCursorVersionSortKey(versionName) {
1116
+ const datePart = versionName.split("-")[0];
1117
+ const parts = datePart.split(".");
1118
+ if (parts.length !== 3) return null;
1119
+ const [year, month, day] = parts;
1120
+ if (!year || !month || !day) return null;
1121
+ return Number(`${year}${month.padStart(2, "0")}${day.padStart(2, "0")}`);
1122
+ }
1123
+ function pickLatestCursorVersionDir(agentRoot) {
1124
+ const versionsRoot = path6.join(agentRoot, "versions");
988
1125
  if (!existsSync7(versionsRoot)) return null;
989
- 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));
990
- return versions[0] ? path6.join(versionsRoot, versions[0]) : null;
991
- }
992
- function resolveBundledCursor(versionDir) {
1126
+ let bestDir = null;
1127
+ let bestKey = -1;
1128
+ for (const entry of readdirSync3(versionsRoot, { withFileTypes: true })) {
1129
+ if (!entry.isDirectory() || !CURSOR_VERSION_DIR.test(entry.name)) continue;
1130
+ const key = parseCursorVersionSortKey(entry.name);
1131
+ if (key == null || key <= bestKey) continue;
1132
+ bestKey = key;
1133
+ bestDir = path6.join(versionsRoot, entry.name);
1134
+ }
1135
+ return bestDir;
1136
+ }
1137
+ function resolveWindowsCursorBundled(agentRoot) {
1138
+ const root = agentRoot?.trim() || path6.join(process.env.LOCALAPPDATA || "", "cursor-agent");
1139
+ const directNode = path6.join(root, "node.exe");
1140
+ const directIndex = path6.join(root, "index.js");
1141
+ if (existsSync7(directNode) && existsSync7(directIndex)) {
1142
+ return { nodeExe: directNode, indexJs: directIndex, versionDir: root };
1143
+ }
1144
+ const versionDir = pickLatestCursorVersionDir(root);
1145
+ if (!versionDir) return null;
993
1146
  const nodeExe = path6.join(versionDir, "node.exe");
994
1147
  const indexJs = path6.join(versionDir, "index.js");
995
1148
  if (!existsSync7(nodeExe) || !existsSync7(indexJs)) return null;
996
- return { executable: nodeExe, prefixArgs: [indexJs], shell: false, detached: true };
1149
+ return { nodeExe, indexJs, versionDir };
997
1150
  }
998
- function resolveWindowsCursorSpawn(agentBin) {
999
- const agentRoot = path6.dirname(agentBin);
1000
- const direct = resolveBundledCursor(agentRoot);
1001
- if (direct) return direct;
1002
- const versionDir = latestVersionDir(path6.join(agentRoot, "versions"));
1003
- return versionDir ? resolveBundledCursor(versionDir) : null;
1151
+
1152
+ // src/providers/cursor.ts
1153
+ var DEFAULT_CURSOR_MODEL = "composer-2.5";
1154
+ function bundledSpawnTarget(nodeExe, indexJs, versionDir) {
1155
+ return {
1156
+ executable: nodeExe,
1157
+ prefixArgs: [indexJs],
1158
+ shell: false,
1159
+ detached: true,
1160
+ bundledVersionDir: versionDir
1161
+ };
1004
1162
  }
1005
1163
  function resolveCursorSpawn(agentBin) {
1006
- if (process.platform === "win32" && /\.(cmd|bat)$/i.test(agentBin)) {
1007
- const bundled = resolveWindowsCursorSpawn(agentBin);
1008
- if (bundled) return bundled;
1009
- return { executable: agentBin, prefixArgs: [], shell: true, detached: false };
1164
+ if (process.platform === "win32") {
1165
+ const isCursorWrapper = /\.(cmd|bat)$/i.test(agentBin);
1166
+ const isBundledNode = /node\.exe$/i.test(agentBin) && existsSync8(path7.join(path7.dirname(agentBin), "index.js"));
1167
+ const isDefaultShim = agentBin === "agent";
1168
+ if (isCursorWrapper || isBundledNode || isDefaultShim) {
1169
+ const bundled = isCursorWrapper ? resolveWindowsCursorBundled(path7.dirname(agentBin)) : isBundledNode ? {
1170
+ nodeExe: agentBin,
1171
+ indexJs: path7.join(path7.dirname(agentBin), "index.js"),
1172
+ versionDir: path7.dirname(agentBin)
1173
+ } : resolveWindowsCursorBundled();
1174
+ if (bundled) {
1175
+ return bundledSpawnTarget(bundled.nodeExe, bundled.indexJs, bundled.versionDir);
1176
+ }
1177
+ throw new Error(
1178
+ "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."
1179
+ );
1180
+ }
1010
1181
  }
1011
1182
  return { executable: agentBin, prefixArgs: [], shell: false, detached: true };
1012
1183
  }
@@ -1014,15 +1185,35 @@ function resolveAgentBin() {
1014
1185
  const configured = process.env.KYNVER_CURSOR_AGENT_BIN?.trim() || process.env.CURSOR_AGENT_BIN?.trim();
1015
1186
  if (configured) return configured;
1016
1187
  if (process.platform === "win32") {
1017
- const localAgent = path6.join(process.env.LOCALAPPDATA || "", "cursor-agent", "agent.cmd");
1018
- if (existsSync7(localAgent)) return localAgent;
1188
+ const bundled = resolveWindowsCursorBundled(
1189
+ process.env.KYNVER_CURSOR_AGENT_ROOT?.trim() || void 0
1190
+ );
1191
+ if (bundled) return bundled.nodeExe;
1192
+ const localAgent = path7.join(process.env.LOCALAPPDATA || "", "cursor-agent", "agent.cmd");
1193
+ if (existsSync8(localAgent)) return localAgent;
1019
1194
  }
1020
1195
  return "agent";
1021
1196
  }
1197
+ function cursorWorkerEnv(agentBin, spawnTarget) {
1198
+ return {
1199
+ ...process.env,
1200
+ CI: "1",
1201
+ NO_COLOR: "1",
1202
+ ...spawnTarget.bundledVersionDir ? { CURSOR_INVOKED_AS: path7.basename(agentBin) || "agent.cmd" } : {}
1203
+ };
1204
+ }
1022
1205
  var cursorProvider = {
1023
1206
  name: "cursor",
1207
+ defaultModel: DEFAULT_CURSOR_MODEL,
1208
+ preflightModel(model) {
1209
+ return preflightCursorModel(model, DEFAULT_CURSOR_MODEL);
1210
+ },
1024
1211
  start(opts) {
1025
- const model = opts.model || DEFAULT_CURSOR_MODEL;
1212
+ const preflight = preflightCursorModel(opts.model, DEFAULT_CURSOR_MODEL);
1213
+ if (!preflight.ok) {
1214
+ throw new Error(`cursor provider model preflight failed: ${preflight.note}`);
1215
+ }
1216
+ const model = preflight.model;
1026
1217
  const stdoutFd = openSync2(opts.stdoutPath, "a");
1027
1218
  const stderrFd = openSync2(opts.stderrPath, "a");
1028
1219
  const agentBin = resolveAgentBin();
@@ -1043,16 +1234,13 @@ var cursorProvider = {
1043
1234
  model,
1044
1235
  opts.prompt
1045
1236
  ],
1046
- {
1237
+ hiddenSpawnOptions({
1047
1238
  cwd: opts.worktreePath,
1048
1239
  detached: spawnTarget.detached,
1049
1240
  shell: spawnTarget.shell,
1050
1241
  stdio: ["ignore", stdoutFd, stderrFd],
1051
- env: {
1052
- ...process.env,
1053
- ...spawnTarget.prefixArgs.length > 0 ? { CURSOR_INVOKED_AS: path6.basename(agentBin) } : {}
1054
- }
1055
- }
1242
+ env: cursorWorkerEnv(agentBin, spawnTarget)
1243
+ })
1056
1244
  );
1057
1245
  closeSync2(stdoutFd);
1058
1246
  closeSync2(stderrFd);
@@ -1084,12 +1272,12 @@ function resolveWorkerProvider(name) {
1084
1272
 
1085
1273
  // src/auto-complete.ts
1086
1274
  import { spawn as spawn3 } from "node:child_process";
1087
- import { existsSync as existsSync8, openSync as openSync3, closeSync as closeSync3 } from "node:fs";
1088
- import path8 from "node:path";
1275
+ import { existsSync as existsSync9, openSync as openSync3, closeSync as closeSync3 } from "node:fs";
1276
+ import path9 from "node:path";
1089
1277
  import { fileURLToPath } from "node:url";
1090
1278
 
1091
1279
  // src/worker-ops.ts
1092
- import path7 from "node:path";
1280
+ import path8 from "node:path";
1093
1281
  async function postCompletion(url, secret, body) {
1094
1282
  const res = await fetch(url, {
1095
1283
  method: "POST",
@@ -1212,7 +1400,7 @@ async function completeWorker(args) {
1212
1400
  function workerStatus(args) {
1213
1401
  const worker = loadWorker(String(args.run), String(args.name));
1214
1402
  const status = computeWorkerStatus(worker);
1215
- writeJson(path7.join(worker.workerDir, "last-status.json"), status);
1403
+ writeJson(path8.join(worker.workerDir, "last-status.json"), status);
1216
1404
  console.log(JSON.stringify(status, null, 2));
1217
1405
  }
1218
1406
  function runStatus(args) {
@@ -1220,7 +1408,7 @@ function runStatus(args) {
1220
1408
  const names = Object.keys(run.workers || {});
1221
1409
  const workers = names.map((name) => {
1222
1410
  const worker = readJson(
1223
- path7.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
1411
+ path8.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
1224
1412
  void 0
1225
1413
  );
1226
1414
  if (!worker) {
@@ -1256,7 +1444,7 @@ function runStatus(args) {
1256
1444
  needsAttention: workers.filter((w) => w.attention && w.attention !== "ok" && w.attention !== "done").map((w) => w.worker),
1257
1445
  workers
1258
1446
  };
1259
- writeJson(path7.join(runDirectory(run.id), "last-board.json"), board);
1447
+ writeJson(path8.join(runDirectory(run.id), "last-board.json"), board);
1260
1448
  console.log(JSON.stringify(board, null, 2));
1261
1449
  }
1262
1450
  function tailWorker(args) {
@@ -1395,12 +1583,12 @@ async function autoCompleteWorkerCli(raw) {
1395
1583
  }
1396
1584
  }
1397
1585
  function resolveDefaultCliPath() {
1398
- return path8.join(fileURLToPath(new URL(".", import.meta.url)), "cli.js");
1586
+ return path9.join(fileURLToPath(new URL(".", import.meta.url)), "cli.js");
1399
1587
  }
1400
1588
  function spawnCompletionSidecar(opts) {
1401
1589
  const cliPath = opts.cliPath ?? resolveDefaultCliPath();
1402
- if (!existsSync8(cliPath)) return void 0;
1403
- const logPath = path8.join(opts.workerDir, "auto-complete.log");
1590
+ if (!existsSync9(cliPath)) return void 0;
1591
+ const logPath = path9.join(opts.workerDir, "auto-complete.log");
1404
1592
  let logFd;
1405
1593
  try {
1406
1594
  logFd = openSync3(logPath, "a");
@@ -1426,11 +1614,15 @@ function spawnCompletionSidecar(opts) {
1426
1614
  if (opts.baseUrl) args.push("--base-url", opts.baseUrl);
1427
1615
  if (opts.secret) args.push("--secret", opts.secret);
1428
1616
  try {
1429
- const child = spawn3(nodeExecutable, args, {
1430
- detached: true,
1431
- stdio,
1432
- env: process.env
1433
- });
1617
+ const child = spawn3(
1618
+ nodeExecutable,
1619
+ args,
1620
+ hiddenSpawnOptions({
1621
+ detached: true,
1622
+ stdio,
1623
+ env: process.env
1624
+ })
1625
+ );
1434
1626
  if (logFd !== void 0) closeSync3(logFd);
1435
1627
  child.unref();
1436
1628
  return { pid: child.pid, logPath, cliPath };
@@ -1454,31 +1646,44 @@ function spawnWorkerProcess(run, opts) {
1454
1646
  const name = safeSlug(rawName);
1455
1647
  if (run.workers?.[name]) throw new Error(`worker already exists in run ${run.id}: ${name}`);
1456
1648
  if (!opts.task) throw new Error(`missing task text for worker ${name}`);
1649
+ const provider = resolveWorkerProvider(opts.provider);
1650
+ let launchModel = opts.model;
1651
+ if (provider.preflightModel) {
1652
+ const preflight = provider.preflightModel(opts.model);
1653
+ if (!preflight.ok) {
1654
+ throw new Error(
1655
+ `model preflight failed for provider "${provider.name}": ${preflight.note ?? "invalid model/provider combination"}`
1656
+ );
1657
+ }
1658
+ if (preflight.normalized) {
1659
+ console.error(`[supervisor] ${name}: ${preflight.note}`);
1660
+ }
1661
+ launchModel = preflight.model;
1662
+ }
1457
1663
  const { worktreesDir } = getPaths();
1458
- const workerDir = path9.join(runDirectory(run.id), "workers", name);
1664
+ const workerDir = path10.join(runDirectory(run.id), "workers", name);
1459
1665
  mkdirSync3(workerDir, { recursive: true });
1460
- const worktreePath = path9.join(worktreesDir, run.id, name);
1666
+ const worktreePath = path10.join(worktreesDir, run.id, name);
1461
1667
  const branch = opts.branch || `agent/${run.id}/${name}`;
1462
- if (existsSync9(worktreePath)) throw new Error(`worktree path already exists: ${worktreePath}`);
1668
+ if (existsSync10(worktreePath)) throw new Error(`worktree path already exists: ${worktreePath}`);
1463
1669
  git(run.repo, ["fetch", "origin", "--prune"], { allowFailure: true });
1464
1670
  git(run.repo, ["worktree", "add", "-b", branch, worktreePath, run.baseCommit], { throwError: true });
1465
- const stdoutPath = path9.join(workerDir, "stdout.jsonl");
1466
- const stderrPath = path9.join(workerDir, "stderr.log");
1467
- const heartbeatPath = path9.join(workerDir, "heartbeat.jsonl");
1671
+ const stdoutPath = path10.join(workerDir, "stdout.jsonl");
1672
+ const stderrPath = path10.join(workerDir, "stderr.log");
1673
+ const heartbeatPath = path10.join(workerDir, "heartbeat.jsonl");
1468
1674
  const prompt = buildPrompt({
1469
1675
  task: opts.task,
1470
1676
  ownedPaths: opts.ownedPaths || [],
1471
1677
  worktreePath,
1472
1678
  heartbeatPath
1473
1679
  });
1474
- const provider = resolveWorkerProvider(opts.provider);
1475
1680
  let started;
1476
1681
  try {
1477
1682
  started = provider.start({
1478
1683
  name,
1479
1684
  task: opts.task,
1480
1685
  ownedPaths: opts.ownedPaths,
1481
- model: opts.model,
1686
+ model: launchModel,
1482
1687
  branch,
1483
1688
  worktreePath,
1484
1689
  workerDir,
@@ -1492,7 +1697,7 @@ function spawnWorkerProcess(run, opts) {
1492
1697
  git(run.repo, ["branch", "-D", branch], { allowFailure: true });
1493
1698
  throw error;
1494
1699
  }
1495
- const model = started.model || opts.model || "claude-opus-4-7";
1700
+ const model = started.model || launchModel || provider.defaultModel || "claude-opus-4-7";
1496
1701
  const worker = {
1497
1702
  name,
1498
1703
  runId: run.id,
@@ -1514,7 +1719,7 @@ function spawnWorkerProcess(run, opts) {
1514
1719
  startedAt: (/* @__PURE__ */ new Date()).toISOString()
1515
1720
  };
1516
1721
  saveWorker(run.id, worker);
1517
- run.workers = { ...run.workers || {}, [name]: { workerDir, statusPath: path9.join(workerDir, "worker.json") } };
1722
+ run.workers = { ...run.workers || {}, [name]: { workerDir, statusPath: path10.join(workerDir, "worker.json") } };
1518
1723
  run.status = "running";
1519
1724
  saveRun(run);
1520
1725
  if (worker.agentOsId && worker.taskId) {
@@ -1728,7 +1933,7 @@ async function dispatchRun(args) {
1728
1933
  }
1729
1934
 
1730
1935
  // src/sweep.ts
1731
- import path10 from "node:path";
1936
+ import path11 from "node:path";
1732
1937
  async function sweepRun(args) {
1733
1938
  const pipeline = args.pipeline === true || args.pipeline === "true";
1734
1939
  try {
@@ -1740,7 +1945,7 @@ async function sweepRun(args) {
1740
1945
  const releasedLocalOrphans = [];
1741
1946
  for (const name of Object.keys(run.workers || {})) {
1742
1947
  const worker = readJson(
1743
- path10.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
1948
+ path11.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
1744
1949
  void 0
1745
1950
  );
1746
1951
  if (!worker || !worker.dispatched || !worker.taskId) continue;
@@ -1783,11 +1988,11 @@ async function sweepRun(args) {
1783
1988
  }
1784
1989
 
1785
1990
  // src/worktree.ts
1786
- import { existsSync as existsSync10, mkdirSync as mkdirSync4 } from "node:fs";
1787
- import path12 from "node:path";
1991
+ import { existsSync as existsSync11, mkdirSync as mkdirSync4 } from "node:fs";
1992
+ import path13 from "node:path";
1788
1993
 
1789
1994
  // src/validate.ts
1790
- import path11 from "node:path";
1995
+ import path12 from "node:path";
1791
1996
  var RUN_ID_RE = /^[a-z0-9][a-z0-9._-]{0,127}$/i;
1792
1997
  function validateRunId(runId) {
1793
1998
  const trimmed = runId.trim();
@@ -1795,7 +2000,7 @@ function validateRunId(runId) {
1795
2000
  return trimmed;
1796
2001
  }
1797
2002
  function validateRepo(repo) {
1798
- const resolved = path11.resolve(repo);
2003
+ const resolved = path12.resolve(repo);
1799
2004
  if (resolved.includes("..")) throw new Error("repo path must not contain .. segments");
1800
2005
  return resolved;
1801
2006
  }
@@ -1806,7 +2011,7 @@ function createRun(args) {
1806
2011
  ensureGitRepo(repo);
1807
2012
  const id = args.id ? validateRunId(String(args.id)) : timestampSlug(String(args.name || "run"));
1808
2013
  const dir = runDirectory(id);
1809
- if (existsSync10(dir)) failExists(`run already exists: ${id}`);
2014
+ if (existsSync11(dir)) failExists(`run already exists: ${id}`);
1810
2015
  mkdirSync4(dir, { recursive: true });
1811
2016
  const base = String(args.base || "origin/main");
1812
2017
  const baseCommit = git(repo, ["rev-parse", base]).trim();
@@ -1820,12 +2025,12 @@ function createRun(args) {
1820
2025
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
1821
2026
  workers: {}
1822
2027
  };
1823
- writeJson(path12.join(dir, "run.json"), run);
2028
+ writeJson(path13.join(dir, "run.json"), run);
1824
2029
  console.log(JSON.stringify({ runId: id, runDir: dir, repo, base, baseCommit }, null, 2));
1825
2030
  }
1826
2031
  function listRuns() {
1827
2032
  const { runsDir } = getPaths();
1828
- const rows = listRunIds(runsDir).map((id) => readJson(path12.join(runDirectory(id), "run.json"), void 0)).filter(Boolean).map((run) => ({
2033
+ const rows = listRunIds(runsDir).map((id) => readJson(path13.join(runDirectory(id), "run.json"), void 0)).filter(Boolean).map((run) => ({
1829
2034
  id: run.id,
1830
2035
  name: run.name,
1831
2036
  status: run.status,
@@ -1840,10 +2045,10 @@ function failExists(message) {
1840
2045
  }
1841
2046
 
1842
2047
  // src/pipeline-tick.ts
1843
- import path15 from "node:path";
2048
+ import path16 from "node:path";
1844
2049
 
1845
2050
  // src/finalize.ts
1846
- import path13 from "node:path";
2051
+ import path14 from "node:path";
1847
2052
  var ACTIVE_RUN_STATUSES = /* @__PURE__ */ new Set(["running", "dispatching", "pending", "queued"]);
1848
2053
  function terminalStatusFor(run) {
1849
2054
  const names = Object.keys(run.workers || {});
@@ -1853,7 +2058,7 @@ function terminalStatusFor(run) {
1853
2058
  let anyCompletionBlocked = false;
1854
2059
  for (const name of names) {
1855
2060
  const worker = readJson(
1856
- path13.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
2061
+ path14.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
1857
2062
  void 0
1858
2063
  );
1859
2064
  if (!worker) continue;
@@ -1886,7 +2091,7 @@ function finalizeStaleRuns() {
1886
2091
  }
1887
2092
 
1888
2093
  // src/plan-progress-daemon-sync.ts
1889
- import path14 from "node:path";
2094
+ import path15 from "node:path";
1890
2095
 
1891
2096
  // src/plan-progress-sync.ts
1892
2097
  async function syncPlanProgress(args) {
@@ -1910,7 +2115,7 @@ async function syncActiveWorkerPlanProgress(runId, args) {
1910
2115
  const outcomes = [];
1911
2116
  for (const name of Object.keys(run.workers || {})) {
1912
2117
  const worker = readJson(
1913
- path14.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
2118
+ path15.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
1914
2119
  void 0
1915
2120
  );
1916
2121
  if (!worker?.dispatched || !worker.taskId) continue;
@@ -1964,7 +2169,7 @@ async function completeFinishedWorkers(runId, args) {
1964
2169
  const outcomes = [];
1965
2170
  for (const name of Object.keys(run.workers || {})) {
1966
2171
  const worker = readJson(
1967
- path15.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
2172
+ path16.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
1968
2173
  void 0
1969
2174
  );
1970
2175
  if (!worker?.taskId) continue;