@kynver-app/runtime 0.1.17 → 0.1.18

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
@@ -605,6 +605,52 @@ function summarizeEvent(event) {
605
605
  return void 0;
606
606
  }
607
607
 
608
+ // src/exit-classify.ts
609
+ var FAILURE_PATTERNS = [
610
+ {
611
+ test: /\b(?:invalid|unknown|unsupported|unrecognized)\b[^.\n]*\bmodel\b/i,
612
+ label: "provider rejected the requested model"
613
+ },
614
+ {
615
+ test: /\bmodel\b[^.\n]*\b(?:not\s+(?:found|supported|available|recognized|valid)|is\s+not\s+valid|does\s+not\s+exist)/i,
616
+ label: "provider rejected the requested model"
617
+ },
618
+ {
619
+ test: /\b(?:did you mean|available models|choose (?:a|one of)|supported models)\b/i,
620
+ label: "provider rejected the requested model"
621
+ },
622
+ {
623
+ test: /model preflight failed/i,
624
+ label: "model/provider preflight failed"
625
+ },
626
+ {
627
+ test: /\b(?:command not found|ENOENT|is the .*CLI on PATH|executable not found|no such file or directory)\b/i,
628
+ label: "provider CLI is missing or not on PATH"
629
+ },
630
+ {
631
+ test: /\bfailed to spawn\b/i,
632
+ label: "provider failed to spawn the worker process"
633
+ },
634
+ {
635
+ test: /\b(?:not logged in|unauthorized|authentication (?:failed|required)|invalid api key|missing api key|401)\b/i,
636
+ label: "provider authentication failed"
637
+ }
638
+ ];
639
+ function tidy(errorText, max = 240) {
640
+ const oneLine2 = errorText.replace(/\s+/g, " ").trim();
641
+ return oneLine2.length > max ? `${oneLine2.slice(0, max - 1)}\u2026` : oneLine2;
642
+ }
643
+ function classifyExitFailure(errorText) {
644
+ const text = (errorText ?? "").trim();
645
+ if (!text) return null;
646
+ for (const pattern of FAILURE_PATTERNS) {
647
+ if (pattern.test.test(text)) {
648
+ return { blocked: true, reason: `${pattern.label}: ${tidy(text)}` };
649
+ }
650
+ }
651
+ return null;
652
+ }
653
+
608
654
  // src/git.ts
609
655
  import { spawnSync } from "node:child_process";
610
656
  function git(cwd, args, options = {}) {
@@ -720,7 +766,15 @@ var STALE_MS = 6e5;
720
766
  function computeAttention(input) {
721
767
  const now = Date.now();
722
768
  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" };
769
+ if (!input.alive) {
770
+ const classified = classifyExitFailure(input.error);
771
+ if (classified) return { state: "blocked", reason: classified.reason };
772
+ const tail = input.error?.trim();
773
+ return {
774
+ state: "needs_attention",
775
+ reason: tail ? `process exited without a final result: ${tail}` : "process exited without a final result"
776
+ };
777
+ }
724
778
  if (input.heartbeatBlocker) {
725
779
  return { state: "blocked", reason: `worker heartbeat reported blocker: ${input.heartbeatBlocker}` };
726
780
  }
@@ -750,6 +804,7 @@ function computeWorkerStatus(worker, options = {}) {
750
804
  fileMtime(worker.stderrPath),
751
805
  fileMtime(worker.heartbeatPath)
752
806
  ]);
807
+ const error = parsed.error || (!alive && !parsed.finalResult ? tailFile(worker.stderrPath, 10).trim() || void 0 : void 0);
753
808
  const attention = computeAttention({
754
809
  alive,
755
810
  finalResult: parsed.finalResult,
@@ -758,7 +813,8 @@ function computeWorkerStatus(worker, options = {}) {
758
813
  heartbeatBytes,
759
814
  lastActivityAt,
760
815
  heartbeatBlocker: heartbeat.heartbeatBlocker,
761
- startedAt: worker.startedAt
816
+ startedAt: worker.startedAt,
817
+ error
762
818
  });
763
819
  return {
764
820
  runId: worker.runId,
@@ -783,7 +839,7 @@ function computeWorkerStatus(worker, options = {}) {
783
839
  lastHeartbeatSummary: heartbeat.lastHeartbeatSummary,
784
840
  heartbeatBlocker: heartbeat.heartbeatBlocker,
785
841
  finalResult: parsed.finalResult,
786
- error: parsed.error || (!alive && !parsed.finalResult ? tailFile(worker.stderrPath, 10).trim() || void 0 : void 0),
842
+ error,
787
843
  changedFiles,
788
844
  gitAncestry
789
845
  };
@@ -943,10 +999,74 @@ function buildPrompt(input) {
943
999
  // src/providers/claude.ts
944
1000
  import { closeSync, openSync } from "node:fs";
945
1001
  import { spawn } from "node:child_process";
1002
+
1003
+ // src/providers/model-preflight.ts
1004
+ var REASONING_SUFFIX_RE = /-(?:thinking(?:-(?:high|medium|low|minimal|max|none))?|high|medium|low|minimal)$/i;
1005
+ function stripReasoningSuffix(model) {
1006
+ return model.replace(REASONING_SUFFIX_RE, "");
1007
+ }
1008
+ var FOREIGN_MODEL_RE = /^(?:gpt-|gpt5|o1|o3|o4|gemini-|grok-|composer|deepseek|llama|mistral|qwen|command-)/i;
1009
+ function looksLikeClaudeModel(model) {
1010
+ return /^claude[-_]/i.test(model) || /^(?:opus|sonnet|haiku)\b/i.test(model);
1011
+ }
1012
+ function preflightClaudeModel(model, defaultModel) {
1013
+ const requested = (model ?? "").trim();
1014
+ if (!requested) {
1015
+ return { ok: true, model: defaultModel, normalized: false };
1016
+ }
1017
+ const stripped = stripReasoningSuffix(requested).trim();
1018
+ const launch = stripped || defaultModel;
1019
+ if (FOREIGN_MODEL_RE.test(launch) || !looksLikeClaudeModel(launch) && launch !== defaultModel) {
1020
+ return {
1021
+ ok: false,
1022
+ model: requested,
1023
+ normalized: false,
1024
+ requested,
1025
+ 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.`
1026
+ };
1027
+ }
1028
+ if (launch !== requested) {
1029
+ return {
1030
+ ok: true,
1031
+ model: launch,
1032
+ normalized: true,
1033
+ requested,
1034
+ note: `normalized model "${requested}" \u2192 "${launch}" (the Claude CLI rejects reasoning-effort suffixes)`
1035
+ };
1036
+ }
1037
+ return { ok: true, model: launch, normalized: false };
1038
+ }
1039
+ function preflightCursorModel(model, defaultModel) {
1040
+ const requested = (model ?? "").trim();
1041
+ if (!requested) {
1042
+ return { ok: true, model: defaultModel, normalized: false };
1043
+ }
1044
+ if (looksLikeClaudeModel(requested)) {
1045
+ return {
1046
+ ok: false,
1047
+ model: requested,
1048
+ normalized: false,
1049
+ requested,
1050
+ note: `model "${requested}" is a Claude model but the worker provider is "cursor". Switch the provider to "claude" or pick a Cursor model.`
1051
+ };
1052
+ }
1053
+ return { ok: true, model: requested, normalized: false };
1054
+ }
1055
+
1056
+ // src/providers/claude.ts
1057
+ var CLAUDE_DEFAULT_MODEL = "claude-opus-4-7";
946
1058
  var claudeProvider = {
947
1059
  name: "claude",
1060
+ defaultModel: CLAUDE_DEFAULT_MODEL,
1061
+ preflightModel(model) {
1062
+ return preflightClaudeModel(model, CLAUDE_DEFAULT_MODEL);
1063
+ },
948
1064
  start(opts) {
949
- const model = opts.model || "claude-opus-4-7";
1065
+ const preflight = preflightClaudeModel(opts.model, CLAUDE_DEFAULT_MODEL);
1066
+ if (!preflight.ok) {
1067
+ throw new Error(`claude provider model preflight failed: ${preflight.note}`);
1068
+ }
1069
+ const model = preflight.model;
950
1070
  const stdoutFd = openSync(opts.stdoutPath, "a");
951
1071
  const stderrFd = openSync(opts.stderrPath, "a");
952
1072
  const child = spawn(
@@ -1022,8 +1142,16 @@ function resolveAgentBin() {
1022
1142
  }
1023
1143
  var cursorProvider = {
1024
1144
  name: "cursor",
1145
+ defaultModel: DEFAULT_CURSOR_MODEL,
1146
+ preflightModel(model) {
1147
+ return preflightCursorModel(model, DEFAULT_CURSOR_MODEL);
1148
+ },
1025
1149
  start(opts) {
1026
- const model = opts.model || DEFAULT_CURSOR_MODEL;
1150
+ const preflight = preflightCursorModel(opts.model, DEFAULT_CURSOR_MODEL);
1151
+ if (!preflight.ok) {
1152
+ throw new Error(`cursor provider model preflight failed: ${preflight.note}`);
1153
+ }
1154
+ const model = preflight.model;
1027
1155
  const stdoutFd = openSync2(opts.stdoutPath, "a");
1028
1156
  const stderrFd = openSync2(opts.stderrPath, "a");
1029
1157
  const agentBin = resolveAgentBin();
@@ -1455,6 +1583,20 @@ function spawnWorkerProcess(run, opts) {
1455
1583
  const name = safeSlug(rawName);
1456
1584
  if (run.workers?.[name]) throw new Error(`worker already exists in run ${run.id}: ${name}`);
1457
1585
  if (!opts.task) throw new Error(`missing task text for worker ${name}`);
1586
+ const provider = resolveWorkerProvider(opts.provider);
1587
+ let launchModel = opts.model;
1588
+ if (provider.preflightModel) {
1589
+ const preflight = provider.preflightModel(opts.model);
1590
+ if (!preflight.ok) {
1591
+ throw new Error(
1592
+ `model preflight failed for provider "${provider.name}": ${preflight.note ?? "invalid model/provider combination"}`
1593
+ );
1594
+ }
1595
+ if (preflight.normalized) {
1596
+ console.error(`[supervisor] ${name}: ${preflight.note}`);
1597
+ }
1598
+ launchModel = preflight.model;
1599
+ }
1458
1600
  const { worktreesDir } = getPaths();
1459
1601
  const workerDir = path9.join(runDirectory(run.id), "workers", name);
1460
1602
  mkdirSync3(workerDir, { recursive: true });
@@ -1472,14 +1614,13 @@ function spawnWorkerProcess(run, opts) {
1472
1614
  worktreePath,
1473
1615
  heartbeatPath
1474
1616
  });
1475
- const provider = resolveWorkerProvider(opts.provider);
1476
1617
  let started;
1477
1618
  try {
1478
1619
  started = provider.start({
1479
1620
  name,
1480
1621
  task: opts.task,
1481
1622
  ownedPaths: opts.ownedPaths,
1482
- model: opts.model,
1623
+ model: launchModel,
1483
1624
  branch,
1484
1625
  worktreePath,
1485
1626
  workerDir,
@@ -1493,7 +1634,7 @@ function spawnWorkerProcess(run, opts) {
1493
1634
  git(run.repo, ["branch", "-D", branch], { allowFailure: true });
1494
1635
  throw error;
1495
1636
  }
1496
- const model = started.model || opts.model || "claude-opus-4-7";
1637
+ const model = started.model || launchModel || provider.defaultModel || "claude-opus-4-7";
1497
1638
  const worker = {
1498
1639
  name,
1499
1640
  runId: run.id,