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