@kynver-app/runtime 0.1.18 → 0.1.21

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 path18 = 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(path18);
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: path18,
421
425
  freeBytes,
422
426
  totalBytes,
423
427
  usedPercent,
@@ -691,22 +695,36 @@ function gitIsAncestor(cwd, ancestor, descendant) {
691
695
  if (res.status === 1) return { isAncestor: false, error: null };
692
696
  return { isAncestor: null, error: res.error || res.stderr || res.stdout || `git exited ${res.status}` };
693
697
  }
694
- function computeGitAncestry(worktreePath, base = "origin/main") {
698
+ function computeGitAncestry(worktreePath, baseOrOptions = "origin/main") {
699
+ const options = typeof baseOrOptions === "string" ? { base: baseOrOptions } : baseOrOptions;
700
+ const baseLabel = options.baseCommit?.trim() || options.base?.trim() || "origin/main";
701
+ const pinnedBaseCommit = options.baseCommit?.trim() || null;
695
702
  if (!worktreePath) {
696
- return unknownAncestry(base, "missing worktree path");
703
+ return unknownAncestry(baseLabel, "missing worktree path");
697
704
  }
698
705
  const head = gitCapture(worktreePath, ["rev-parse", "HEAD"]);
699
- if (head.status !== 0) return unknownAncestry(base, head.error || head.stderr || head.stdout || "failed to resolve HEAD");
700
- const baseHead = gitCapture(worktreePath, ["rev-parse", base]);
701
- if (baseHead.status !== 0) {
702
- return unknownAncestry(base, baseHead.error || baseHead.stderr || baseHead.stdout || `failed to resolve ${base}`, head.stdout.trim());
706
+ if (head.status !== 0) {
707
+ return unknownAncestry(baseLabel, head.error || head.stderr || head.stdout || "failed to resolve HEAD");
708
+ }
709
+ let baseSha;
710
+ if (pinnedBaseCommit) {
711
+ baseSha = pinnedBaseCommit;
712
+ } else {
713
+ const baseHead = gitCapture(worktreePath, ["rev-parse", baseLabel]);
714
+ if (baseHead.status !== 0) {
715
+ return unknownAncestry(
716
+ baseLabel,
717
+ baseHead.error || baseHead.stderr || baseHead.stdout || `failed to resolve ${baseLabel}`,
718
+ head.stdout.trim()
719
+ );
720
+ }
721
+ baseSha = baseHead.stdout.trim();
703
722
  }
704
723
  const headSha = head.stdout.trim();
705
- const baseSha = baseHead.stdout.trim();
706
724
  if (headSha === baseSha) {
707
725
  return {
708
726
  checked: true,
709
- base,
727
+ base: baseLabel,
710
728
  head: headSha,
711
729
  baseHead: baseSha,
712
730
  baseIsAncestorOfHead: true,
@@ -720,7 +738,7 @@ function computeGitAncestry(worktreePath, base = "origin/main") {
720
738
  if (baseIsAncestorOfHead.isAncestor == null || headIsAncestorOfBase.isAncestor == null) {
721
739
  return {
722
740
  checked: false,
723
- base,
741
+ base: baseLabel,
724
742
  head: headSha,
725
743
  baseHead: baseSha,
726
744
  baseIsAncestorOfHead: baseIsAncestorOfHead.isAncestor,
@@ -732,7 +750,7 @@ function computeGitAncestry(worktreePath, base = "origin/main") {
732
750
  const relation = baseIsAncestorOfHead.isAncestor ? "ahead" : headIsAncestorOfBase.isAncestor ? "merged" : "diverged";
733
751
  return {
734
752
  checked: true,
735
- base,
753
+ base: baseLabel,
736
754
  head: headSha,
737
755
  baseHead: baseSha,
738
756
  baseIsAncestorOfHead: baseIsAncestorOfHead.isAncestor,
@@ -759,12 +777,74 @@ function scrubClaudeEnv(env) {
759
777
  return next;
760
778
  }
761
779
 
780
+ // src/landing-gate.ts
781
+ function trimOrNull(value) {
782
+ if (typeof value !== "string") return null;
783
+ const trimmed = value.trim();
784
+ return trimmed.length ? trimmed : null;
785
+ }
786
+ function hasFinalResult(value) {
787
+ if (value === void 0 || value === null) return false;
788
+ if (typeof value === "string") return value.trim().length > 0;
789
+ if (typeof value === "boolean") return value;
790
+ if (Array.isArray(value)) return value.length > 0;
791
+ if (typeof value === "object") return Object.keys(value).length > 0;
792
+ return true;
793
+ }
794
+ function hasCommittedLandingRef(snapshot) {
795
+ if (trimOrNull(snapshot.headCommit)) return true;
796
+ if (trimOrNull(snapshot.prUrl)) return true;
797
+ if (trimOrNull(snapshot.artifactBundlePath)) return true;
798
+ if (trimOrNull(snapshot.patchPath)) return true;
799
+ const ancestry = snapshot.gitAncestry;
800
+ if (ancestry?.checked && ancestry.headIsAncestorOfBase === false && trimOrNull(ancestry.head)) {
801
+ return true;
802
+ }
803
+ return false;
804
+ }
805
+ function assessWorkerLanding(snapshot) {
806
+ if (!hasFinalResult(snapshot.finalResult)) return { blocked: false };
807
+ if (snapshot.changedFiles.length === 0) return { blocked: false };
808
+ if (!hasCommittedLandingRef(snapshot)) {
809
+ return {
810
+ blocked: true,
811
+ reason: "dirty_worktree_no_pr",
812
+ detail: `Worktree has ${snapshot.changedFiles.length} uncommitted change(s) with no commit or PR; commit, open a PR, or discard before landing`
813
+ };
814
+ }
815
+ return {
816
+ blocked: true,
817
+ detail: `Worktree has ${snapshot.changedFiles.length} uncommitted change(s); commit or discard before landing`
818
+ };
819
+ }
820
+ function landingAttentionReason(verdict) {
821
+ if (!verdict.blocked) return void 0;
822
+ return verdict.detail ?? verdict.reason ?? "dirty_worktree_no_pr";
823
+ }
824
+
762
825
  // src/status.ts
763
826
  var NO_START_MS = 18e4;
764
827
  var STALE_MS = 6e5;
765
828
  function computeAttention(input) {
766
829
  const now = Date.now();
767
- if (input.finalResult) return { state: "done", reason: "final result recorded" };
830
+ if (input.completionBlocker) {
831
+ return { state: "blocked", reason: input.completionBlocker };
832
+ }
833
+ if (input.finalResult) {
834
+ const landing = assessWorkerLanding({
835
+ finalResult: input.finalResult,
836
+ changedFiles: input.changedFiles ?? [],
837
+ gitAncestry: input.gitAncestry ?? null
838
+ });
839
+ if (landing.blocked) {
840
+ const detail = landingAttentionReason(landing);
841
+ return {
842
+ state: "needs_attention",
843
+ reason: landing.reason ? `landing blocked (${landing.reason}): ${detail}` : `landing blocked: ${detail}`
844
+ };
845
+ }
846
+ return { state: "done", reason: "final result recorded" };
847
+ }
768
848
  if (!input.alive) {
769
849
  const classified = classifyExitFailure(input.error);
770
850
  if (classified) return { state: "blocked", reason: classified.reason };
@@ -795,7 +875,10 @@ function computeWorkerStatus(worker, options = {}) {
795
875
  const stderrBytes = fileSize(worker.stderrPath);
796
876
  const heartbeatBytes = fileSize(worker.heartbeatPath);
797
877
  const changedFiles = gitStatusShort(worker.worktreePath);
798
- const gitAncestry = computeGitAncestry(worker.worktreePath, options.base);
878
+ const gitAncestry = computeGitAncestry(worker.worktreePath, {
879
+ base: options.base,
880
+ baseCommit: options.baseCommit
881
+ });
799
882
  const lastActivityAt = latestIso([
800
883
  parsed.lastEventAt,
801
884
  heartbeat.lastHeartbeatAt,
@@ -804,6 +887,7 @@ function computeWorkerStatus(worker, options = {}) {
804
887
  fileMtime(worker.heartbeatPath)
805
888
  ]);
806
889
  const error = parsed.error || (!alive && !parsed.finalResult ? tailFile(worker.stderrPath, 10).trim() || void 0 : void 0);
890
+ const completionBlocker = typeof worker.completionBlocker === "string" && worker.completionBlocker.trim() ? worker.completionBlocker.trim() : null;
807
891
  const attention = computeAttention({
808
892
  alive,
809
893
  finalResult: parsed.finalResult,
@@ -813,14 +897,18 @@ function computeWorkerStatus(worker, options = {}) {
813
897
  lastActivityAt,
814
898
  heartbeatBlocker: heartbeat.heartbeatBlocker,
815
899
  startedAt: worker.startedAt,
816
- error
900
+ error,
901
+ changedFiles,
902
+ gitAncestry,
903
+ completionBlocker
817
904
  });
905
+ const workerStatusLabel = completionBlocker || attention.state === "blocked" ? "blocked" : attention.state === "done" ? "done" : parsed.finalResult ? "exited" : alive ? "running" : "exited";
818
906
  return {
819
907
  runId: worker.runId,
820
908
  worker: worker.name,
821
909
  pid: worker.pid,
822
910
  alive,
823
- status: parsed.finalResult ? "done" : alive ? "running" : "exited",
911
+ status: workerStatusLabel,
824
912
  attention,
825
913
  branch: worker.branch,
826
914
  worktreePath: worker.worktreePath,
@@ -849,6 +937,10 @@ function isFinishedWorkerStatus(status) {
849
937
  if (status.status === "exited" || status.status === "done") return true;
850
938
  return false;
851
939
  }
940
+ function isLandingBlockedWorkerStatus(status) {
941
+ if (!status.finalResult) return false;
942
+ return status.attention.state === "needs_attention" || status.attention.state === "blocked";
943
+ }
852
944
  function deriveRunStatus(fallback, workers) {
853
945
  if (workers.length === 0) return fallback;
854
946
  if (workers.some((w) => w.attention === "needs_attention" || w.attention === "stale" || w.attention === "blocked")) {
@@ -899,6 +991,10 @@ function readAvailableMemBytes() {
899
991
  }
900
992
  return os.freemem();
901
993
  }
994
+ function isActiveHarnessWorker(worker) {
995
+ const status = computeWorkerStatus(worker);
996
+ return status.alive && !status.finalResult && status.attention.state !== "done";
997
+ }
902
998
  function countActiveWorkersForRun(run) {
903
999
  let active = 0;
904
1000
  for (const name of Object.keys(run.workers || {})) {
@@ -906,11 +1002,8 @@ function countActiveWorkersForRun(run) {
906
1002
  path5.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
907
1003
  void 0
908
1004
  );
909
- if (!worker) continue;
910
- const status = computeWorkerStatus(worker);
911
- if (status.alive && !status.finalResult && status.attention.state !== "done") {
912
- active++;
913
- }
1005
+ if (!worker || !isActiveHarnessWorker(worker)) continue;
1006
+ active++;
914
1007
  }
915
1008
  return active;
916
1009
  }
@@ -935,7 +1028,7 @@ function observeRunnerResourceGate(input) {
935
1028
  const maxConcurrentWorkers = Math.max(0, Math.min(targetCap, capacityFromTotal));
936
1029
  const slotsByCapacity = Math.max(0, maxConcurrentWorkers - activeWorkers);
937
1030
  const slotsByFreeMem = capacityFromFree;
938
- const slotsAvailable = Math.min(slotsByCapacity, slotsByFreeMem);
1031
+ let slotsAvailable = Math.min(slotsByCapacity, slotsByFreeMem);
939
1032
  let reason = null;
940
1033
  if (slotsAvailable <= 0) {
941
1034
  if (activeWorkers >= maxConcurrentWorkers) {
@@ -962,39 +1055,6 @@ function observeRunnerResourceGate(input) {
962
1055
  };
963
1056
  }
964
1057
 
965
- // src/supervisor.ts
966
- import { existsSync as existsSync9, mkdirSync as mkdirSync3 } from "node:fs";
967
- import path9 from "node:path";
968
-
969
- // src/prompt.ts
970
- function buildPrompt(input) {
971
- const ownership = input.ownedPaths.length ? `Owned paths: ${input.ownedPaths.join(", ")}. Do not edit outside these paths without stopping and reporting why.` : "Owned paths: unrestricted for this worker, but keep edits tightly scoped.";
972
- const progressLines = [
973
- "Structured plan progress (required when planId is set):",
974
- "- Harness checkpoints only: `kynver plan progress --plan <planId> --row <rowKey> --role implementer --status running|partial|blocked` (the by-id harness route rejects `done` and confirm events).",
975
- "- When a slice is finished, emit `partial` with evidence (`--evidence pr:<url>`, `--evidence path:<file>`, or `--evidence command:<cmd>`). Do not propose or confirm row `done` from the worker CLI.",
976
- "- Propose/confirm row `done` is MCP/session only: chat agents use `agent_os_plan_progress_event_append` on the slug route (implementer proposes with `proposed: true`; report_reviewer/deep_reviewer confirm with `proposed: false`).",
977
- "- When blocked on operator/Ghost/runtime review, create a linked review task (MCP `agent_os_plan_review_task_create` or API) and pass `--review-task <taskId>`.",
978
- "- Before the completion report: mark completion-report rows partial with evidence; do not skip report review.",
979
- "- After implementation: wait for report_reviewer then deep_reviewer confirmation (via MCP/session agents) before follow-up rows close.",
980
- input.planId ? `Active planId: ${input.planId}${input.taskId ? ` \xB7 taskId: ${input.taskId}` : ""}` : "No planId on this worker \u2014 still emit progress when you touch plan-scoped work."
981
- ];
982
- return [
983
- "You are running under the Kynver AgentOS runtime.",
984
- "Immediately state your plan before editing.",
985
- ownership,
986
- `Worktree: ${input.worktreePath}`,
987
- `Progress heartbeat file: ${input.heartbeatPath}`,
988
- "After each major step, append one JSON line to the heartbeat file with fields: ts, phase, summary, changedFiles, blocker.",
989
- "Final response must include files changed, verification commands, and unresolved risks.",
990
- "",
991
- ...progressLines,
992
- "",
993
- "Task:",
994
- input.task
995
- ].join("\n");
996
- }
997
-
998
1058
  // src/providers/claude.ts
999
1059
  import { closeSync, openSync } from "node:fs";
1000
1060
  import { spawn } from "node:child_process";
@@ -1053,7 +1113,7 @@ function preflightCursorModel(model, defaultModel) {
1053
1113
  }
1054
1114
 
1055
1115
  // src/providers/claude.ts
1056
- var CLAUDE_DEFAULT_MODEL = "claude-opus-4-7";
1116
+ var CLAUDE_DEFAULT_MODEL = "claude-sonnet-4-6";
1057
1117
  var claudeProvider = {
1058
1118
  name: "claude",
1059
1119
  defaultModel: CLAUDE_DEFAULT_MODEL,
@@ -1082,12 +1142,12 @@ var claudeProvider = {
1082
1142
  "--include-partial-messages",
1083
1143
  opts.prompt
1084
1144
  ],
1085
- {
1145
+ hiddenSpawnOptions({
1086
1146
  cwd: opts.worktreePath,
1087
1147
  detached: true,
1088
1148
  stdio: ["ignore", stdoutFd, stderrFd],
1089
1149
  env: scrubClaudeEnv(process.env)
1090
- }
1150
+ })
1091
1151
  );
1092
1152
  closeSync(stdoutFd);
1093
1153
  closeSync(stderrFd);
@@ -1099,34 +1159,234 @@ var claudeProvider = {
1099
1159
  }
1100
1160
  };
1101
1161
 
1162
+ // src/model-routing.ts
1163
+ var GLOBAL_DEFAULT_MODEL = "claude-sonnet-4-6";
1164
+ function taskString(task, key) {
1165
+ const v = task[key];
1166
+ return typeof v === "string" ? v.trim() : "";
1167
+ }
1168
+ function normalizeRef(ref) {
1169
+ return ref.toLowerCase();
1170
+ }
1171
+ function resolveGlobalDefaultModel(config = loadUserConfig()) {
1172
+ const fromConfig = config.defaultModel?.trim();
1173
+ if (fromConfig) return fromConfig;
1174
+ const fromEnv = process.env.KYNVER_DEFAULT_MODEL?.trim();
1175
+ if (fromEnv) return fromEnv;
1176
+ return GLOBAL_DEFAULT_MODEL;
1177
+ }
1178
+ function inferProviderFromModel(model) {
1179
+ const m = (model ?? "").toLowerCase();
1180
+ if (!m) return "claude";
1181
+ if (m.includes("composer") || m.includes("cursor") || m.includes("codex") || m.startsWith("gpt-") || m.startsWith("gpt5")) {
1182
+ return "cursor";
1183
+ }
1184
+ return "claude";
1185
+ }
1186
+ function isOpusLane(ref, title) {
1187
+ if (ref.includes("deep") && ref.includes("review")) return true;
1188
+ if (ref.includes("security")) return true;
1189
+ if (ref.includes("plan_author") || ref.includes("plan-author")) return true;
1190
+ if (title.includes("deep review") || title.includes("security review")) return true;
1191
+ if (ref.includes("plan") && !ref.includes("review") && (ref.includes("author") || ref.includes("strategy"))) {
1192
+ return true;
1193
+ }
1194
+ return false;
1195
+ }
1196
+ function inferModelRoutingFromTask(task) {
1197
+ const ref = normalizeRef(taskString(task, "executorRef"));
1198
+ const title = taskString(task, "title").toLowerCase();
1199
+ const priority = taskString(task, "priority") || "normal";
1200
+ const roleLane = normalizeRef(taskString(task, "roleLane"));
1201
+ if (ref.includes("cursor") || ref.includes("codex") || ref.includes("composer") || ref.includes("copilot") || roleLane === "implementer" || roleLane === "repair_implementer") {
1202
+ return { provider: "cursor", rule: "lane:implementation" };
1203
+ }
1204
+ if (ref.includes("landing") || title.startsWith("land:") || title.includes(" merge")) {
1205
+ return {
1206
+ model: "claude-haiku-4-5-20251001",
1207
+ provider: "claude",
1208
+ rule: "lane:landing"
1209
+ };
1210
+ }
1211
+ if (ref.includes("review") || title.startsWith("review ") || roleLane.includes("review")) {
1212
+ if (isOpusLane(ref, title) || roleLane === "deep_reviewer") {
1213
+ return { model: "claude-opus-4-7", provider: "claude", rule: "lane:deep_review" };
1214
+ }
1215
+ return { model: "claude-sonnet-4-6", provider: "claude", rule: "lane:review" };
1216
+ }
1217
+ if (isOpusLane(ref, title) || roleLane === "plan_author") {
1218
+ return { model: "claude-opus-4-7", provider: "claude", rule: "lane:planning" };
1219
+ }
1220
+ if (priority === "critical") {
1221
+ return { model: "claude-opus-4-7", provider: "claude", rule: "priority:critical" };
1222
+ }
1223
+ if (priority === "low") {
1224
+ return {
1225
+ model: "claude-haiku-4-5-20251001",
1226
+ provider: "claude",
1227
+ rule: "priority:low"
1228
+ };
1229
+ }
1230
+ return {
1231
+ model: resolveGlobalDefaultModel(),
1232
+ provider: "claude",
1233
+ rule: "default:sonnet"
1234
+ };
1235
+ }
1236
+ function resolveWorkerLaunch(input) {
1237
+ if (input.explicitModel?.trim()) {
1238
+ const model2 = input.explicitModel.trim();
1239
+ return {
1240
+ model: model2,
1241
+ provider: input.explicitProvider?.trim() || inferProviderFromModel(model2),
1242
+ rule: "explicit:cli",
1243
+ requestedModel: model2
1244
+ };
1245
+ }
1246
+ if (input.task && Object.keys(input.task).length > 0) {
1247
+ const inferred = inferModelRoutingFromTask(input.task);
1248
+ return {
1249
+ ...inferred,
1250
+ requestedModel: inferred.model
1251
+ };
1252
+ }
1253
+ const model = resolveGlobalDefaultModel();
1254
+ return {
1255
+ model,
1256
+ provider: input.explicitProvider?.trim() || inferProviderFromModel(model),
1257
+ rule: "default:global",
1258
+ requestedModel: model
1259
+ };
1260
+ }
1261
+ function resolveModelFallback(startedModel, launchModel, providerDefault) {
1262
+ return startedModel || launchModel || providerDefault || resolveGlobalDefaultModel() || CLAUDE_DEFAULT_MODEL;
1263
+ }
1264
+
1265
+ // src/retry-limits.ts
1266
+ function positiveInt2(value, fallback) {
1267
+ const n = Number(value);
1268
+ if (!Number.isFinite(n) || n <= 0) return fallback;
1269
+ return Math.floor(n);
1270
+ }
1271
+ function readHarnessRetryLimits() {
1272
+ return {
1273
+ maxTaskAttempts: positiveInt2(process.env.KYNVER_MAX_TASK_ATTEMPTS, 3),
1274
+ dispatchCooldownMs: positiveInt2(process.env.KYNVER_DISPATCH_COOLDOWN_MS, 5e3)
1275
+ };
1276
+ }
1277
+
1278
+ // src/supervisor.ts
1279
+ import { existsSync as existsSync10, mkdirSync as mkdirSync3 } from "node:fs";
1280
+ import path10 from "node:path";
1281
+
1282
+ // src/prompt.ts
1283
+ function buildPrompt(input) {
1284
+ const ownership = input.ownedPaths.length ? `Owned paths: ${input.ownedPaths.join(", ")}. Do not edit outside these paths without stopping and reporting why.` : "Owned paths: unrestricted for this worker, but keep edits tightly scoped.";
1285
+ const compact = Boolean(input.model?.toLowerCase().includes("haiku"));
1286
+ const progressLines = compact ? [
1287
+ "Plan progress: when planId is set, use `kynver plan progress` for running|partial|blocked only; row `done` is MCP/session only.",
1288
+ input.planId ? `Active planId: ${input.planId}` : "No planId on this worker."
1289
+ ] : [
1290
+ "Structured plan progress (required when planId is set):",
1291
+ "- Harness checkpoints only: `kynver plan progress --plan <planId> --row <rowKey> --role implementer --status running|partial|blocked` (the by-id harness route rejects `done` and confirm events).",
1292
+ "- When a slice is finished, emit `partial` with evidence (`--evidence pr:<url>`, `--evidence path:<file>`, or `--evidence command:<cmd>`). Do not propose or confirm row `done` from the worker CLI.",
1293
+ "- Propose/confirm row `done` is MCP/session only: chat agents use `agent_os_plan_progress_event_append` on the slug route (implementer proposes with `proposed: true`; report_reviewer/deep_reviewer confirm with `proposed: false`).",
1294
+ "- When blocked on operator/Ghost/runtime review, create a linked review task (MCP `agent_os_plan_review_task_create` or API) and pass `--review-task <taskId>`.",
1295
+ "- Before the completion report: mark completion-report rows partial with evidence; do not skip report review.",
1296
+ "- After implementation: wait for report_reviewer then deep_reviewer confirmation (via MCP/session agents) before follow-up rows close.",
1297
+ input.planId ? `Active planId: ${input.planId}${input.taskId ? ` \xB7 taskId: ${input.taskId}` : ""}` : "No planId on this worker \u2014 still emit progress when you touch plan-scoped work."
1298
+ ];
1299
+ return [
1300
+ "You are running under the Kynver AgentOS runtime.",
1301
+ "Immediately state your plan before editing.",
1302
+ ownership,
1303
+ `Worktree: ${input.worktreePath}`,
1304
+ `Progress heartbeat file: ${input.heartbeatPath}`,
1305
+ "After each major step, append one JSON line to the heartbeat file with fields: ts, phase, summary, changedFiles, blocker.",
1306
+ "Final response must include files changed, verification commands, and unresolved risks.",
1307
+ "",
1308
+ ...progressLines,
1309
+ "",
1310
+ "Task:",
1311
+ input.task
1312
+ ].join("\n");
1313
+ }
1314
+
1102
1315
  // src/providers/cursor.ts
1103
- import { closeSync as closeSync2, existsSync as existsSync7, openSync as openSync2, readdirSync as readdirSync3 } from "node:fs";
1316
+ import { closeSync as closeSync2, existsSync as existsSync8, openSync as openSync2 } from "node:fs";
1104
1317
  import { spawn as spawn2 } from "node:child_process";
1318
+ import path7 from "node:path";
1319
+
1320
+ // src/providers/cursor-windows.ts
1321
+ import { existsSync as existsSync7, readdirSync as readdirSync3 } from "node:fs";
1105
1322
  import path6 from "node:path";
1106
- var DEFAULT_CURSOR_MODEL = "composer-2.5";
1107
- function latestVersionDir(versionsRoot) {
1323
+ var CURSOR_VERSION_DIR = /^\d{4}\.\d{1,2}\.\d{1,2}-[a-f0-9]+$/i;
1324
+ function parseCursorVersionSortKey(versionName) {
1325
+ const datePart = versionName.split("-")[0];
1326
+ const parts = datePart.split(".");
1327
+ if (parts.length !== 3) return null;
1328
+ const [year, month, day] = parts;
1329
+ if (!year || !month || !day) return null;
1330
+ return Number(`${year}${month.padStart(2, "0")}${day.padStart(2, "0")}`);
1331
+ }
1332
+ function pickLatestCursorVersionDir(agentRoot) {
1333
+ const versionsRoot = path6.join(agentRoot, "versions");
1108
1334
  if (!existsSync7(versionsRoot)) return null;
1109
- 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));
1110
- return versions[0] ? path6.join(versionsRoot, versions[0]) : null;
1111
- }
1112
- function resolveBundledCursor(versionDir) {
1335
+ let bestDir = null;
1336
+ let bestKey = -1;
1337
+ for (const entry of readdirSync3(versionsRoot, { withFileTypes: true })) {
1338
+ if (!entry.isDirectory() || !CURSOR_VERSION_DIR.test(entry.name)) continue;
1339
+ const key = parseCursorVersionSortKey(entry.name);
1340
+ if (key == null || key <= bestKey) continue;
1341
+ bestKey = key;
1342
+ bestDir = path6.join(versionsRoot, entry.name);
1343
+ }
1344
+ return bestDir;
1345
+ }
1346
+ function resolveWindowsCursorBundled(agentRoot) {
1347
+ const root = agentRoot?.trim() || path6.join(process.env.LOCALAPPDATA || "", "cursor-agent");
1348
+ const directNode = path6.join(root, "node.exe");
1349
+ const directIndex = path6.join(root, "index.js");
1350
+ if (existsSync7(directNode) && existsSync7(directIndex)) {
1351
+ return { nodeExe: directNode, indexJs: directIndex, versionDir: root };
1352
+ }
1353
+ const versionDir = pickLatestCursorVersionDir(root);
1354
+ if (!versionDir) return null;
1113
1355
  const nodeExe = path6.join(versionDir, "node.exe");
1114
1356
  const indexJs = path6.join(versionDir, "index.js");
1115
1357
  if (!existsSync7(nodeExe) || !existsSync7(indexJs)) return null;
1116
- return { executable: nodeExe, prefixArgs: [indexJs], shell: false, detached: true };
1358
+ return { nodeExe, indexJs, versionDir };
1117
1359
  }
1118
- function resolveWindowsCursorSpawn(agentBin) {
1119
- const agentRoot = path6.dirname(agentBin);
1120
- const direct = resolveBundledCursor(agentRoot);
1121
- if (direct) return direct;
1122
- const versionDir = latestVersionDir(path6.join(agentRoot, "versions"));
1123
- return versionDir ? resolveBundledCursor(versionDir) : null;
1360
+
1361
+ // src/providers/cursor.ts
1362
+ var DEFAULT_CURSOR_MODEL = "composer-2.5";
1363
+ function bundledSpawnTarget(nodeExe, indexJs, versionDir) {
1364
+ return {
1365
+ executable: nodeExe,
1366
+ prefixArgs: [indexJs],
1367
+ shell: false,
1368
+ detached: true,
1369
+ bundledVersionDir: versionDir
1370
+ };
1124
1371
  }
1125
1372
  function resolveCursorSpawn(agentBin) {
1126
- if (process.platform === "win32" && /\.(cmd|bat)$/i.test(agentBin)) {
1127
- const bundled = resolveWindowsCursorSpawn(agentBin);
1128
- if (bundled) return bundled;
1129
- return { executable: agentBin, prefixArgs: [], shell: true, detached: false };
1373
+ if (process.platform === "win32") {
1374
+ const isCursorWrapper = /\.(cmd|bat)$/i.test(agentBin);
1375
+ const isBundledNode = /node\.exe$/i.test(agentBin) && existsSync8(path7.join(path7.dirname(agentBin), "index.js"));
1376
+ const isDefaultShim = agentBin === "agent";
1377
+ if (isCursorWrapper || isBundledNode || isDefaultShim) {
1378
+ const bundled = isCursorWrapper ? resolveWindowsCursorBundled(path7.dirname(agentBin)) : isBundledNode ? {
1379
+ nodeExe: agentBin,
1380
+ indexJs: path7.join(path7.dirname(agentBin), "index.js"),
1381
+ versionDir: path7.dirname(agentBin)
1382
+ } : resolveWindowsCursorBundled();
1383
+ if (bundled) {
1384
+ return bundledSpawnTarget(bundled.nodeExe, bundled.indexJs, bundled.versionDir);
1385
+ }
1386
+ throw new Error(
1387
+ "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."
1388
+ );
1389
+ }
1130
1390
  }
1131
1391
  return { executable: agentBin, prefixArgs: [], shell: false, detached: true };
1132
1392
  }
@@ -1134,11 +1394,23 @@ function resolveAgentBin() {
1134
1394
  const configured = process.env.KYNVER_CURSOR_AGENT_BIN?.trim() || process.env.CURSOR_AGENT_BIN?.trim();
1135
1395
  if (configured) return configured;
1136
1396
  if (process.platform === "win32") {
1137
- const localAgent = path6.join(process.env.LOCALAPPDATA || "", "cursor-agent", "agent.cmd");
1138
- if (existsSync7(localAgent)) return localAgent;
1397
+ const bundled = resolveWindowsCursorBundled(
1398
+ process.env.KYNVER_CURSOR_AGENT_ROOT?.trim() || void 0
1399
+ );
1400
+ if (bundled) return bundled.nodeExe;
1401
+ const localAgent = path7.join(process.env.LOCALAPPDATA || "", "cursor-agent", "agent.cmd");
1402
+ if (existsSync8(localAgent)) return localAgent;
1139
1403
  }
1140
1404
  return "agent";
1141
1405
  }
1406
+ function cursorWorkerEnv(agentBin, spawnTarget) {
1407
+ return {
1408
+ ...process.env,
1409
+ CI: "1",
1410
+ NO_COLOR: "1",
1411
+ ...spawnTarget.bundledVersionDir ? { CURSOR_INVOKED_AS: path7.basename(agentBin) || "agent.cmd" } : {}
1412
+ };
1413
+ }
1142
1414
  var cursorProvider = {
1143
1415
  name: "cursor",
1144
1416
  defaultModel: DEFAULT_CURSOR_MODEL,
@@ -1171,16 +1443,13 @@ var cursorProvider = {
1171
1443
  model,
1172
1444
  opts.prompt
1173
1445
  ],
1174
- {
1446
+ hiddenSpawnOptions({
1175
1447
  cwd: opts.worktreePath,
1176
1448
  detached: spawnTarget.detached,
1177
1449
  shell: spawnTarget.shell,
1178
1450
  stdio: ["ignore", stdoutFd, stderrFd],
1179
- env: {
1180
- ...process.env,
1181
- ...spawnTarget.prefixArgs.length > 0 ? { CURSOR_INVOKED_AS: path6.basename(agentBin) } : {}
1182
- }
1183
- }
1451
+ env: cursorWorkerEnv(agentBin, spawnTarget)
1452
+ })
1184
1453
  );
1185
1454
  closeSync2(stdoutFd);
1186
1455
  closeSync2(stderrFd);
@@ -1212,12 +1481,12 @@ function resolveWorkerProvider(name) {
1212
1481
 
1213
1482
  // src/auto-complete.ts
1214
1483
  import { spawn as spawn3 } from "node:child_process";
1215
- import { existsSync as existsSync8, openSync as openSync3, closeSync as closeSync3 } from "node:fs";
1216
- import path8 from "node:path";
1484
+ import { existsSync as existsSync9, openSync as openSync3, closeSync as closeSync3 } from "node:fs";
1485
+ import path9 from "node:path";
1217
1486
  import { fileURLToPath } from "node:url";
1218
1487
 
1219
1488
  // src/worker-ops.ts
1220
- import path7 from "node:path";
1489
+ import path8 from "node:path";
1221
1490
  async function postCompletion(url, secret, body) {
1222
1491
  const res = await fetch(url, {
1223
1492
  method: "POST",
@@ -1246,9 +1515,13 @@ function persistCompletionBlocker(worker, reason) {
1246
1515
  else delete worker.completionBlocker;
1247
1516
  saveWorker(worker.runId, worker);
1248
1517
  }
1518
+ function workerStatusOptions(run) {
1519
+ return run ? { base: run.base, baseCommit: run.baseCommit } : {};
1520
+ }
1249
1521
  async function tryCompleteWorker(args) {
1250
1522
  const worker = loadWorker(String(args.run), String(args.name));
1251
- const status = computeWorkerStatus(worker);
1523
+ const run = loadRun(worker.runId);
1524
+ const status = computeWorkerStatus(worker, workerStatusOptions(run));
1252
1525
  const agentOsId = (args.agentOsId ? String(args.agentOsId) : worker.agentOsId) || "";
1253
1526
  const taskId = (args.taskId ? String(args.taskId) : worker.taskId) || null;
1254
1527
  if (!agentOsId) {
@@ -1292,7 +1565,8 @@ async function tryCompleteWorker(args) {
1292
1565
  async function completeWorker(args) {
1293
1566
  try {
1294
1567
  const worker = loadWorker(String(args.run), String(args.name));
1295
- const status = computeWorkerStatus(worker);
1568
+ const run = loadRun(worker.runId);
1569
+ const status = computeWorkerStatus(worker, workerStatusOptions(run));
1296
1570
  const agentOsId = (args.agentOsId ? String(args.agentOsId) : worker.agentOsId) || "";
1297
1571
  const taskId = (args.taskId ? String(args.taskId) : worker.taskId) || null;
1298
1572
  if (!agentOsId) {
@@ -1339,8 +1613,9 @@ async function completeWorker(args) {
1339
1613
  }
1340
1614
  function workerStatus(args) {
1341
1615
  const worker = loadWorker(String(args.run), String(args.name));
1342
- const status = computeWorkerStatus(worker);
1343
- writeJson(path7.join(worker.workerDir, "last-status.json"), status);
1616
+ const run = loadRun(worker.runId);
1617
+ const status = computeWorkerStatus(worker, workerStatusOptions(run));
1618
+ writeJson(path8.join(worker.workerDir, "last-status.json"), status);
1344
1619
  console.log(JSON.stringify(status, null, 2));
1345
1620
  }
1346
1621
  function runStatus(args) {
@@ -1348,20 +1623,27 @@ function runStatus(args) {
1348
1623
  const names = Object.keys(run.workers || {});
1349
1624
  const workers = names.map((name) => {
1350
1625
  const worker = readJson(
1351
- path7.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
1626
+ path8.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
1352
1627
  void 0
1353
1628
  );
1354
1629
  if (!worker) {
1355
1630
  return { worker: name, status: "missing", attention: "needs_attention", attentionReason: "worker.json not found" };
1356
1631
  }
1357
- const status = computeWorkerStatus(worker, { base: run.base });
1632
+ const status = computeWorkerStatus(worker, {
1633
+ base: run.base,
1634
+ baseCommit: run.baseCommit
1635
+ });
1636
+ const headCommit = status.gitAncestry.headIsAncestorOfBase === false && status.gitAncestry.head ? status.gitAncestry.head : void 0;
1358
1637
  const rawBlocker = worker.completionBlocker;
1359
1638
  const completionBlocker = typeof rawBlocker === "string" && rawBlocker ? rawBlocker : void 0;
1639
+ const boardStatus = completionBlocker ? "blocked" : status.status;
1640
+ const boardAttention = completionBlocker ? "blocked" : status.attention.state;
1360
1641
  return {
1361
1642
  worker: status.worker,
1362
- status: completionBlocker ? "blocked" : status.status,
1363
- attention: completionBlocker ? "blocked" : status.attention.state,
1643
+ status: boardStatus,
1644
+ attention: boardAttention,
1364
1645
  attentionReason: completionBlocker ?? status.attention.reason,
1646
+ landingBlocked: status.finalResult ? boardAttention === "needs_attention" || boardAttention === "blocked" : false,
1365
1647
  pid: status.pid,
1366
1648
  alive: status.alive,
1367
1649
  currentTool: status.currentTool,
@@ -1370,7 +1652,14 @@ function runStatus(args) {
1370
1652
  lastHeartbeatSummary: status.lastHeartbeatSummary,
1371
1653
  heartbeatBlocker: status.heartbeatBlocker,
1372
1654
  changedFileCount: status.changedFiles.length,
1655
+ changedFiles: status.changedFiles,
1373
1656
  branch: status.branch,
1657
+ model: typeof worker.model === "string" ? worker.model : void 0,
1658
+ routingRule: typeof worker.routingRule === "string" ? worker.routingRule : void 0,
1659
+ requestedModel: typeof worker.requestedModel === "string" ? worker.requestedModel : void 0,
1660
+ headCommit,
1661
+ gitAncestry: status.gitAncestry,
1662
+ finalResult: status.finalResult,
1374
1663
  ancestry: status.gitAncestry.relation,
1375
1664
  ancestryChecked: status.gitAncestry.checked
1376
1665
  };
@@ -1384,7 +1673,7 @@ function runStatus(args) {
1384
1673
  needsAttention: workers.filter((w) => w.attention && w.attention !== "ok" && w.attention !== "done").map((w) => w.worker),
1385
1674
  workers
1386
1675
  };
1387
- writeJson(path7.join(runDirectory(run.id), "last-board.json"), board);
1676
+ writeJson(path8.join(runDirectory(run.id), "last-board.json"), board);
1388
1677
  console.log(JSON.stringify(board, null, 2));
1389
1678
  }
1390
1679
  function tailWorker(args) {
@@ -1523,12 +1812,12 @@ async function autoCompleteWorkerCli(raw) {
1523
1812
  }
1524
1813
  }
1525
1814
  function resolveDefaultCliPath() {
1526
- return path8.join(fileURLToPath(new URL(".", import.meta.url)), "cli.js");
1815
+ return path9.join(fileURLToPath(new URL(".", import.meta.url)), "cli.js");
1527
1816
  }
1528
1817
  function spawnCompletionSidecar(opts) {
1529
1818
  const cliPath = opts.cliPath ?? resolveDefaultCliPath();
1530
- if (!existsSync8(cliPath)) return void 0;
1531
- const logPath = path8.join(opts.workerDir, "auto-complete.log");
1819
+ if (!existsSync9(cliPath)) return void 0;
1820
+ const logPath = path9.join(opts.workerDir, "auto-complete.log");
1532
1821
  let logFd;
1533
1822
  try {
1534
1823
  logFd = openSync3(logPath, "a");
@@ -1554,11 +1843,15 @@ function spawnCompletionSidecar(opts) {
1554
1843
  if (opts.baseUrl) args.push("--base-url", opts.baseUrl);
1555
1844
  if (opts.secret) args.push("--secret", opts.secret);
1556
1845
  try {
1557
- const child = spawn3(nodeExecutable, args, {
1558
- detached: true,
1559
- stdio,
1560
- env: process.env
1561
- });
1846
+ const child = spawn3(
1847
+ nodeExecutable,
1848
+ args,
1849
+ hiddenSpawnOptions({
1850
+ detached: true,
1851
+ stdio,
1852
+ env: process.env
1853
+ })
1854
+ );
1562
1855
  if (logFd !== void 0) closeSync3(logFd);
1563
1856
  child.unref();
1564
1857
  return { pid: child.pid, logPath, cliPath };
@@ -1582,8 +1875,17 @@ function spawnWorkerProcess(run, opts) {
1582
1875
  const name = safeSlug(rawName);
1583
1876
  if (run.workers?.[name]) throw new Error(`worker already exists in run ${run.id}: ${name}`);
1584
1877
  if (!opts.task) throw new Error(`missing task text for worker ${name}`);
1585
- const provider = resolveWorkerProvider(opts.provider);
1586
- let launchModel = opts.model;
1878
+ const routing = opts.routingRule || opts.requestedModel ? {
1879
+ provider: opts.provider || "claude",
1880
+ model: opts.model,
1881
+ rule: opts.routingRule || "explicit:spawn",
1882
+ requestedModel: opts.requestedModel ?? opts.model
1883
+ } : resolveWorkerLaunch({
1884
+ explicitModel: opts.model,
1885
+ explicitProvider: opts.provider
1886
+ });
1887
+ const provider = resolveWorkerProvider(routing.provider);
1888
+ let launchModel = routing.model;
1587
1889
  if (provider.preflightModel) {
1588
1890
  const preflight = provider.preflightModel(opts.model);
1589
1891
  if (!preflight.ok) {
@@ -1597,21 +1899,24 @@ function spawnWorkerProcess(run, opts) {
1597
1899
  launchModel = preflight.model;
1598
1900
  }
1599
1901
  const { worktreesDir } = getPaths();
1600
- const workerDir = path9.join(runDirectory(run.id), "workers", name);
1902
+ const workerDir = path10.join(runDirectory(run.id), "workers", name);
1601
1903
  mkdirSync3(workerDir, { recursive: true });
1602
- const worktreePath = path9.join(worktreesDir, run.id, name);
1904
+ const worktreePath = path10.join(worktreesDir, run.id, name);
1603
1905
  const branch = opts.branch || `agent/${run.id}/${name}`;
1604
- if (existsSync9(worktreePath)) throw new Error(`worktree path already exists: ${worktreePath}`);
1906
+ if (existsSync10(worktreePath)) throw new Error(`worktree path already exists: ${worktreePath}`);
1605
1907
  git(run.repo, ["fetch", "origin", "--prune"], { allowFailure: true });
1606
1908
  git(run.repo, ["worktree", "add", "-b", branch, worktreePath, run.baseCommit], { throwError: true });
1607
- const stdoutPath = path9.join(workerDir, "stdout.jsonl");
1608
- const stderrPath = path9.join(workerDir, "stderr.log");
1609
- const heartbeatPath = path9.join(workerDir, "heartbeat.jsonl");
1909
+ const stdoutPath = path10.join(workerDir, "stdout.jsonl");
1910
+ const stderrPath = path10.join(workerDir, "stderr.log");
1911
+ const heartbeatPath = path10.join(workerDir, "heartbeat.jsonl");
1610
1912
  const prompt = buildPrompt({
1611
1913
  task: opts.task,
1612
1914
  ownedPaths: opts.ownedPaths || [],
1613
1915
  worktreePath,
1614
- heartbeatPath
1916
+ heartbeatPath,
1917
+ planId: opts.planId,
1918
+ taskId: opts.taskId,
1919
+ model: launchModel
1615
1920
  });
1616
1921
  let started;
1617
1922
  try {
@@ -1633,7 +1938,7 @@ function spawnWorkerProcess(run, opts) {
1633
1938
  git(run.repo, ["branch", "-D", branch], { allowFailure: true });
1634
1939
  throw error;
1635
1940
  }
1636
- const model = started.model || launchModel || provider.defaultModel || "claude-opus-4-7";
1941
+ const model = resolveModelFallback(started.model, launchModel, provider.defaultModel);
1637
1942
  const worker = {
1638
1943
  name,
1639
1944
  runId: run.id,
@@ -1652,21 +1957,32 @@ function spawnWorkerProcess(run, opts) {
1652
1957
  ...opts.planId ? { planId: String(opts.planId) } : {},
1653
1958
  ...opts.leaseOwner ? { leaseOwner: String(opts.leaseOwner) } : {},
1654
1959
  ...opts.dispatched ? { dispatched: true } : {},
1960
+ routingRule: routing.rule,
1961
+ ...routing.requestedModel ? { requestedModel: routing.requestedModel } : {},
1655
1962
  startedAt: (/* @__PURE__ */ new Date()).toISOString()
1656
1963
  };
1657
1964
  saveWorker(run.id, worker);
1658
- run.workers = { ...run.workers || {}, [name]: { workerDir, statusPath: path9.join(workerDir, "worker.json") } };
1965
+ run.workers = { ...run.workers || {}, [name]: { workerDir, statusPath: path10.join(workerDir, "worker.json") } };
1659
1966
  run.status = "running";
1660
1967
  saveRun(run);
1661
1968
  if (worker.agentOsId && worker.taskId) {
1969
+ let sidecarSpawned;
1662
1970
  try {
1663
- spawnCompletionSidecar({
1971
+ sidecarSpawned = spawnCompletionSidecar({
1664
1972
  runId: run.id,
1665
1973
  workerName: name,
1666
1974
  workerDir,
1667
1975
  agentOsId: worker.agentOsId
1668
1976
  });
1669
- } catch {
1977
+ } catch (error) {
1978
+ const reason = `completion sidecar failed to spawn: ${error.message}`;
1979
+ worker.completionBlocker = reason;
1980
+ saveWorker(run.id, worker);
1981
+ }
1982
+ if (!sidecarSpawned) {
1983
+ const reason = "completion sidecar failed to spawn (CLI not found or spawn error)";
1984
+ worker.completionBlocker = reason;
1985
+ saveWorker(run.id, worker);
1670
1986
  }
1671
1987
  }
1672
1988
  return worker;
@@ -1801,17 +2117,34 @@ async function dispatchRun(args) {
1801
2117
  console.log(JSON.stringify(summary2, null, 2));
1802
2118
  return;
1803
2119
  }
2120
+ const retryLimits = readHarnessRetryLimits();
1804
2121
  const outcomes = [];
1805
2122
  for (const decision of result.started) {
1806
2123
  const task = decision.task;
2124
+ const attempt = Number(task.attempt) || 1;
2125
+ if (attempt > retryLimits.maxTaskAttempts) {
2126
+ outcomes.push({
2127
+ taskId: task.id,
2128
+ started: false,
2129
+ error: `task attempt ${attempt} exceeds KYNVER_MAX_TASK_ATTEMPTS (${retryLimits.maxTaskAttempts})`
2130
+ });
2131
+ continue;
2132
+ }
1807
2133
  const name = safeSlug(`t-${task.id}-a${task.attempt}`);
2134
+ const routing = resolveWorkerLaunch({
2135
+ explicitModel: args.model ? String(args.model) : void 0,
2136
+ task
2137
+ });
1808
2138
  try {
1809
2139
  const planId = task.planId ? String(task.planId) : void 0;
1810
2140
  const worker = spawnWorkerProcess(run, {
1811
2141
  name,
1812
2142
  task: buildDispatchTaskText(task, agentOsId),
1813
2143
  ownedPaths: args.owned ? String(args.owned).split(",").map((s) => s.trim()).filter(Boolean) : [],
1814
- model: args.model ? String(args.model) : void 0,
2144
+ model: routing.model,
2145
+ provider: routing.provider,
2146
+ routingRule: routing.rule,
2147
+ requestedModel: routing.requestedModel,
1815
2148
  agentOsId,
1816
2149
  taskId: String(task.id),
1817
2150
  planId,
@@ -1823,7 +2156,10 @@ async function dispatchRun(args) {
1823
2156
  started: true,
1824
2157
  worker: worker.name,
1825
2158
  pid: worker.pid,
1826
- branch: worker.branch
2159
+ branch: worker.branch,
2160
+ model: worker.model,
2161
+ provider: routing.provider,
2162
+ routingRule: routing.rule
1827
2163
  });
1828
2164
  } catch (error) {
1829
2165
  const releaseUrl = `${base}/api/agent-os/by-id/${encodeURIComponent(agentOsId)}/tasks/${encodeURIComponent(String(task.id))}/release`;
@@ -1869,7 +2205,7 @@ async function dispatchRun(args) {
1869
2205
  }
1870
2206
 
1871
2207
  // src/sweep.ts
1872
- import path10 from "node:path";
2208
+ import path11 from "node:path";
1873
2209
  async function sweepRun(args) {
1874
2210
  const pipeline = args.pipeline === true || args.pipeline === "true";
1875
2211
  try {
@@ -1881,7 +2217,7 @@ async function sweepRun(args) {
1881
2217
  const releasedLocalOrphans = [];
1882
2218
  for (const name of Object.keys(run.workers || {})) {
1883
2219
  const worker = readJson(
1884
- path10.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
2220
+ path11.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
1885
2221
  void 0
1886
2222
  );
1887
2223
  if (!worker || !worker.dispatched || !worker.taskId) continue;
@@ -1924,11 +2260,11 @@ async function sweepRun(args) {
1924
2260
  }
1925
2261
 
1926
2262
  // src/worktree.ts
1927
- import { existsSync as existsSync10, mkdirSync as mkdirSync4 } from "node:fs";
1928
- import path12 from "node:path";
2263
+ import { existsSync as existsSync11, mkdirSync as mkdirSync4 } from "node:fs";
2264
+ import path13 from "node:path";
1929
2265
 
1930
2266
  // src/validate.ts
1931
- import path11 from "node:path";
2267
+ import path12 from "node:path";
1932
2268
  var RUN_ID_RE = /^[a-z0-9][a-z0-9._-]{0,127}$/i;
1933
2269
  function validateRunId(runId) {
1934
2270
  const trimmed = runId.trim();
@@ -1936,7 +2272,7 @@ function validateRunId(runId) {
1936
2272
  return trimmed;
1937
2273
  }
1938
2274
  function validateRepo(repo) {
1939
- const resolved = path11.resolve(repo);
2275
+ const resolved = path12.resolve(repo);
1940
2276
  if (resolved.includes("..")) throw new Error("repo path must not contain .. segments");
1941
2277
  return resolved;
1942
2278
  }
@@ -1947,7 +2283,7 @@ function createRun(args) {
1947
2283
  ensureGitRepo(repo);
1948
2284
  const id = args.id ? validateRunId(String(args.id)) : timestampSlug(String(args.name || "run"));
1949
2285
  const dir = runDirectory(id);
1950
- if (existsSync10(dir)) failExists(`run already exists: ${id}`);
2286
+ if (existsSync11(dir)) failExists(`run already exists: ${id}`);
1951
2287
  mkdirSync4(dir, { recursive: true });
1952
2288
  const base = String(args.base || "origin/main");
1953
2289
  const baseCommit = git(repo, ["rev-parse", base]).trim();
@@ -1961,12 +2297,12 @@ function createRun(args) {
1961
2297
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
1962
2298
  workers: {}
1963
2299
  };
1964
- writeJson(path12.join(dir, "run.json"), run);
2300
+ writeJson(path13.join(dir, "run.json"), run);
1965
2301
  console.log(JSON.stringify({ runId: id, runDir: dir, repo, base, baseCommit }, null, 2));
1966
2302
  }
1967
2303
  function listRuns() {
1968
2304
  const { runsDir } = getPaths();
1969
- const rows = listRunIds(runsDir).map((id) => readJson(path12.join(runDirectory(id), "run.json"), void 0)).filter(Boolean).map((run) => ({
2305
+ const rows = listRunIds(runsDir).map((id) => readJson(path13.join(runDirectory(id), "run.json"), void 0)).filter(Boolean).map((run) => ({
1970
2306
  id: run.id,
1971
2307
  name: run.name,
1972
2308
  status: run.status,
@@ -1981,10 +2317,13 @@ function failExists(message) {
1981
2317
  }
1982
2318
 
1983
2319
  // src/pipeline-tick.ts
2320
+ import path17 from "node:path";
2321
+
2322
+ // src/stale-reconcile.ts
1984
2323
  import path15 from "node:path";
1985
2324
 
1986
2325
  // src/finalize.ts
1987
- import path13 from "node:path";
2326
+ import path14 from "node:path";
1988
2327
  var ACTIVE_RUN_STATUSES = /* @__PURE__ */ new Set(["running", "dispatching", "pending", "queued"]);
1989
2328
  function terminalStatusFor(run) {
1990
2329
  const names = Object.keys(run.workers || {});
@@ -1992,13 +2331,17 @@ function terminalStatusFor(run) {
1992
2331
  let anyAlive = false;
1993
2332
  let anyResult = false;
1994
2333
  let anyCompletionBlocked = false;
2334
+ let anyLandingBlocked = false;
1995
2335
  for (const name of names) {
1996
2336
  const worker = readJson(
1997
- path13.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
2337
+ path14.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
1998
2338
  void 0
1999
2339
  );
2000
2340
  if (!worker) continue;
2001
- const status = computeWorkerStatus(worker);
2341
+ const status = computeWorkerStatus(worker, {
2342
+ base: run.base,
2343
+ baseCommit: run.baseCommit
2344
+ });
2002
2345
  if (status.alive && !status.finalResult) {
2003
2346
  anyAlive = true;
2004
2347
  break;
@@ -2006,10 +2349,14 @@ function terminalStatusFor(run) {
2006
2349
  if (typeof worker.completionBlocker === "string" && worker.completionBlocker) {
2007
2350
  anyCompletionBlocked = true;
2008
2351
  }
2009
- if (status.finalResult) anyResult = true;
2352
+ if (isLandingBlockedWorkerStatus(status)) {
2353
+ anyLandingBlocked = true;
2354
+ }
2355
+ if (status.finalResult && status.attention.state === "done") anyResult = true;
2010
2356
  }
2011
2357
  if (anyAlive) return null;
2012
2358
  if (anyCompletionBlocked) return null;
2359
+ if (anyLandingBlocked) return null;
2013
2360
  return anyResult ? "completed" : "failed";
2014
2361
  }
2015
2362
  function finalizeStaleRuns() {
@@ -2026,8 +2373,82 @@ function finalizeStaleRuns() {
2026
2373
  return finalized;
2027
2374
  }
2028
2375
 
2376
+ // src/stale-reconcile.ts
2377
+ var STALE_RECONCILE_HEARTBEAT_MS = 15 * 60 * 1e3;
2378
+ function staleReconcileDisabled() {
2379
+ return process.env.KYNVER_NO_STALE_CLEANUP === "1";
2380
+ }
2381
+ function reconcileStaleWorkers() {
2382
+ if (staleReconcileDisabled()) {
2383
+ return { workers: [], finalizedRuns: finalizeStaleRuns() };
2384
+ }
2385
+ const outcomes = [];
2386
+ const now = Date.now();
2387
+ for (const run of listRunRecords()) {
2388
+ for (const name of Object.keys(run.workers || {})) {
2389
+ const workerPath = path15.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json");
2390
+ const worker = readJson(workerPath, void 0);
2391
+ if (!worker || worker.status !== "running") {
2392
+ outcomes.push({
2393
+ runId: run.id,
2394
+ worker: name,
2395
+ action: "skipped",
2396
+ reason: worker ? `worker status is ${worker.status}` : "worker.json missing"
2397
+ });
2398
+ continue;
2399
+ }
2400
+ const status = computeWorkerStatus(worker, { base: run.base, baseCommit: run.baseCommit });
2401
+ if (status.finalResult) {
2402
+ outcomes.push({ runId: run.id, worker: name, action: "skipped", reason: "final result present" });
2403
+ continue;
2404
+ }
2405
+ if (!status.alive) {
2406
+ const nextStatus = status.attention.state === "blocked" ? "blocked" : status.status === "done" ? "done" : "exited";
2407
+ worker.status = nextStatus;
2408
+ worker.reconciledAt = (/* @__PURE__ */ new Date()).toISOString();
2409
+ worker.reconcileReason = status.attention.reason;
2410
+ saveWorker(run.id, worker);
2411
+ outcomes.push({
2412
+ runId: run.id,
2413
+ worker: name,
2414
+ action: "marked_exited",
2415
+ reason: status.attention.reason
2416
+ });
2417
+ continue;
2418
+ }
2419
+ if (status.attention.state === "stale" && worker.pid && isPidAlive(worker.pid)) {
2420
+ const hbMs = status.lastHeartbeatAt ? Date.parse(status.lastHeartbeatAt) : NaN;
2421
+ const actMs = status.lastActivityAt ? Date.parse(status.lastActivityAt) : NaN;
2422
+ const hbStale = !Number.isFinite(hbMs) || now - hbMs > STALE_RECONCILE_HEARTBEAT_MS;
2423
+ const actStale = Number.isFinite(actMs) && now - actMs > STALE_MS;
2424
+ if (hbStale && actStale) {
2425
+ killWorkerProcess(worker.pid, "SIGTERM");
2426
+ worker.status = "exited";
2427
+ worker.reconciledAt = (/* @__PURE__ */ new Date()).toISOString();
2428
+ worker.reconcileReason = `reconciled stale worker: ${status.attention.reason}`;
2429
+ saveWorker(run.id, worker);
2430
+ outcomes.push({
2431
+ runId: run.id,
2432
+ worker: name,
2433
+ action: "killed_stale",
2434
+ reason: status.attention.reason
2435
+ });
2436
+ continue;
2437
+ }
2438
+ }
2439
+ outcomes.push({
2440
+ runId: run.id,
2441
+ worker: name,
2442
+ action: "skipped",
2443
+ reason: status.attention.reason
2444
+ });
2445
+ }
2446
+ }
2447
+ return { workers: outcomes, finalizedRuns: finalizeStaleRuns() };
2448
+ }
2449
+
2029
2450
  // src/plan-progress-daemon-sync.ts
2030
- import path14 from "node:path";
2451
+ import path16 from "node:path";
2031
2452
 
2032
2453
  // src/plan-progress-sync.ts
2033
2454
  async function syncPlanProgress(args) {
@@ -2051,7 +2472,7 @@ async function syncActiveWorkerPlanProgress(runId, args) {
2051
2472
  const outcomes = [];
2052
2473
  for (const name of Object.keys(run.workers || {})) {
2053
2474
  const worker = readJson(
2054
- path14.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
2475
+ path16.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
2055
2476
  void 0
2056
2477
  );
2057
2478
  if (!worker?.dispatched || !worker.taskId) continue;
@@ -2105,7 +2526,7 @@ async function completeFinishedWorkers(runId, args) {
2105
2526
  const outcomes = [];
2106
2527
  for (const name of Object.keys(run.workers || {})) {
2107
2528
  const worker = readJson(
2108
- path15.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
2529
+ path17.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
2109
2530
  void 0
2110
2531
  );
2111
2532
  if (!worker?.taskId) continue;
@@ -2140,7 +2561,7 @@ async function runPipelineTick(args) {
2140
2561
  const execute = args.execute !== false && args.execute !== "false";
2141
2562
  runStatus({ run: runId });
2142
2563
  const completedWorkers = await completeFinishedWorkers(runId, args);
2143
- const finalizedStaleRuns = finalizeStaleRuns();
2564
+ const staleReconcile = reconcileStaleWorkers();
2144
2565
  const planProgressSync = await syncActiveWorkerPlanProgress(runId, args);
2145
2566
  const workspacePrefs = await fetchWorkspaceRuntimePreferences(agentOsId, args);
2146
2567
  const resourceGate = observeRunnerResourceGate({
@@ -2181,7 +2602,7 @@ async function runPipelineTick(args) {
2181
2602
  execute,
2182
2603
  resourceGate,
2183
2604
  completedWorkers,
2184
- finalizedStaleRuns,
2605
+ staleReconcile,
2185
2606
  planProgressSync,
2186
2607
  operatorTick,
2187
2608
  sweep,