@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/index.js CHANGED
@@ -10,6 +10,10 @@ function fail(message) {
10
10
  console.error(message);
11
11
  process.exit(1);
12
12
  }
13
+ function hiddenSpawnOptions(opts) {
14
+ if (process.platform !== "win32") return opts;
15
+ return { windowsHide: true, ...opts };
16
+ }
13
17
  function required(value, name) {
14
18
  if (!value) fail(`missing ${name}`);
15
19
  return value;
@@ -393,12 +397,12 @@ var DEFAULT_CRITICAL_FREE_BYTES = 15 * 1024 * 1024 * 1024;
393
397
  var DEFAULT_MAX_USED_PERCENT = 80;
394
398
  var DEFAULT_HARD_MAX_USED_PERCENT = 90;
395
399
  function observeRunnerDiskGate(input = {}) {
396
- const path16 = input.diskPath?.trim() || "/";
400
+ const path18 = input.diskPath?.trim() || "/";
397
401
  const warnBelowBytes = input.diskFreeWarnBytes ?? DEFAULT_WARN_FREE_BYTES;
398
402
  const criticalBelowBytes = input.diskFreeCriticalBytes ?? DEFAULT_CRITICAL_FREE_BYTES;
399
403
  const maxUsedPercent = input.diskMaxUsedPercent ?? DEFAULT_MAX_USED_PERCENT;
400
404
  const hardMaxUsedPercent = input.diskHardMaxUsedPercent ?? DEFAULT_HARD_MAX_USED_PERCENT;
401
- const stats = statfsSync(path16);
405
+ const stats = statfsSync(path18);
402
406
  const freeBytes = Number(stats.bavail) * Number(stats.bsize);
403
407
  const totalBytes = Number(stats.blocks) * Number(stats.bsize);
404
408
  const usedPercent = totalBytes > 0 ? (totalBytes - freeBytes) / totalBytes * 100 : 100;
@@ -418,7 +422,7 @@ function observeRunnerDiskGate(input = {}) {
418
422
  }
419
423
  return {
420
424
  ok,
421
- path: path16,
425
+ path: path18,
422
426
  freeBytes,
423
427
  totalBytes,
424
428
  usedPercent,
@@ -692,22 +696,36 @@ function gitIsAncestor(cwd, ancestor, descendant) {
692
696
  if (res.status === 1) return { isAncestor: false, error: null };
693
697
  return { isAncestor: null, error: res.error || res.stderr || res.stdout || `git exited ${res.status}` };
694
698
  }
695
- function computeGitAncestry(worktreePath, base = "origin/main") {
699
+ function computeGitAncestry(worktreePath, baseOrOptions = "origin/main") {
700
+ const options = typeof baseOrOptions === "string" ? { base: baseOrOptions } : baseOrOptions;
701
+ const baseLabel = options.baseCommit?.trim() || options.base?.trim() || "origin/main";
702
+ const pinnedBaseCommit = options.baseCommit?.trim() || null;
696
703
  if (!worktreePath) {
697
- return unknownAncestry(base, "missing worktree path");
704
+ return unknownAncestry(baseLabel, "missing worktree path");
698
705
  }
699
706
  const head = gitCapture(worktreePath, ["rev-parse", "HEAD"]);
700
- if (head.status !== 0) return unknownAncestry(base, head.error || head.stderr || head.stdout || "failed to resolve HEAD");
701
- const baseHead = gitCapture(worktreePath, ["rev-parse", base]);
702
- if (baseHead.status !== 0) {
703
- return unknownAncestry(base, baseHead.error || baseHead.stderr || baseHead.stdout || `failed to resolve ${base}`, head.stdout.trim());
707
+ if (head.status !== 0) {
708
+ return unknownAncestry(baseLabel, head.error || head.stderr || head.stdout || "failed to resolve HEAD");
709
+ }
710
+ let baseSha;
711
+ if (pinnedBaseCommit) {
712
+ baseSha = pinnedBaseCommit;
713
+ } else {
714
+ const baseHead = gitCapture(worktreePath, ["rev-parse", baseLabel]);
715
+ if (baseHead.status !== 0) {
716
+ return unknownAncestry(
717
+ baseLabel,
718
+ baseHead.error || baseHead.stderr || baseHead.stdout || `failed to resolve ${baseLabel}`,
719
+ head.stdout.trim()
720
+ );
721
+ }
722
+ baseSha = baseHead.stdout.trim();
704
723
  }
705
724
  const headSha = head.stdout.trim();
706
- const baseSha = baseHead.stdout.trim();
707
725
  if (headSha === baseSha) {
708
726
  return {
709
727
  checked: true,
710
- base,
728
+ base: baseLabel,
711
729
  head: headSha,
712
730
  baseHead: baseSha,
713
731
  baseIsAncestorOfHead: true,
@@ -721,7 +739,7 @@ function computeGitAncestry(worktreePath, base = "origin/main") {
721
739
  if (baseIsAncestorOfHead.isAncestor == null || headIsAncestorOfBase.isAncestor == null) {
722
740
  return {
723
741
  checked: false,
724
- base,
742
+ base: baseLabel,
725
743
  head: headSha,
726
744
  baseHead: baseSha,
727
745
  baseIsAncestorOfHead: baseIsAncestorOfHead.isAncestor,
@@ -733,7 +751,7 @@ function computeGitAncestry(worktreePath, base = "origin/main") {
733
751
  const relation = baseIsAncestorOfHead.isAncestor ? "ahead" : headIsAncestorOfBase.isAncestor ? "merged" : "diverged";
734
752
  return {
735
753
  checked: true,
736
- base,
754
+ base: baseLabel,
737
755
  head: headSha,
738
756
  baseHead: baseSha,
739
757
  baseIsAncestorOfHead: baseIsAncestorOfHead.isAncestor,
@@ -760,12 +778,74 @@ function scrubClaudeEnv(env) {
760
778
  return next;
761
779
  }
762
780
 
781
+ // src/landing-gate.ts
782
+ function trimOrNull(value) {
783
+ if (typeof value !== "string") return null;
784
+ const trimmed = value.trim();
785
+ return trimmed.length ? trimmed : null;
786
+ }
787
+ function hasFinalResult(value) {
788
+ if (value === void 0 || value === null) return false;
789
+ if (typeof value === "string") return value.trim().length > 0;
790
+ if (typeof value === "boolean") return value;
791
+ if (Array.isArray(value)) return value.length > 0;
792
+ if (typeof value === "object") return Object.keys(value).length > 0;
793
+ return true;
794
+ }
795
+ function hasCommittedLandingRef(snapshot) {
796
+ if (trimOrNull(snapshot.headCommit)) return true;
797
+ if (trimOrNull(snapshot.prUrl)) return true;
798
+ if (trimOrNull(snapshot.artifactBundlePath)) return true;
799
+ if (trimOrNull(snapshot.patchPath)) return true;
800
+ const ancestry = snapshot.gitAncestry;
801
+ if (ancestry?.checked && ancestry.headIsAncestorOfBase === false && trimOrNull(ancestry.head)) {
802
+ return true;
803
+ }
804
+ return false;
805
+ }
806
+ function assessWorkerLanding(snapshot) {
807
+ if (!hasFinalResult(snapshot.finalResult)) return { blocked: false };
808
+ if (snapshot.changedFiles.length === 0) return { blocked: false };
809
+ if (!hasCommittedLandingRef(snapshot)) {
810
+ return {
811
+ blocked: true,
812
+ reason: "dirty_worktree_no_pr",
813
+ detail: `Worktree has ${snapshot.changedFiles.length} uncommitted change(s) with no commit or PR; commit, open a PR, or discard before landing`
814
+ };
815
+ }
816
+ return {
817
+ blocked: true,
818
+ detail: `Worktree has ${snapshot.changedFiles.length} uncommitted change(s); commit or discard before landing`
819
+ };
820
+ }
821
+ function landingAttentionReason(verdict) {
822
+ if (!verdict.blocked) return void 0;
823
+ return verdict.detail ?? verdict.reason ?? "dirty_worktree_no_pr";
824
+ }
825
+
763
826
  // src/status.ts
764
827
  var NO_START_MS = 18e4;
765
828
  var STALE_MS = 6e5;
766
829
  function computeAttention(input) {
767
830
  const now = Date.now();
768
- if (input.finalResult) return { state: "done", reason: "final result recorded" };
831
+ if (input.completionBlocker) {
832
+ return { state: "blocked", reason: input.completionBlocker };
833
+ }
834
+ if (input.finalResult) {
835
+ const landing = assessWorkerLanding({
836
+ finalResult: input.finalResult,
837
+ changedFiles: input.changedFiles ?? [],
838
+ gitAncestry: input.gitAncestry ?? null
839
+ });
840
+ if (landing.blocked) {
841
+ const detail = landingAttentionReason(landing);
842
+ return {
843
+ state: "needs_attention",
844
+ reason: landing.reason ? `landing blocked (${landing.reason}): ${detail}` : `landing blocked: ${detail}`
845
+ };
846
+ }
847
+ return { state: "done", reason: "final result recorded" };
848
+ }
769
849
  if (!input.alive) {
770
850
  const classified = classifyExitFailure(input.error);
771
851
  if (classified) return { state: "blocked", reason: classified.reason };
@@ -796,7 +876,10 @@ function computeWorkerStatus(worker, options = {}) {
796
876
  const stderrBytes = fileSize(worker.stderrPath);
797
877
  const heartbeatBytes = fileSize(worker.heartbeatPath);
798
878
  const changedFiles = gitStatusShort(worker.worktreePath);
799
- const gitAncestry = computeGitAncestry(worker.worktreePath, options.base);
879
+ const gitAncestry = computeGitAncestry(worker.worktreePath, {
880
+ base: options.base,
881
+ baseCommit: options.baseCommit
882
+ });
800
883
  const lastActivityAt = latestIso([
801
884
  parsed.lastEventAt,
802
885
  heartbeat.lastHeartbeatAt,
@@ -805,6 +888,7 @@ function computeWorkerStatus(worker, options = {}) {
805
888
  fileMtime(worker.heartbeatPath)
806
889
  ]);
807
890
  const error = parsed.error || (!alive && !parsed.finalResult ? tailFile(worker.stderrPath, 10).trim() || void 0 : void 0);
891
+ const completionBlocker = typeof worker.completionBlocker === "string" && worker.completionBlocker.trim() ? worker.completionBlocker.trim() : null;
808
892
  const attention = computeAttention({
809
893
  alive,
810
894
  finalResult: parsed.finalResult,
@@ -814,14 +898,18 @@ function computeWorkerStatus(worker, options = {}) {
814
898
  lastActivityAt,
815
899
  heartbeatBlocker: heartbeat.heartbeatBlocker,
816
900
  startedAt: worker.startedAt,
817
- error
901
+ error,
902
+ changedFiles,
903
+ gitAncestry,
904
+ completionBlocker
818
905
  });
906
+ const workerStatusLabel = completionBlocker || attention.state === "blocked" ? "blocked" : attention.state === "done" ? "done" : parsed.finalResult ? "exited" : alive ? "running" : "exited";
819
907
  return {
820
908
  runId: worker.runId,
821
909
  worker: worker.name,
822
910
  pid: worker.pid,
823
911
  alive,
824
- status: parsed.finalResult ? "done" : alive ? "running" : "exited",
912
+ status: workerStatusLabel,
825
913
  attention,
826
914
  branch: worker.branch,
827
915
  worktreePath: worker.worktreePath,
@@ -850,6 +938,10 @@ function isFinishedWorkerStatus(status) {
850
938
  if (status.status === "exited" || status.status === "done") return true;
851
939
  return false;
852
940
  }
941
+ function isLandingBlockedWorkerStatus(status) {
942
+ if (!status.finalResult) return false;
943
+ return status.attention.state === "needs_attention" || status.attention.state === "blocked";
944
+ }
853
945
  function deriveRunStatus(fallback, workers) {
854
946
  if (workers.length === 0) return fallback;
855
947
  if (workers.some((w) => w.attention === "needs_attention" || w.attention === "stale" || w.attention === "blocked")) {
@@ -900,6 +992,10 @@ function readAvailableMemBytes() {
900
992
  }
901
993
  return os.freemem();
902
994
  }
995
+ function isActiveHarnessWorker(worker) {
996
+ const status = computeWorkerStatus(worker);
997
+ return status.alive && !status.finalResult && status.attention.state !== "done";
998
+ }
903
999
  function countActiveWorkersForRun(run) {
904
1000
  let active = 0;
905
1001
  for (const name of Object.keys(run.workers || {})) {
@@ -907,11 +1003,8 @@ function countActiveWorkersForRun(run) {
907
1003
  path5.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
908
1004
  void 0
909
1005
  );
910
- if (!worker) continue;
911
- const status = computeWorkerStatus(worker);
912
- if (status.alive && !status.finalResult && status.attention.state !== "done") {
913
- active++;
914
- }
1006
+ if (!worker || !isActiveHarnessWorker(worker)) continue;
1007
+ active++;
915
1008
  }
916
1009
  return active;
917
1010
  }
@@ -936,7 +1029,7 @@ function observeRunnerResourceGate(input) {
936
1029
  const maxConcurrentWorkers = Math.max(0, Math.min(targetCap, capacityFromTotal));
937
1030
  const slotsByCapacity = Math.max(0, maxConcurrentWorkers - activeWorkers);
938
1031
  const slotsByFreeMem = capacityFromFree;
939
- const slotsAvailable = Math.min(slotsByCapacity, slotsByFreeMem);
1032
+ let slotsAvailable = Math.min(slotsByCapacity, slotsByFreeMem);
940
1033
  let reason = null;
941
1034
  if (slotsAvailable <= 0) {
942
1035
  if (activeWorkers >= maxConcurrentWorkers) {
@@ -963,39 +1056,6 @@ function observeRunnerResourceGate(input) {
963
1056
  };
964
1057
  }
965
1058
 
966
- // src/supervisor.ts
967
- import { existsSync as existsSync9, mkdirSync as mkdirSync3 } from "node:fs";
968
- import path9 from "node:path";
969
-
970
- // src/prompt.ts
971
- function buildPrompt(input) {
972
- 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.";
973
- const progressLines = [
974
- "Structured plan progress (required when planId is set):",
975
- "- 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).",
976
- "- 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.",
977
- "- 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`).",
978
- "- 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>`.",
979
- "- Before the completion report: mark completion-report rows partial with evidence; do not skip report review.",
980
- "- After implementation: wait for report_reviewer then deep_reviewer confirmation (via MCP/session agents) before follow-up rows close.",
981
- 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."
982
- ];
983
- return [
984
- "You are running under the Kynver AgentOS runtime.",
985
- "Immediately state your plan before editing.",
986
- ownership,
987
- `Worktree: ${input.worktreePath}`,
988
- `Progress heartbeat file: ${input.heartbeatPath}`,
989
- "After each major step, append one JSON line to the heartbeat file with fields: ts, phase, summary, changedFiles, blocker.",
990
- "Final response must include files changed, verification commands, and unresolved risks.",
991
- "",
992
- ...progressLines,
993
- "",
994
- "Task:",
995
- input.task
996
- ].join("\n");
997
- }
998
-
999
1059
  // src/providers/claude.ts
1000
1060
  import { closeSync, openSync } from "node:fs";
1001
1061
  import { spawn } from "node:child_process";
@@ -1054,7 +1114,7 @@ function preflightCursorModel(model, defaultModel) {
1054
1114
  }
1055
1115
 
1056
1116
  // src/providers/claude.ts
1057
- var CLAUDE_DEFAULT_MODEL = "claude-opus-4-7";
1117
+ var CLAUDE_DEFAULT_MODEL = "claude-sonnet-4-6";
1058
1118
  var claudeProvider = {
1059
1119
  name: "claude",
1060
1120
  defaultModel: CLAUDE_DEFAULT_MODEL,
@@ -1083,12 +1143,12 @@ var claudeProvider = {
1083
1143
  "--include-partial-messages",
1084
1144
  opts.prompt
1085
1145
  ],
1086
- {
1146
+ hiddenSpawnOptions({
1087
1147
  cwd: opts.worktreePath,
1088
1148
  detached: true,
1089
1149
  stdio: ["ignore", stdoutFd, stderrFd],
1090
1150
  env: scrubClaudeEnv(process.env)
1091
- }
1151
+ })
1092
1152
  );
1093
1153
  closeSync(stdoutFd);
1094
1154
  closeSync(stderrFd);
@@ -1100,34 +1160,234 @@ var claudeProvider = {
1100
1160
  }
1101
1161
  };
1102
1162
 
1163
+ // src/model-routing.ts
1164
+ var GLOBAL_DEFAULT_MODEL = "claude-sonnet-4-6";
1165
+ function taskString(task, key) {
1166
+ const v = task[key];
1167
+ return typeof v === "string" ? v.trim() : "";
1168
+ }
1169
+ function normalizeRef(ref) {
1170
+ return ref.toLowerCase();
1171
+ }
1172
+ function resolveGlobalDefaultModel(config = loadUserConfig()) {
1173
+ const fromConfig = config.defaultModel?.trim();
1174
+ if (fromConfig) return fromConfig;
1175
+ const fromEnv = process.env.KYNVER_DEFAULT_MODEL?.trim();
1176
+ if (fromEnv) return fromEnv;
1177
+ return GLOBAL_DEFAULT_MODEL;
1178
+ }
1179
+ function inferProviderFromModel(model) {
1180
+ const m = (model ?? "").toLowerCase();
1181
+ if (!m) return "claude";
1182
+ if (m.includes("composer") || m.includes("cursor") || m.includes("codex") || m.startsWith("gpt-") || m.startsWith("gpt5")) {
1183
+ return "cursor";
1184
+ }
1185
+ return "claude";
1186
+ }
1187
+ function isOpusLane(ref, title) {
1188
+ if (ref.includes("deep") && ref.includes("review")) return true;
1189
+ if (ref.includes("security")) return true;
1190
+ if (ref.includes("plan_author") || ref.includes("plan-author")) return true;
1191
+ if (title.includes("deep review") || title.includes("security review")) return true;
1192
+ if (ref.includes("plan") && !ref.includes("review") && (ref.includes("author") || ref.includes("strategy"))) {
1193
+ return true;
1194
+ }
1195
+ return false;
1196
+ }
1197
+ function inferModelRoutingFromTask(task) {
1198
+ const ref = normalizeRef(taskString(task, "executorRef"));
1199
+ const title = taskString(task, "title").toLowerCase();
1200
+ const priority = taskString(task, "priority") || "normal";
1201
+ const roleLane = normalizeRef(taskString(task, "roleLane"));
1202
+ if (ref.includes("cursor") || ref.includes("codex") || ref.includes("composer") || ref.includes("copilot") || roleLane === "implementer" || roleLane === "repair_implementer") {
1203
+ return { provider: "cursor", rule: "lane:implementation" };
1204
+ }
1205
+ if (ref.includes("landing") || title.startsWith("land:") || title.includes(" merge")) {
1206
+ return {
1207
+ model: "claude-haiku-4-5-20251001",
1208
+ provider: "claude",
1209
+ rule: "lane:landing"
1210
+ };
1211
+ }
1212
+ if (ref.includes("review") || title.startsWith("review ") || roleLane.includes("review")) {
1213
+ if (isOpusLane(ref, title) || roleLane === "deep_reviewer") {
1214
+ return { model: "claude-opus-4-7", provider: "claude", rule: "lane:deep_review" };
1215
+ }
1216
+ return { model: "claude-sonnet-4-6", provider: "claude", rule: "lane:review" };
1217
+ }
1218
+ if (isOpusLane(ref, title) || roleLane === "plan_author") {
1219
+ return { model: "claude-opus-4-7", provider: "claude", rule: "lane:planning" };
1220
+ }
1221
+ if (priority === "critical") {
1222
+ return { model: "claude-opus-4-7", provider: "claude", rule: "priority:critical" };
1223
+ }
1224
+ if (priority === "low") {
1225
+ return {
1226
+ model: "claude-haiku-4-5-20251001",
1227
+ provider: "claude",
1228
+ rule: "priority:low"
1229
+ };
1230
+ }
1231
+ return {
1232
+ model: resolveGlobalDefaultModel(),
1233
+ provider: "claude",
1234
+ rule: "default:sonnet"
1235
+ };
1236
+ }
1237
+ function resolveWorkerLaunch(input) {
1238
+ if (input.explicitModel?.trim()) {
1239
+ const model2 = input.explicitModel.trim();
1240
+ return {
1241
+ model: model2,
1242
+ provider: input.explicitProvider?.trim() || inferProviderFromModel(model2),
1243
+ rule: "explicit:cli",
1244
+ requestedModel: model2
1245
+ };
1246
+ }
1247
+ if (input.task && Object.keys(input.task).length > 0) {
1248
+ const inferred = inferModelRoutingFromTask(input.task);
1249
+ return {
1250
+ ...inferred,
1251
+ requestedModel: inferred.model
1252
+ };
1253
+ }
1254
+ const model = resolveGlobalDefaultModel();
1255
+ return {
1256
+ model,
1257
+ provider: input.explicitProvider?.trim() || inferProviderFromModel(model),
1258
+ rule: "default:global",
1259
+ requestedModel: model
1260
+ };
1261
+ }
1262
+ function resolveModelFallback(startedModel, launchModel, providerDefault) {
1263
+ return startedModel || launchModel || providerDefault || resolveGlobalDefaultModel() || CLAUDE_DEFAULT_MODEL;
1264
+ }
1265
+
1266
+ // src/retry-limits.ts
1267
+ function positiveInt2(value, fallback) {
1268
+ const n = Number(value);
1269
+ if (!Number.isFinite(n) || n <= 0) return fallback;
1270
+ return Math.floor(n);
1271
+ }
1272
+ function readHarnessRetryLimits() {
1273
+ return {
1274
+ maxTaskAttempts: positiveInt2(process.env.KYNVER_MAX_TASK_ATTEMPTS, 3),
1275
+ dispatchCooldownMs: positiveInt2(process.env.KYNVER_DISPATCH_COOLDOWN_MS, 5e3)
1276
+ };
1277
+ }
1278
+
1279
+ // src/supervisor.ts
1280
+ import { existsSync as existsSync10, mkdirSync as mkdirSync3 } from "node:fs";
1281
+ import path10 from "node:path";
1282
+
1283
+ // src/prompt.ts
1284
+ function buildPrompt(input) {
1285
+ 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.";
1286
+ const compact = Boolean(input.model?.toLowerCase().includes("haiku"));
1287
+ const progressLines = compact ? [
1288
+ "Plan progress: when planId is set, use `kynver plan progress` for running|partial|blocked only; row `done` is MCP/session only.",
1289
+ input.planId ? `Active planId: ${input.planId}` : "No planId on this worker."
1290
+ ] : [
1291
+ "Structured plan progress (required when planId is set):",
1292
+ "- 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).",
1293
+ "- 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.",
1294
+ "- 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`).",
1295
+ "- 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>`.",
1296
+ "- Before the completion report: mark completion-report rows partial with evidence; do not skip report review.",
1297
+ "- After implementation: wait for report_reviewer then deep_reviewer confirmation (via MCP/session agents) before follow-up rows close.",
1298
+ 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."
1299
+ ];
1300
+ return [
1301
+ "You are running under the Kynver AgentOS runtime.",
1302
+ "Immediately state your plan before editing.",
1303
+ ownership,
1304
+ `Worktree: ${input.worktreePath}`,
1305
+ `Progress heartbeat file: ${input.heartbeatPath}`,
1306
+ "After each major step, append one JSON line to the heartbeat file with fields: ts, phase, summary, changedFiles, blocker.",
1307
+ "Final response must include files changed, verification commands, and unresolved risks.",
1308
+ "",
1309
+ ...progressLines,
1310
+ "",
1311
+ "Task:",
1312
+ input.task
1313
+ ].join("\n");
1314
+ }
1315
+
1103
1316
  // src/providers/cursor.ts
1104
- import { closeSync as closeSync2, existsSync as existsSync7, openSync as openSync2, readdirSync as readdirSync3 } from "node:fs";
1317
+ import { closeSync as closeSync2, existsSync as existsSync8, openSync as openSync2 } from "node:fs";
1105
1318
  import { spawn as spawn2 } from "node:child_process";
1319
+ import path7 from "node:path";
1320
+
1321
+ // src/providers/cursor-windows.ts
1322
+ import { existsSync as existsSync7, readdirSync as readdirSync3 } from "node:fs";
1106
1323
  import path6 from "node:path";
1107
- var DEFAULT_CURSOR_MODEL = "composer-2.5";
1108
- function latestVersionDir(versionsRoot) {
1324
+ var CURSOR_VERSION_DIR = /^\d{4}\.\d{1,2}\.\d{1,2}-[a-f0-9]+$/i;
1325
+ function parseCursorVersionSortKey(versionName) {
1326
+ const datePart = versionName.split("-")[0];
1327
+ const parts = datePart.split(".");
1328
+ if (parts.length !== 3) return null;
1329
+ const [year, month, day] = parts;
1330
+ if (!year || !month || !day) return null;
1331
+ return Number(`${year}${month.padStart(2, "0")}${day.padStart(2, "0")}`);
1332
+ }
1333
+ function pickLatestCursorVersionDir(agentRoot) {
1334
+ const versionsRoot = path6.join(agentRoot, "versions");
1109
1335
  if (!existsSync7(versionsRoot)) return null;
1110
- 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));
1111
- return versions[0] ? path6.join(versionsRoot, versions[0]) : null;
1112
- }
1113
- function resolveBundledCursor(versionDir) {
1336
+ let bestDir = null;
1337
+ let bestKey = -1;
1338
+ for (const entry of readdirSync3(versionsRoot, { withFileTypes: true })) {
1339
+ if (!entry.isDirectory() || !CURSOR_VERSION_DIR.test(entry.name)) continue;
1340
+ const key = parseCursorVersionSortKey(entry.name);
1341
+ if (key == null || key <= bestKey) continue;
1342
+ bestKey = key;
1343
+ bestDir = path6.join(versionsRoot, entry.name);
1344
+ }
1345
+ return bestDir;
1346
+ }
1347
+ function resolveWindowsCursorBundled(agentRoot) {
1348
+ const root = agentRoot?.trim() || path6.join(process.env.LOCALAPPDATA || "", "cursor-agent");
1349
+ const directNode = path6.join(root, "node.exe");
1350
+ const directIndex = path6.join(root, "index.js");
1351
+ if (existsSync7(directNode) && existsSync7(directIndex)) {
1352
+ return { nodeExe: directNode, indexJs: directIndex, versionDir: root };
1353
+ }
1354
+ const versionDir = pickLatestCursorVersionDir(root);
1355
+ if (!versionDir) return null;
1114
1356
  const nodeExe = path6.join(versionDir, "node.exe");
1115
1357
  const indexJs = path6.join(versionDir, "index.js");
1116
1358
  if (!existsSync7(nodeExe) || !existsSync7(indexJs)) return null;
1117
- return { executable: nodeExe, prefixArgs: [indexJs], shell: false, detached: true };
1359
+ return { nodeExe, indexJs, versionDir };
1118
1360
  }
1119
- function resolveWindowsCursorSpawn(agentBin) {
1120
- const agentRoot = path6.dirname(agentBin);
1121
- const direct = resolveBundledCursor(agentRoot);
1122
- if (direct) return direct;
1123
- const versionDir = latestVersionDir(path6.join(agentRoot, "versions"));
1124
- return versionDir ? resolveBundledCursor(versionDir) : null;
1361
+
1362
+ // src/providers/cursor.ts
1363
+ var DEFAULT_CURSOR_MODEL = "composer-2.5";
1364
+ function bundledSpawnTarget(nodeExe, indexJs, versionDir) {
1365
+ return {
1366
+ executable: nodeExe,
1367
+ prefixArgs: [indexJs],
1368
+ shell: false,
1369
+ detached: true,
1370
+ bundledVersionDir: versionDir
1371
+ };
1125
1372
  }
1126
1373
  function resolveCursorSpawn(agentBin) {
1127
- if (process.platform === "win32" && /\.(cmd|bat)$/i.test(agentBin)) {
1128
- const bundled = resolveWindowsCursorSpawn(agentBin);
1129
- if (bundled) return bundled;
1130
- return { executable: agentBin, prefixArgs: [], shell: true, detached: false };
1374
+ if (process.platform === "win32") {
1375
+ const isCursorWrapper = /\.(cmd|bat)$/i.test(agentBin);
1376
+ const isBundledNode = /node\.exe$/i.test(agentBin) && existsSync8(path7.join(path7.dirname(agentBin), "index.js"));
1377
+ const isDefaultShim = agentBin === "agent";
1378
+ if (isCursorWrapper || isBundledNode || isDefaultShim) {
1379
+ const bundled = isCursorWrapper ? resolveWindowsCursorBundled(path7.dirname(agentBin)) : isBundledNode ? {
1380
+ nodeExe: agentBin,
1381
+ indexJs: path7.join(path7.dirname(agentBin), "index.js"),
1382
+ versionDir: path7.dirname(agentBin)
1383
+ } : resolveWindowsCursorBundled();
1384
+ if (bundled) {
1385
+ return bundledSpawnTarget(bundled.nodeExe, bundled.indexJs, bundled.versionDir);
1386
+ }
1387
+ throw new Error(
1388
+ "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."
1389
+ );
1390
+ }
1131
1391
  }
1132
1392
  return { executable: agentBin, prefixArgs: [], shell: false, detached: true };
1133
1393
  }
@@ -1135,11 +1395,23 @@ function resolveAgentBin() {
1135
1395
  const configured = process.env.KYNVER_CURSOR_AGENT_BIN?.trim() || process.env.CURSOR_AGENT_BIN?.trim();
1136
1396
  if (configured) return configured;
1137
1397
  if (process.platform === "win32") {
1138
- const localAgent = path6.join(process.env.LOCALAPPDATA || "", "cursor-agent", "agent.cmd");
1139
- if (existsSync7(localAgent)) return localAgent;
1398
+ const bundled = resolveWindowsCursorBundled(
1399
+ process.env.KYNVER_CURSOR_AGENT_ROOT?.trim() || void 0
1400
+ );
1401
+ if (bundled) return bundled.nodeExe;
1402
+ const localAgent = path7.join(process.env.LOCALAPPDATA || "", "cursor-agent", "agent.cmd");
1403
+ if (existsSync8(localAgent)) return localAgent;
1140
1404
  }
1141
1405
  return "agent";
1142
1406
  }
1407
+ function cursorWorkerEnv(agentBin, spawnTarget) {
1408
+ return {
1409
+ ...process.env,
1410
+ CI: "1",
1411
+ NO_COLOR: "1",
1412
+ ...spawnTarget.bundledVersionDir ? { CURSOR_INVOKED_AS: path7.basename(agentBin) || "agent.cmd" } : {}
1413
+ };
1414
+ }
1143
1415
  var cursorProvider = {
1144
1416
  name: "cursor",
1145
1417
  defaultModel: DEFAULT_CURSOR_MODEL,
@@ -1172,16 +1444,13 @@ var cursorProvider = {
1172
1444
  model,
1173
1445
  opts.prompt
1174
1446
  ],
1175
- {
1447
+ hiddenSpawnOptions({
1176
1448
  cwd: opts.worktreePath,
1177
1449
  detached: spawnTarget.detached,
1178
1450
  shell: spawnTarget.shell,
1179
1451
  stdio: ["ignore", stdoutFd, stderrFd],
1180
- env: {
1181
- ...process.env,
1182
- ...spawnTarget.prefixArgs.length > 0 ? { CURSOR_INVOKED_AS: path6.basename(agentBin) } : {}
1183
- }
1184
- }
1452
+ env: cursorWorkerEnv(agentBin, spawnTarget)
1453
+ })
1185
1454
  );
1186
1455
  closeSync2(stdoutFd);
1187
1456
  closeSync2(stderrFd);
@@ -1213,12 +1482,12 @@ function resolveWorkerProvider(name) {
1213
1482
 
1214
1483
  // src/auto-complete.ts
1215
1484
  import { spawn as spawn3 } from "node:child_process";
1216
- import { existsSync as existsSync8, openSync as openSync3, closeSync as closeSync3 } from "node:fs";
1217
- import path8 from "node:path";
1485
+ import { existsSync as existsSync9, openSync as openSync3, closeSync as closeSync3 } from "node:fs";
1486
+ import path9 from "node:path";
1218
1487
  import { fileURLToPath } from "node:url";
1219
1488
 
1220
1489
  // src/worker-ops.ts
1221
- import path7 from "node:path";
1490
+ import path8 from "node:path";
1222
1491
  async function postCompletion(url, secret, body) {
1223
1492
  const res = await fetch(url, {
1224
1493
  method: "POST",
@@ -1247,9 +1516,13 @@ function persistCompletionBlocker(worker, reason) {
1247
1516
  else delete worker.completionBlocker;
1248
1517
  saveWorker(worker.runId, worker);
1249
1518
  }
1519
+ function workerStatusOptions(run) {
1520
+ return run ? { base: run.base, baseCommit: run.baseCommit } : {};
1521
+ }
1250
1522
  async function tryCompleteWorker(args) {
1251
1523
  const worker = loadWorker(String(args.run), String(args.name));
1252
- const status = computeWorkerStatus(worker);
1524
+ const run = loadRun(worker.runId);
1525
+ const status = computeWorkerStatus(worker, workerStatusOptions(run));
1253
1526
  const agentOsId = (args.agentOsId ? String(args.agentOsId) : worker.agentOsId) || "";
1254
1527
  const taskId = (args.taskId ? String(args.taskId) : worker.taskId) || null;
1255
1528
  if (!agentOsId) {
@@ -1293,7 +1566,8 @@ async function tryCompleteWorker(args) {
1293
1566
  async function completeWorker(args) {
1294
1567
  try {
1295
1568
  const worker = loadWorker(String(args.run), String(args.name));
1296
- const status = computeWorkerStatus(worker);
1569
+ const run = loadRun(worker.runId);
1570
+ const status = computeWorkerStatus(worker, workerStatusOptions(run));
1297
1571
  const agentOsId = (args.agentOsId ? String(args.agentOsId) : worker.agentOsId) || "";
1298
1572
  const taskId = (args.taskId ? String(args.taskId) : worker.taskId) || null;
1299
1573
  if (!agentOsId) {
@@ -1340,8 +1614,9 @@ async function completeWorker(args) {
1340
1614
  }
1341
1615
  function workerStatus(args) {
1342
1616
  const worker = loadWorker(String(args.run), String(args.name));
1343
- const status = computeWorkerStatus(worker);
1344
- writeJson(path7.join(worker.workerDir, "last-status.json"), status);
1617
+ const run = loadRun(worker.runId);
1618
+ const status = computeWorkerStatus(worker, workerStatusOptions(run));
1619
+ writeJson(path8.join(worker.workerDir, "last-status.json"), status);
1345
1620
  console.log(JSON.stringify(status, null, 2));
1346
1621
  }
1347
1622
  function runStatus(args) {
@@ -1349,20 +1624,27 @@ function runStatus(args) {
1349
1624
  const names = Object.keys(run.workers || {});
1350
1625
  const workers = names.map((name) => {
1351
1626
  const worker = readJson(
1352
- path7.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
1627
+ path8.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
1353
1628
  void 0
1354
1629
  );
1355
1630
  if (!worker) {
1356
1631
  return { worker: name, status: "missing", attention: "needs_attention", attentionReason: "worker.json not found" };
1357
1632
  }
1358
- const status = computeWorkerStatus(worker, { base: run.base });
1633
+ const status = computeWorkerStatus(worker, {
1634
+ base: run.base,
1635
+ baseCommit: run.baseCommit
1636
+ });
1637
+ const headCommit = status.gitAncestry.headIsAncestorOfBase === false && status.gitAncestry.head ? status.gitAncestry.head : void 0;
1359
1638
  const rawBlocker = worker.completionBlocker;
1360
1639
  const completionBlocker = typeof rawBlocker === "string" && rawBlocker ? rawBlocker : void 0;
1640
+ const boardStatus = completionBlocker ? "blocked" : status.status;
1641
+ const boardAttention = completionBlocker ? "blocked" : status.attention.state;
1361
1642
  return {
1362
1643
  worker: status.worker,
1363
- status: completionBlocker ? "blocked" : status.status,
1364
- attention: completionBlocker ? "blocked" : status.attention.state,
1644
+ status: boardStatus,
1645
+ attention: boardAttention,
1365
1646
  attentionReason: completionBlocker ?? status.attention.reason,
1647
+ landingBlocked: status.finalResult ? boardAttention === "needs_attention" || boardAttention === "blocked" : false,
1366
1648
  pid: status.pid,
1367
1649
  alive: status.alive,
1368
1650
  currentTool: status.currentTool,
@@ -1371,7 +1653,14 @@ function runStatus(args) {
1371
1653
  lastHeartbeatSummary: status.lastHeartbeatSummary,
1372
1654
  heartbeatBlocker: status.heartbeatBlocker,
1373
1655
  changedFileCount: status.changedFiles.length,
1656
+ changedFiles: status.changedFiles,
1374
1657
  branch: status.branch,
1658
+ model: typeof worker.model === "string" ? worker.model : void 0,
1659
+ routingRule: typeof worker.routingRule === "string" ? worker.routingRule : void 0,
1660
+ requestedModel: typeof worker.requestedModel === "string" ? worker.requestedModel : void 0,
1661
+ headCommit,
1662
+ gitAncestry: status.gitAncestry,
1663
+ finalResult: status.finalResult,
1375
1664
  ancestry: status.gitAncestry.relation,
1376
1665
  ancestryChecked: status.gitAncestry.checked
1377
1666
  };
@@ -1385,7 +1674,7 @@ function runStatus(args) {
1385
1674
  needsAttention: workers.filter((w) => w.attention && w.attention !== "ok" && w.attention !== "done").map((w) => w.worker),
1386
1675
  workers
1387
1676
  };
1388
- writeJson(path7.join(runDirectory(run.id), "last-board.json"), board);
1677
+ writeJson(path8.join(runDirectory(run.id), "last-board.json"), board);
1389
1678
  console.log(JSON.stringify(board, null, 2));
1390
1679
  }
1391
1680
  function tailWorker(args) {
@@ -1524,12 +1813,12 @@ async function autoCompleteWorkerCli(raw) {
1524
1813
  }
1525
1814
  }
1526
1815
  function resolveDefaultCliPath() {
1527
- return path8.join(fileURLToPath(new URL(".", import.meta.url)), "cli.js");
1816
+ return path9.join(fileURLToPath(new URL(".", import.meta.url)), "cli.js");
1528
1817
  }
1529
1818
  function spawnCompletionSidecar(opts) {
1530
1819
  const cliPath = opts.cliPath ?? resolveDefaultCliPath();
1531
- if (!existsSync8(cliPath)) return void 0;
1532
- const logPath = path8.join(opts.workerDir, "auto-complete.log");
1820
+ if (!existsSync9(cliPath)) return void 0;
1821
+ const logPath = path9.join(opts.workerDir, "auto-complete.log");
1533
1822
  let logFd;
1534
1823
  try {
1535
1824
  logFd = openSync3(logPath, "a");
@@ -1555,11 +1844,15 @@ function spawnCompletionSidecar(opts) {
1555
1844
  if (opts.baseUrl) args.push("--base-url", opts.baseUrl);
1556
1845
  if (opts.secret) args.push("--secret", opts.secret);
1557
1846
  try {
1558
- const child = spawn3(nodeExecutable, args, {
1559
- detached: true,
1560
- stdio,
1561
- env: process.env
1562
- });
1847
+ const child = spawn3(
1848
+ nodeExecutable,
1849
+ args,
1850
+ hiddenSpawnOptions({
1851
+ detached: true,
1852
+ stdio,
1853
+ env: process.env
1854
+ })
1855
+ );
1563
1856
  if (logFd !== void 0) closeSync3(logFd);
1564
1857
  child.unref();
1565
1858
  return { pid: child.pid, logPath, cliPath };
@@ -1583,8 +1876,17 @@ function spawnWorkerProcess(run, opts) {
1583
1876
  const name = safeSlug(rawName);
1584
1877
  if (run.workers?.[name]) throw new Error(`worker already exists in run ${run.id}: ${name}`);
1585
1878
  if (!opts.task) throw new Error(`missing task text for worker ${name}`);
1586
- const provider = resolveWorkerProvider(opts.provider);
1587
- let launchModel = opts.model;
1879
+ const routing = opts.routingRule || opts.requestedModel ? {
1880
+ provider: opts.provider || "claude",
1881
+ model: opts.model,
1882
+ rule: opts.routingRule || "explicit:spawn",
1883
+ requestedModel: opts.requestedModel ?? opts.model
1884
+ } : resolveWorkerLaunch({
1885
+ explicitModel: opts.model,
1886
+ explicitProvider: opts.provider
1887
+ });
1888
+ const provider = resolveWorkerProvider(routing.provider);
1889
+ let launchModel = routing.model;
1588
1890
  if (provider.preflightModel) {
1589
1891
  const preflight = provider.preflightModel(opts.model);
1590
1892
  if (!preflight.ok) {
@@ -1598,21 +1900,24 @@ function spawnWorkerProcess(run, opts) {
1598
1900
  launchModel = preflight.model;
1599
1901
  }
1600
1902
  const { worktreesDir } = getPaths();
1601
- const workerDir = path9.join(runDirectory(run.id), "workers", name);
1903
+ const workerDir = path10.join(runDirectory(run.id), "workers", name);
1602
1904
  mkdirSync3(workerDir, { recursive: true });
1603
- const worktreePath = path9.join(worktreesDir, run.id, name);
1905
+ const worktreePath = path10.join(worktreesDir, run.id, name);
1604
1906
  const branch = opts.branch || `agent/${run.id}/${name}`;
1605
- if (existsSync9(worktreePath)) throw new Error(`worktree path already exists: ${worktreePath}`);
1907
+ if (existsSync10(worktreePath)) throw new Error(`worktree path already exists: ${worktreePath}`);
1606
1908
  git(run.repo, ["fetch", "origin", "--prune"], { allowFailure: true });
1607
1909
  git(run.repo, ["worktree", "add", "-b", branch, worktreePath, run.baseCommit], { throwError: true });
1608
- const stdoutPath = path9.join(workerDir, "stdout.jsonl");
1609
- const stderrPath = path9.join(workerDir, "stderr.log");
1610
- const heartbeatPath = path9.join(workerDir, "heartbeat.jsonl");
1910
+ const stdoutPath = path10.join(workerDir, "stdout.jsonl");
1911
+ const stderrPath = path10.join(workerDir, "stderr.log");
1912
+ const heartbeatPath = path10.join(workerDir, "heartbeat.jsonl");
1611
1913
  const prompt = buildPrompt({
1612
1914
  task: opts.task,
1613
1915
  ownedPaths: opts.ownedPaths || [],
1614
1916
  worktreePath,
1615
- heartbeatPath
1917
+ heartbeatPath,
1918
+ planId: opts.planId,
1919
+ taskId: opts.taskId,
1920
+ model: launchModel
1616
1921
  });
1617
1922
  let started;
1618
1923
  try {
@@ -1634,7 +1939,7 @@ function spawnWorkerProcess(run, opts) {
1634
1939
  git(run.repo, ["branch", "-D", branch], { allowFailure: true });
1635
1940
  throw error;
1636
1941
  }
1637
- const model = started.model || launchModel || provider.defaultModel || "claude-opus-4-7";
1942
+ const model = resolveModelFallback(started.model, launchModel, provider.defaultModel);
1638
1943
  const worker = {
1639
1944
  name,
1640
1945
  runId: run.id,
@@ -1653,21 +1958,32 @@ function spawnWorkerProcess(run, opts) {
1653
1958
  ...opts.planId ? { planId: String(opts.planId) } : {},
1654
1959
  ...opts.leaseOwner ? { leaseOwner: String(opts.leaseOwner) } : {},
1655
1960
  ...opts.dispatched ? { dispatched: true } : {},
1961
+ routingRule: routing.rule,
1962
+ ...routing.requestedModel ? { requestedModel: routing.requestedModel } : {},
1656
1963
  startedAt: (/* @__PURE__ */ new Date()).toISOString()
1657
1964
  };
1658
1965
  saveWorker(run.id, worker);
1659
- run.workers = { ...run.workers || {}, [name]: { workerDir, statusPath: path9.join(workerDir, "worker.json") } };
1966
+ run.workers = { ...run.workers || {}, [name]: { workerDir, statusPath: path10.join(workerDir, "worker.json") } };
1660
1967
  run.status = "running";
1661
1968
  saveRun(run);
1662
1969
  if (worker.agentOsId && worker.taskId) {
1970
+ let sidecarSpawned;
1663
1971
  try {
1664
- spawnCompletionSidecar({
1972
+ sidecarSpawned = spawnCompletionSidecar({
1665
1973
  runId: run.id,
1666
1974
  workerName: name,
1667
1975
  workerDir,
1668
1976
  agentOsId: worker.agentOsId
1669
1977
  });
1670
- } catch {
1978
+ } catch (error) {
1979
+ const reason = `completion sidecar failed to spawn: ${error.message}`;
1980
+ worker.completionBlocker = reason;
1981
+ saveWorker(run.id, worker);
1982
+ }
1983
+ if (!sidecarSpawned) {
1984
+ const reason = "completion sidecar failed to spawn (CLI not found or spawn error)";
1985
+ worker.completionBlocker = reason;
1986
+ saveWorker(run.id, worker);
1671
1987
  }
1672
1988
  }
1673
1989
  return worker;
@@ -1802,17 +2118,34 @@ async function dispatchRun(args) {
1802
2118
  console.log(JSON.stringify(summary2, null, 2));
1803
2119
  return;
1804
2120
  }
2121
+ const retryLimits = readHarnessRetryLimits();
1805
2122
  const outcomes = [];
1806
2123
  for (const decision of result.started) {
1807
2124
  const task = decision.task;
2125
+ const attempt = Number(task.attempt) || 1;
2126
+ if (attempt > retryLimits.maxTaskAttempts) {
2127
+ outcomes.push({
2128
+ taskId: task.id,
2129
+ started: false,
2130
+ error: `task attempt ${attempt} exceeds KYNVER_MAX_TASK_ATTEMPTS (${retryLimits.maxTaskAttempts})`
2131
+ });
2132
+ continue;
2133
+ }
1808
2134
  const name = safeSlug(`t-${task.id}-a${task.attempt}`);
2135
+ const routing = resolveWorkerLaunch({
2136
+ explicitModel: args.model ? String(args.model) : void 0,
2137
+ task
2138
+ });
1809
2139
  try {
1810
2140
  const planId = task.planId ? String(task.planId) : void 0;
1811
2141
  const worker = spawnWorkerProcess(run, {
1812
2142
  name,
1813
2143
  task: buildDispatchTaskText(task, agentOsId),
1814
2144
  ownedPaths: args.owned ? String(args.owned).split(",").map((s) => s.trim()).filter(Boolean) : [],
1815
- model: args.model ? String(args.model) : void 0,
2145
+ model: routing.model,
2146
+ provider: routing.provider,
2147
+ routingRule: routing.rule,
2148
+ requestedModel: routing.requestedModel,
1816
2149
  agentOsId,
1817
2150
  taskId: String(task.id),
1818
2151
  planId,
@@ -1824,7 +2157,10 @@ async function dispatchRun(args) {
1824
2157
  started: true,
1825
2158
  worker: worker.name,
1826
2159
  pid: worker.pid,
1827
- branch: worker.branch
2160
+ branch: worker.branch,
2161
+ model: worker.model,
2162
+ provider: routing.provider,
2163
+ routingRule: routing.rule
1828
2164
  });
1829
2165
  } catch (error) {
1830
2166
  const releaseUrl = `${base}/api/agent-os/by-id/${encodeURIComponent(agentOsId)}/tasks/${encodeURIComponent(String(task.id))}/release`;
@@ -1879,7 +2215,7 @@ function redactHarness(text, secret) {
1879
2215
  }
1880
2216
 
1881
2217
  // src/validate.ts
1882
- import path10 from "node:path";
2218
+ import path11 from "node:path";
1883
2219
  var RUN_ID_RE = /^[a-z0-9][a-z0-9._-]{0,127}$/i;
1884
2220
  var WORKER_NAME_RE = /^[a-z0-9][a-z0-9._-]{0,63}$/i;
1885
2221
  function validateRunId(runId) {
@@ -1893,15 +2229,15 @@ function validateWorkerName(name) {
1893
2229
  return trimmed;
1894
2230
  }
1895
2231
  function validateRepo(repo) {
1896
- const resolved = path10.resolve(repo);
2232
+ const resolved = path11.resolve(repo);
1897
2233
  if (resolved.includes("..")) throw new Error("repo path must not contain .. segments");
1898
2234
  return resolved;
1899
2235
  }
1900
2236
  function validateOwnedPaths(repoRoot, ownedPaths) {
1901
2237
  return ownedPaths.map((owned) => {
1902
- const resolved = path10.resolve(repoRoot, owned);
1903
- const rel = path10.relative(repoRoot, resolved);
1904
- if (rel.startsWith("..") || path10.isAbsolute(rel)) {
2238
+ const resolved = path11.resolve(repoRoot, owned);
2239
+ const rel = path11.relative(repoRoot, resolved);
2240
+ if (rel.startsWith("..") || path11.isAbsolute(rel)) {
1905
2241
  throw new Error(`owned path escapes repo: ${owned}`);
1906
2242
  }
1907
2243
  return resolved;
@@ -1913,14 +2249,14 @@ function validateTailLines(lines) {
1913
2249
  }
1914
2250
 
1915
2251
  // src/worktree.ts
1916
- import { existsSync as existsSync10, mkdirSync as mkdirSync4 } from "node:fs";
1917
- import path11 from "node:path";
2252
+ import { existsSync as existsSync11, mkdirSync as mkdirSync4 } from "node:fs";
2253
+ import path12 from "node:path";
1918
2254
  function createRun(args) {
1919
2255
  const repo = validateRepo(required(String(args.repo || ""), "--repo"));
1920
2256
  ensureGitRepo(repo);
1921
2257
  const id = args.id ? validateRunId(String(args.id)) : timestampSlug(String(args.name || "run"));
1922
2258
  const dir = runDirectory(id);
1923
- if (existsSync10(dir)) failExists(`run already exists: ${id}`);
2259
+ if (existsSync11(dir)) failExists(`run already exists: ${id}`);
1924
2260
  mkdirSync4(dir, { recursive: true });
1925
2261
  const base = String(args.base || "origin/main");
1926
2262
  const baseCommit = git(repo, ["rev-parse", base]).trim();
@@ -1934,12 +2270,12 @@ function createRun(args) {
1934
2270
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
1935
2271
  workers: {}
1936
2272
  };
1937
- writeJson(path11.join(dir, "run.json"), run);
2273
+ writeJson(path12.join(dir, "run.json"), run);
1938
2274
  console.log(JSON.stringify({ runId: id, runDir: dir, repo, base, baseCommit }, null, 2));
1939
2275
  }
1940
2276
  function listRuns() {
1941
2277
  const { runsDir } = getPaths();
1942
- const rows = listRunIds(runsDir).map((id) => readJson(path11.join(runDirectory(id), "run.json"), void 0)).filter(Boolean).map((run) => ({
2278
+ const rows = listRunIds(runsDir).map((id) => readJson(path12.join(runDirectory(id), "run.json"), void 0)).filter(Boolean).map((run) => ({
1943
2279
  id: run.id,
1944
2280
  name: run.name,
1945
2281
  status: run.status,
@@ -1954,7 +2290,7 @@ function failExists(message) {
1954
2290
  }
1955
2291
 
1956
2292
  // src/sweep.ts
1957
- import path12 from "node:path";
2293
+ import path13 from "node:path";
1958
2294
  async function sweepRun(args) {
1959
2295
  const pipeline = args.pipeline === true || args.pipeline === "true";
1960
2296
  try {
@@ -1966,7 +2302,7 @@ async function sweepRun(args) {
1966
2302
  const releasedLocalOrphans = [];
1967
2303
  for (const name of Object.keys(run.workers || {})) {
1968
2304
  const worker = readJson(
1969
- path12.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
2305
+ path13.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
1970
2306
  void 0
1971
2307
  );
1972
2308
  if (!worker || !worker.dispatched || !worker.taskId) continue;
@@ -2013,10 +2349,13 @@ import { mkdirSync as mkdirSync5, realpathSync } from "node:fs";
2013
2349
  import { fileURLToPath as fileURLToPath2 } from "node:url";
2014
2350
 
2015
2351
  // src/pipeline-tick.ts
2352
+ import path17 from "node:path";
2353
+
2354
+ // src/stale-reconcile.ts
2016
2355
  import path15 from "node:path";
2017
2356
 
2018
2357
  // src/finalize.ts
2019
- import path13 from "node:path";
2358
+ import path14 from "node:path";
2020
2359
  var ACTIVE_RUN_STATUSES = /* @__PURE__ */ new Set(["running", "dispatching", "pending", "queued"]);
2021
2360
  function terminalStatusFor(run) {
2022
2361
  const names = Object.keys(run.workers || {});
@@ -2024,13 +2363,17 @@ function terminalStatusFor(run) {
2024
2363
  let anyAlive = false;
2025
2364
  let anyResult = false;
2026
2365
  let anyCompletionBlocked = false;
2366
+ let anyLandingBlocked = false;
2027
2367
  for (const name of names) {
2028
2368
  const worker = readJson(
2029
- path13.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
2369
+ path14.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
2030
2370
  void 0
2031
2371
  );
2032
2372
  if (!worker) continue;
2033
- const status = computeWorkerStatus(worker);
2373
+ const status = computeWorkerStatus(worker, {
2374
+ base: run.base,
2375
+ baseCommit: run.baseCommit
2376
+ });
2034
2377
  if (status.alive && !status.finalResult) {
2035
2378
  anyAlive = true;
2036
2379
  break;
@@ -2038,10 +2381,14 @@ function terminalStatusFor(run) {
2038
2381
  if (typeof worker.completionBlocker === "string" && worker.completionBlocker) {
2039
2382
  anyCompletionBlocked = true;
2040
2383
  }
2041
- if (status.finalResult) anyResult = true;
2384
+ if (isLandingBlockedWorkerStatus(status)) {
2385
+ anyLandingBlocked = true;
2386
+ }
2387
+ if (status.finalResult && status.attention.state === "done") anyResult = true;
2042
2388
  }
2043
2389
  if (anyAlive) return null;
2044
2390
  if (anyCompletionBlocked) return null;
2391
+ if (anyLandingBlocked) return null;
2045
2392
  return anyResult ? "completed" : "failed";
2046
2393
  }
2047
2394
  function finalizeStaleRuns() {
@@ -2058,8 +2405,82 @@ function finalizeStaleRuns() {
2058
2405
  return finalized;
2059
2406
  }
2060
2407
 
2408
+ // src/stale-reconcile.ts
2409
+ var STALE_RECONCILE_HEARTBEAT_MS = 15 * 60 * 1e3;
2410
+ function staleReconcileDisabled() {
2411
+ return process.env.KYNVER_NO_STALE_CLEANUP === "1";
2412
+ }
2413
+ function reconcileStaleWorkers() {
2414
+ if (staleReconcileDisabled()) {
2415
+ return { workers: [], finalizedRuns: finalizeStaleRuns() };
2416
+ }
2417
+ const outcomes = [];
2418
+ const now = Date.now();
2419
+ for (const run of listRunRecords()) {
2420
+ for (const name of Object.keys(run.workers || {})) {
2421
+ const workerPath = path15.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json");
2422
+ const worker = readJson(workerPath, void 0);
2423
+ if (!worker || worker.status !== "running") {
2424
+ outcomes.push({
2425
+ runId: run.id,
2426
+ worker: name,
2427
+ action: "skipped",
2428
+ reason: worker ? `worker status is ${worker.status}` : "worker.json missing"
2429
+ });
2430
+ continue;
2431
+ }
2432
+ const status = computeWorkerStatus(worker, { base: run.base, baseCommit: run.baseCommit });
2433
+ if (status.finalResult) {
2434
+ outcomes.push({ runId: run.id, worker: name, action: "skipped", reason: "final result present" });
2435
+ continue;
2436
+ }
2437
+ if (!status.alive) {
2438
+ const nextStatus = status.attention.state === "blocked" ? "blocked" : status.status === "done" ? "done" : "exited";
2439
+ worker.status = nextStatus;
2440
+ worker.reconciledAt = (/* @__PURE__ */ new Date()).toISOString();
2441
+ worker.reconcileReason = status.attention.reason;
2442
+ saveWorker(run.id, worker);
2443
+ outcomes.push({
2444
+ runId: run.id,
2445
+ worker: name,
2446
+ action: "marked_exited",
2447
+ reason: status.attention.reason
2448
+ });
2449
+ continue;
2450
+ }
2451
+ if (status.attention.state === "stale" && worker.pid && isPidAlive(worker.pid)) {
2452
+ const hbMs = status.lastHeartbeatAt ? Date.parse(status.lastHeartbeatAt) : NaN;
2453
+ const actMs = status.lastActivityAt ? Date.parse(status.lastActivityAt) : NaN;
2454
+ const hbStale = !Number.isFinite(hbMs) || now - hbMs > STALE_RECONCILE_HEARTBEAT_MS;
2455
+ const actStale = Number.isFinite(actMs) && now - actMs > STALE_MS;
2456
+ if (hbStale && actStale) {
2457
+ killWorkerProcess(worker.pid, "SIGTERM");
2458
+ worker.status = "exited";
2459
+ worker.reconciledAt = (/* @__PURE__ */ new Date()).toISOString();
2460
+ worker.reconcileReason = `reconciled stale worker: ${status.attention.reason}`;
2461
+ saveWorker(run.id, worker);
2462
+ outcomes.push({
2463
+ runId: run.id,
2464
+ worker: name,
2465
+ action: "killed_stale",
2466
+ reason: status.attention.reason
2467
+ });
2468
+ continue;
2469
+ }
2470
+ }
2471
+ outcomes.push({
2472
+ runId: run.id,
2473
+ worker: name,
2474
+ action: "skipped",
2475
+ reason: status.attention.reason
2476
+ });
2477
+ }
2478
+ }
2479
+ return { workers: outcomes, finalizedRuns: finalizeStaleRuns() };
2480
+ }
2481
+
2061
2482
  // src/plan-progress-daemon-sync.ts
2062
- import path14 from "node:path";
2483
+ import path16 from "node:path";
2063
2484
 
2064
2485
  // src/plan-progress-sync.ts
2065
2486
  async function syncPlanProgress(args) {
@@ -2083,7 +2504,7 @@ async function syncActiveWorkerPlanProgress(runId, args) {
2083
2504
  const outcomes = [];
2084
2505
  for (const name of Object.keys(run.workers || {})) {
2085
2506
  const worker = readJson(
2086
- path14.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
2507
+ path16.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
2087
2508
  void 0
2088
2509
  );
2089
2510
  if (!worker?.dispatched || !worker.taskId) continue;
@@ -2137,7 +2558,7 @@ async function completeFinishedWorkers(runId, args) {
2137
2558
  const outcomes = [];
2138
2559
  for (const name of Object.keys(run.workers || {})) {
2139
2560
  const worker = readJson(
2140
- path15.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
2561
+ path17.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
2141
2562
  void 0
2142
2563
  );
2143
2564
  if (!worker?.taskId) continue;
@@ -2172,7 +2593,7 @@ async function runPipelineTick(args) {
2172
2593
  const execute = args.execute !== false && args.execute !== "false";
2173
2594
  runStatus({ run: runId });
2174
2595
  const completedWorkers = await completeFinishedWorkers(runId, args);
2175
- const finalizedStaleRuns = finalizeStaleRuns();
2596
+ const staleReconcile = reconcileStaleWorkers();
2176
2597
  const planProgressSync = await syncActiveWorkerPlanProgress(runId, args);
2177
2598
  const workspacePrefs = await fetchWorkspaceRuntimePreferences(agentOsId, args);
2178
2599
  const resourceGate = observeRunnerResourceGate({
@@ -2213,7 +2634,7 @@ async function runPipelineTick(args) {
2213
2634
  execute,
2214
2635
  resourceGate,
2215
2636
  completedWorkers,
2216
- finalizedStaleRuns,
2637
+ staleReconcile,
2217
2638
  planProgressSync,
2218
2639
  operatorTick,
2219
2640
  sweep,
@@ -2431,6 +2852,7 @@ if (isCliEntry) {
2431
2852
  }
2432
2853
  export {
2433
2854
  DEFAULT_DISPATCH_LEASE_MS,
2855
+ assessWorkerLanding,
2434
2856
  autoCompleteWorker,
2435
2857
  autoCompleteWorkerCli,
2436
2858
  buildDispatchTaskText,
@@ -2443,6 +2865,7 @@ export {
2443
2865
  dispatchRun,
2444
2866
  getHarnessPaths,
2445
2867
  isFinishedWorkerStatus,
2868
+ isLandingBlockedWorkerStatus,
2446
2869
  listRuns,
2447
2870
  loadUserConfig,
2448
2871
  main,