@kynver-app/runtime 0.1.19 → 0.1.22

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
@@ -182,7 +182,7 @@ function resolveConfiguredBaseUrl(argsBaseUrl) {
182
182
  return baseUrl ? trimTrailingSlash(String(baseUrl)) : void 0;
183
183
  }
184
184
  function resolveConfiguredCallbackSecret(argsSecret, agentOsId) {
185
- const scoped = argsSecret || loadRunnerToken(agentOsId) || loadRunnerToken(loadUserConfig().agentOsId);
185
+ const scoped = argsSecret || loadRunnerToken(agentOsId) || (agentOsId ? void 0 : loadRunnerToken(loadUserConfig().agentOsId));
186
186
  if (scoped) return String(scoped);
187
187
  const globalSecret = process.env.KYNVER_RUNTIME_SECRET || process.env.OPENCLAW_CRON_SECRET;
188
188
  if (globalSecret) {
@@ -223,6 +223,23 @@ async function refreshRunnerToken(agentOsId, opts) {
223
223
  return null;
224
224
  }
225
225
  }
226
+ async function refreshRunnerTokenForAuthFailure(rejectedSecret, agentOsId, opts) {
227
+ const apiKey = loadApiKey();
228
+ const baseUrl = resolveConfiguredBaseUrl(opts?.baseUrl);
229
+ if (!apiKey) return { ok: false, reason: "KYNVER_API_KEY is required to refresh a rejected runner token" };
230
+ if (!agentOsId) return { ok: false, reason: "agentOsId is required to refresh a rejected runner token" };
231
+ if (!baseUrl) return { ok: false, reason: "KYNVER_API_URL or --base-url is required to refresh a rejected runner token" };
232
+ try {
233
+ const token = await fetchRunnerCredential(agentOsId, { baseUrl, apiKey });
234
+ if (token && token !== rejectedSecret) {
235
+ saveRunnerToken(agentOsId, token);
236
+ return { ok: true, token };
237
+ }
238
+ return { ok: false, reason: "runner credential refresh returned the rejected token" };
239
+ } catch (error) {
240
+ return { ok: false, reason: error.message };
241
+ }
242
+ }
226
243
  async function fetchRunnerCredential(agentOsId, opts) {
227
244
  const apiKey = opts?.apiKey || loadApiKey();
228
245
  if (!apiKey) throw new Error("API key required \u2014 run `kynver login` first");
@@ -375,6 +392,14 @@ async function postJson(url, secret, body) {
375
392
  }
376
393
  return { ok: res.ok, status: res.status, response };
377
394
  }
395
+ async function postJsonWithCredentialRefresh(url, secret, body, opts) {
396
+ const first = await postJson(url, secret, body);
397
+ if (first.ok || first.status !== 401) return first;
398
+ const refreshed = await refreshRunnerTokenForAuthFailure(secret, opts.agentOsId, { baseUrl: opts.baseUrl });
399
+ if (!refreshed.ok) return { ...first, authRefreshFailure: refreshed.reason };
400
+ const retry = await postJson(url, refreshed.token, body);
401
+ return { ...retry, refreshedAuth: true };
402
+ }
378
403
  async function getJson(url, secret) {
379
404
  const res = await fetch(url, {
380
405
  method: "GET",
@@ -396,12 +421,12 @@ var DEFAULT_CRITICAL_FREE_BYTES = 15 * 1024 * 1024 * 1024;
396
421
  var DEFAULT_MAX_USED_PERCENT = 80;
397
422
  var DEFAULT_HARD_MAX_USED_PERCENT = 90;
398
423
  function observeRunnerDiskGate(input = {}) {
399
- const path17 = input.diskPath?.trim() || "/";
424
+ const path18 = input.diskPath?.trim() || "/";
400
425
  const warnBelowBytes = input.diskFreeWarnBytes ?? DEFAULT_WARN_FREE_BYTES;
401
426
  const criticalBelowBytes = input.diskFreeCriticalBytes ?? DEFAULT_CRITICAL_FREE_BYTES;
402
427
  const maxUsedPercent = input.diskMaxUsedPercent ?? DEFAULT_MAX_USED_PERCENT;
403
428
  const hardMaxUsedPercent = input.diskHardMaxUsedPercent ?? DEFAULT_HARD_MAX_USED_PERCENT;
404
- const stats = statfsSync(path17);
429
+ const stats = statfsSync(path18);
405
430
  const freeBytes = Number(stats.bavail) * Number(stats.bsize);
406
431
  const totalBytes = Number(stats.blocks) * Number(stats.bsize);
407
432
  const usedPercent = totalBytes > 0 ? (totalBytes - freeBytes) / totalBytes * 100 : 100;
@@ -421,7 +446,7 @@ function observeRunnerDiskGate(input = {}) {
421
446
  }
422
447
  return {
423
448
  ok,
424
- path: path17,
449
+ path: path18,
425
450
  freeBytes,
426
451
  totalBytes,
427
452
  usedPercent,
@@ -695,22 +720,36 @@ function gitIsAncestor(cwd, ancestor, descendant) {
695
720
  if (res.status === 1) return { isAncestor: false, error: null };
696
721
  return { isAncestor: null, error: res.error || res.stderr || res.stdout || `git exited ${res.status}` };
697
722
  }
698
- function computeGitAncestry(worktreePath, base = "origin/main") {
723
+ function computeGitAncestry(worktreePath, baseOrOptions = "origin/main") {
724
+ const options = typeof baseOrOptions === "string" ? { base: baseOrOptions } : baseOrOptions;
725
+ const baseLabel = options.baseCommit?.trim() || options.base?.trim() || "origin/main";
726
+ const pinnedBaseCommit = options.baseCommit?.trim() || null;
699
727
  if (!worktreePath) {
700
- return unknownAncestry(base, "missing worktree path");
728
+ return unknownAncestry(baseLabel, "missing worktree path");
701
729
  }
702
730
  const head = gitCapture(worktreePath, ["rev-parse", "HEAD"]);
703
- if (head.status !== 0) return unknownAncestry(base, head.error || head.stderr || head.stdout || "failed to resolve HEAD");
704
- const baseHead = gitCapture(worktreePath, ["rev-parse", base]);
705
- if (baseHead.status !== 0) {
706
- return unknownAncestry(base, baseHead.error || baseHead.stderr || baseHead.stdout || `failed to resolve ${base}`, head.stdout.trim());
731
+ if (head.status !== 0) {
732
+ return unknownAncestry(baseLabel, head.error || head.stderr || head.stdout || "failed to resolve HEAD");
733
+ }
734
+ let baseSha;
735
+ if (pinnedBaseCommit) {
736
+ baseSha = pinnedBaseCommit;
737
+ } else {
738
+ const baseHead = gitCapture(worktreePath, ["rev-parse", baseLabel]);
739
+ if (baseHead.status !== 0) {
740
+ return unknownAncestry(
741
+ baseLabel,
742
+ baseHead.error || baseHead.stderr || baseHead.stdout || `failed to resolve ${baseLabel}`,
743
+ head.stdout.trim()
744
+ );
745
+ }
746
+ baseSha = baseHead.stdout.trim();
707
747
  }
708
748
  const headSha = head.stdout.trim();
709
- const baseSha = baseHead.stdout.trim();
710
749
  if (headSha === baseSha) {
711
750
  return {
712
751
  checked: true,
713
- base,
752
+ base: baseLabel,
714
753
  head: headSha,
715
754
  baseHead: baseSha,
716
755
  baseIsAncestorOfHead: true,
@@ -724,7 +763,7 @@ function computeGitAncestry(worktreePath, base = "origin/main") {
724
763
  if (baseIsAncestorOfHead.isAncestor == null || headIsAncestorOfBase.isAncestor == null) {
725
764
  return {
726
765
  checked: false,
727
- base,
766
+ base: baseLabel,
728
767
  head: headSha,
729
768
  baseHead: baseSha,
730
769
  baseIsAncestorOfHead: baseIsAncestorOfHead.isAncestor,
@@ -736,7 +775,7 @@ function computeGitAncestry(worktreePath, base = "origin/main") {
736
775
  const relation = baseIsAncestorOfHead.isAncestor ? "ahead" : headIsAncestorOfBase.isAncestor ? "merged" : "diverged";
737
776
  return {
738
777
  checked: true,
739
- base,
778
+ base: baseLabel,
740
779
  head: headSha,
741
780
  baseHead: baseSha,
742
781
  baseIsAncestorOfHead: baseIsAncestorOfHead.isAncestor,
@@ -763,12 +802,74 @@ function scrubClaudeEnv(env) {
763
802
  return next;
764
803
  }
765
804
 
805
+ // src/landing-gate.ts
806
+ function trimOrNull(value) {
807
+ if (typeof value !== "string") return null;
808
+ const trimmed = value.trim();
809
+ return trimmed.length ? trimmed : null;
810
+ }
811
+ function hasFinalResult(value) {
812
+ if (value === void 0 || value === null) return false;
813
+ if (typeof value === "string") return value.trim().length > 0;
814
+ if (typeof value === "boolean") return value;
815
+ if (Array.isArray(value)) return value.length > 0;
816
+ if (typeof value === "object") return Object.keys(value).length > 0;
817
+ return true;
818
+ }
819
+ function hasCommittedLandingRef(snapshot) {
820
+ if (trimOrNull(snapshot.headCommit)) return true;
821
+ if (trimOrNull(snapshot.prUrl)) return true;
822
+ if (trimOrNull(snapshot.artifactBundlePath)) return true;
823
+ if (trimOrNull(snapshot.patchPath)) return true;
824
+ const ancestry = snapshot.gitAncestry;
825
+ if (ancestry?.checked && ancestry.headIsAncestorOfBase === false && trimOrNull(ancestry.head)) {
826
+ return true;
827
+ }
828
+ return false;
829
+ }
830
+ function assessWorkerLanding(snapshot) {
831
+ if (!hasFinalResult(snapshot.finalResult)) return { blocked: false };
832
+ if (snapshot.changedFiles.length === 0) return { blocked: false };
833
+ if (!hasCommittedLandingRef(snapshot)) {
834
+ return {
835
+ blocked: true,
836
+ reason: "dirty_worktree_no_pr",
837
+ detail: `Worktree has ${snapshot.changedFiles.length} uncommitted change(s) with no commit or PR; commit, open a PR, or discard before landing`
838
+ };
839
+ }
840
+ return {
841
+ blocked: true,
842
+ detail: `Worktree has ${snapshot.changedFiles.length} uncommitted change(s); commit or discard before landing`
843
+ };
844
+ }
845
+ function landingAttentionReason(verdict) {
846
+ if (!verdict.blocked) return void 0;
847
+ return verdict.detail ?? verdict.reason ?? "dirty_worktree_no_pr";
848
+ }
849
+
766
850
  // src/status.ts
767
851
  var NO_START_MS = 18e4;
768
852
  var STALE_MS = 6e5;
769
853
  function computeAttention(input) {
770
854
  const now = Date.now();
771
- if (input.finalResult) return { state: "done", reason: "final result recorded" };
855
+ if (input.completionBlocker) {
856
+ return { state: "blocked", reason: input.completionBlocker };
857
+ }
858
+ if (input.finalResult) {
859
+ const landing = assessWorkerLanding({
860
+ finalResult: input.finalResult,
861
+ changedFiles: input.changedFiles ?? [],
862
+ gitAncestry: input.gitAncestry ?? null
863
+ });
864
+ if (landing.blocked) {
865
+ const detail = landingAttentionReason(landing);
866
+ return {
867
+ state: "needs_attention",
868
+ reason: landing.reason ? `landing blocked (${landing.reason}): ${detail}` : `landing blocked: ${detail}`
869
+ };
870
+ }
871
+ return { state: "done", reason: "final result recorded" };
872
+ }
772
873
  if (!input.alive) {
773
874
  const classified = classifyExitFailure(input.error);
774
875
  if (classified) return { state: "blocked", reason: classified.reason };
@@ -799,7 +900,10 @@ function computeWorkerStatus(worker, options = {}) {
799
900
  const stderrBytes = fileSize(worker.stderrPath);
800
901
  const heartbeatBytes = fileSize(worker.heartbeatPath);
801
902
  const changedFiles = gitStatusShort(worker.worktreePath);
802
- const gitAncestry = computeGitAncestry(worker.worktreePath, options.base);
903
+ const gitAncestry = computeGitAncestry(worker.worktreePath, {
904
+ base: options.base,
905
+ baseCommit: options.baseCommit
906
+ });
803
907
  const lastActivityAt = latestIso([
804
908
  parsed.lastEventAt,
805
909
  heartbeat.lastHeartbeatAt,
@@ -808,6 +912,7 @@ function computeWorkerStatus(worker, options = {}) {
808
912
  fileMtime(worker.heartbeatPath)
809
913
  ]);
810
914
  const error = parsed.error || (!alive && !parsed.finalResult ? tailFile(worker.stderrPath, 10).trim() || void 0 : void 0);
915
+ const completionBlocker = typeof worker.completionBlocker === "string" && worker.completionBlocker.trim() ? worker.completionBlocker.trim() : null;
811
916
  const attention = computeAttention({
812
917
  alive,
813
918
  finalResult: parsed.finalResult,
@@ -817,14 +922,18 @@ function computeWorkerStatus(worker, options = {}) {
817
922
  lastActivityAt,
818
923
  heartbeatBlocker: heartbeat.heartbeatBlocker,
819
924
  startedAt: worker.startedAt,
820
- error
925
+ error,
926
+ changedFiles,
927
+ gitAncestry,
928
+ completionBlocker
821
929
  });
930
+ const workerStatusLabel = completionBlocker || attention.state === "blocked" ? "blocked" : attention.state === "done" ? "done" : parsed.finalResult ? "exited" : alive ? "running" : "exited";
822
931
  return {
823
932
  runId: worker.runId,
824
933
  worker: worker.name,
825
934
  pid: worker.pid,
826
935
  alive,
827
- status: parsed.finalResult ? "done" : alive ? "running" : "exited",
936
+ status: workerStatusLabel,
828
937
  attention,
829
938
  branch: worker.branch,
830
939
  worktreePath: worker.worktreePath,
@@ -853,6 +962,10 @@ function isFinishedWorkerStatus(status) {
853
962
  if (status.status === "exited" || status.status === "done") return true;
854
963
  return false;
855
964
  }
965
+ function isLandingBlockedWorkerStatus(status) {
966
+ if (!status.finalResult) return false;
967
+ return status.attention.state === "needs_attention" || status.attention.state === "blocked";
968
+ }
856
969
  function deriveRunStatus(fallback, workers) {
857
970
  if (workers.length === 0) return fallback;
858
971
  if (workers.some((w) => w.attention === "needs_attention" || w.attention === "stale" || w.attention === "blocked")) {
@@ -903,6 +1016,10 @@ function readAvailableMemBytes() {
903
1016
  }
904
1017
  return os.freemem();
905
1018
  }
1019
+ function isActiveHarnessWorker(worker) {
1020
+ const status = computeWorkerStatus(worker);
1021
+ return status.alive && !status.finalResult && status.attention.state !== "done";
1022
+ }
906
1023
  function countActiveWorkersForRun(run) {
907
1024
  let active = 0;
908
1025
  for (const name of Object.keys(run.workers || {})) {
@@ -910,11 +1027,8 @@ function countActiveWorkersForRun(run) {
910
1027
  path5.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
911
1028
  void 0
912
1029
  );
913
- if (!worker) continue;
914
- const status = computeWorkerStatus(worker);
915
- if (status.alive && !status.finalResult && status.attention.state !== "done") {
916
- active++;
917
- }
1030
+ if (!worker || !isActiveHarnessWorker(worker)) continue;
1031
+ active++;
918
1032
  }
919
1033
  return active;
920
1034
  }
@@ -939,7 +1053,7 @@ function observeRunnerResourceGate(input) {
939
1053
  const maxConcurrentWorkers = Math.max(0, Math.min(targetCap, capacityFromTotal));
940
1054
  const slotsByCapacity = Math.max(0, maxConcurrentWorkers - activeWorkers);
941
1055
  const slotsByFreeMem = capacityFromFree;
942
- const slotsAvailable = Math.min(slotsByCapacity, slotsByFreeMem);
1056
+ let slotsAvailable = Math.min(slotsByCapacity, slotsByFreeMem);
943
1057
  let reason = null;
944
1058
  if (slotsAvailable <= 0) {
945
1059
  if (activeWorkers >= maxConcurrentWorkers) {
@@ -966,37 +1080,57 @@ function observeRunnerResourceGate(input) {
966
1080
  };
967
1081
  }
968
1082
 
969
- // src/supervisor.ts
970
- import { existsSync as existsSync10, mkdirSync as mkdirSync3 } from "node:fs";
971
- import path10 from "node:path";
972
-
973
- // src/prompt.ts
974
- function buildPrompt(input) {
975
- 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.";
976
- const progressLines = [
977
- "Structured plan progress (required when planId is set):",
978
- "- 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).",
979
- "- 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.",
980
- "- 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`).",
981
- "- 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>`.",
982
- "- Before the completion report: mark completion-report rows partial with evidence; do not skip report review.",
983
- "- After implementation: wait for report_reviewer then deep_reviewer confirmation (via MCP/session agents) before follow-up rows close.",
984
- 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."
985
- ];
986
- return [
987
- "You are running under the Kynver AgentOS runtime.",
988
- "Immediately state your plan before editing.",
989
- ownership,
990
- `Worktree: ${input.worktreePath}`,
991
- `Progress heartbeat file: ${input.heartbeatPath}`,
992
- "After each major step, append one JSON line to the heartbeat file with fields: ts, phase, summary, changedFiles, blocker.",
993
- "Final response must include files changed, verification commands, and unresolved risks.",
994
- "",
995
- ...progressLines,
996
- "",
997
- "Task:",
998
- input.task
999
- ].join("\n");
1083
+ // src/model-routing-task-enrich.ts
1084
+ function taskString(task, key) {
1085
+ const v = task[key];
1086
+ return typeof v === "string" ? v.trim() : "";
1087
+ }
1088
+ function normalize(value) {
1089
+ return value.toLowerCase();
1090
+ }
1091
+ var PERSONA_DEFAULT_LANE = {
1092
+ dalton: "implementer",
1093
+ lorentz: "report_reviewer"
1094
+ };
1095
+ function inferRoleLaneFromTask(task) {
1096
+ const existing = taskString(task, "roleLane");
1097
+ if (existing) return existing;
1098
+ const ref = normalize(taskString(task, "executorRef"));
1099
+ const title = normalize(taskString(task, "title"));
1100
+ const persona = normalize(taskString(task, "personaSlug"));
1101
+ const combined = `${ref} ${title}`;
1102
+ if (combined.includes("deep review") || combined.includes("security review") || ref.includes("deep-reviewer")) {
1103
+ return "deep_reviewer";
1104
+ }
1105
+ if (combined.includes("plan author") || combined.includes("plan-author") || title.includes("strategy plan")) {
1106
+ return "plan_author";
1107
+ }
1108
+ if (combined.includes("plan review") || ref.includes("plan-reviewer")) {
1109
+ return "plan_reviewer";
1110
+ }
1111
+ if (combined.includes("report review") || combined.includes("completion report")) {
1112
+ return "report_reviewer";
1113
+ }
1114
+ if (combined.includes("repair") || title.startsWith("fix ") || ref.includes("repair")) {
1115
+ return "repair_implementer";
1116
+ }
1117
+ if (ref.includes("cursor") || ref.includes("codex") || ref.includes("composer") || title.includes("implement") || title.includes("land:")) {
1118
+ return "implementer";
1119
+ }
1120
+ if (persona && PERSONA_DEFAULT_LANE[persona]) {
1121
+ const base = PERSONA_DEFAULT_LANE[persona];
1122
+ if (persona === "lorentz" && (combined.includes("deep") || combined.includes("security"))) {
1123
+ return "deep_reviewer";
1124
+ }
1125
+ return base;
1126
+ }
1127
+ if (combined.includes("review")) return "report_reviewer";
1128
+ return void 0;
1129
+ }
1130
+ function enrichTaskForModelRouting(task) {
1131
+ const roleLane = inferRoleLaneFromTask(task);
1132
+ if (!roleLane) return task;
1133
+ return { ...task, roleLane };
1000
1134
  }
1001
1135
 
1002
1136
  // src/providers/claude.ts
@@ -1057,7 +1191,7 @@ function preflightCursorModel(model, defaultModel) {
1057
1191
  }
1058
1192
 
1059
1193
  // src/providers/claude.ts
1060
- var CLAUDE_DEFAULT_MODEL = "claude-opus-4-7";
1194
+ var CLAUDE_DEFAULT_MODEL = "claude-sonnet-4-6";
1061
1195
  var claudeProvider = {
1062
1196
  name: "claude",
1063
1197
  defaultModel: CLAUDE_DEFAULT_MODEL,
@@ -1103,6 +1237,172 @@ var claudeProvider = {
1103
1237
  }
1104
1238
  };
1105
1239
 
1240
+ // src/model-routing.ts
1241
+ var GLOBAL_DEFAULT_MODEL = "claude-sonnet-4-6";
1242
+ function taskString2(task, key) {
1243
+ const v = task[key];
1244
+ return typeof v === "string" ? v.trim() : "";
1245
+ }
1246
+ function normalizeRef(ref) {
1247
+ return ref.toLowerCase();
1248
+ }
1249
+ function resolveGlobalDefaultModel(config = loadUserConfig()) {
1250
+ const fromConfig = config.defaultModel?.trim();
1251
+ if (fromConfig) return fromConfig;
1252
+ const fromEnv = process.env.KYNVER_DEFAULT_MODEL?.trim();
1253
+ if (fromEnv) return fromEnv;
1254
+ return GLOBAL_DEFAULT_MODEL;
1255
+ }
1256
+ function inferProviderFromModel(model) {
1257
+ const m = (model ?? "").toLowerCase();
1258
+ if (!m) return "claude";
1259
+ if (m.includes("composer") || m.includes("cursor") || m.includes("codex") || m.startsWith("gpt-") || m.startsWith("gpt5")) {
1260
+ return "cursor";
1261
+ }
1262
+ return "claude";
1263
+ }
1264
+ function isOpusLane(ref, title) {
1265
+ if (ref.includes("deep") && ref.includes("review")) return true;
1266
+ if (ref.includes("security")) return true;
1267
+ if (ref.includes("plan_author") || ref.includes("plan-author")) return true;
1268
+ if (title.includes("deep review") || title.includes("security review")) return true;
1269
+ if (ref.includes("plan") && !ref.includes("review") && (ref.includes("author") || ref.includes("strategy"))) {
1270
+ return true;
1271
+ }
1272
+ return false;
1273
+ }
1274
+ function inferModelRoutingFromTask(task) {
1275
+ const ref = normalizeRef(taskString2(task, "executorRef"));
1276
+ const title = taskString2(task, "title").toLowerCase();
1277
+ const priority = taskString2(task, "priority") || "normal";
1278
+ const roleLane = normalizeRef(taskString2(task, "roleLane"));
1279
+ if (ref.includes("cursor") || ref.includes("codex") || ref.includes("composer") || ref.includes("copilot") || roleLane === "implementer" || roleLane === "repair_implementer") {
1280
+ return { provider: "cursor", rule: "lane:implementation" };
1281
+ }
1282
+ if (ref.includes("landing") || title.startsWith("land:") || title.includes(" merge")) {
1283
+ return {
1284
+ model: "claude-haiku-4-5-20251001",
1285
+ provider: "claude",
1286
+ rule: "lane:landing"
1287
+ };
1288
+ }
1289
+ if (ref.includes("review") || title.startsWith("review ") || roleLane.includes("review")) {
1290
+ if (isOpusLane(ref, title) || roleLane === "deep_reviewer") {
1291
+ return { model: "claude-opus-4-7", provider: "claude", rule: "lane:deep_review" };
1292
+ }
1293
+ return { model: "claude-sonnet-4-6", provider: "claude", rule: "lane:review" };
1294
+ }
1295
+ if (isOpusLane(ref, title) || roleLane === "plan_author") {
1296
+ return { model: "claude-opus-4-7", provider: "claude", rule: "lane:planning" };
1297
+ }
1298
+ if (priority === "critical") {
1299
+ return { model: "claude-opus-4-7", provider: "claude", rule: "priority:critical" };
1300
+ }
1301
+ if (priority === "high") {
1302
+ return { model: "claude-sonnet-4-6", provider: "claude", rule: "priority:high" };
1303
+ }
1304
+ if (priority === "low") {
1305
+ return {
1306
+ model: "claude-haiku-4-5-20251001",
1307
+ provider: "claude",
1308
+ rule: "priority:low"
1309
+ };
1310
+ }
1311
+ return {
1312
+ model: resolveGlobalDefaultModel(),
1313
+ provider: "claude",
1314
+ rule: "default:sonnet"
1315
+ };
1316
+ }
1317
+ function resolveWorkerLaunch(input) {
1318
+ if (input.explicitModel?.trim()) {
1319
+ const model2 = input.explicitModel.trim();
1320
+ return {
1321
+ model: model2,
1322
+ provider: input.explicitProvider?.trim() || inferProviderFromModel(model2),
1323
+ rule: "explicit:cli",
1324
+ requestedModel: model2
1325
+ };
1326
+ }
1327
+ if (input.task && Object.keys(input.task).length > 0) {
1328
+ const inferred = inferModelRoutingFromTask(input.task);
1329
+ return {
1330
+ ...inferred,
1331
+ requestedModel: inferred.model
1332
+ };
1333
+ }
1334
+ const model = resolveGlobalDefaultModel();
1335
+ return {
1336
+ model,
1337
+ provider: input.explicitProvider?.trim() || inferProviderFromModel(model),
1338
+ rule: "default:global",
1339
+ requestedModel: model
1340
+ };
1341
+ }
1342
+ function resolveModelFallback(startedModel, launchModel, providerDefault) {
1343
+ return startedModel || launchModel || providerDefault || resolveGlobalDefaultModel() || CLAUDE_DEFAULT_MODEL;
1344
+ }
1345
+
1346
+ // src/retry-limits.ts
1347
+ function positiveInt2(value, fallback) {
1348
+ const n = Number(value);
1349
+ if (!Number.isFinite(n) || n <= 0) return fallback;
1350
+ return Math.floor(n);
1351
+ }
1352
+ function readHarnessRetryLimits() {
1353
+ return {
1354
+ maxTaskAttempts: positiveInt2(process.env.KYNVER_MAX_TASK_ATTEMPTS, 3),
1355
+ dispatchCooldownMs: positiveInt2(process.env.KYNVER_DISPATCH_COOLDOWN_MS, 5e3)
1356
+ };
1357
+ }
1358
+
1359
+ // src/supervisor.ts
1360
+ import { existsSync as existsSync10, mkdirSync as mkdirSync3 } from "node:fs";
1361
+ import path10 from "node:path";
1362
+
1363
+ // src/prompt.ts
1364
+ function buildPrompt(input) {
1365
+ 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.";
1366
+ const compact = Boolean(input.model?.toLowerCase().includes("haiku"));
1367
+ const progressLines = compact ? [
1368
+ "Plan progress: when planId is set, use `kynver plan progress` for running|partial|blocked only; row `done` is MCP/session only.",
1369
+ input.planId ? `Active planId: ${input.planId}` : "No planId on this worker."
1370
+ ] : [
1371
+ "Structured plan progress (required when planId is set):",
1372
+ "- 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).",
1373
+ "- 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.",
1374
+ "- 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`).",
1375
+ "- 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>`.",
1376
+ "- Before the completion report: mark completion-report rows partial with evidence; do not skip report review.",
1377
+ "- After implementation: wait for report_reviewer then deep_reviewer confirmation (via MCP/session agents) before follow-up rows close.",
1378
+ 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."
1379
+ ];
1380
+ const planArtifactLines = compact ? [
1381
+ "Plan artifacts: when authoring/revising docs/superpowers/plans/, open a GitHub PR early and iterate from that PR branch; do not leave the canonical plan only in the harness worktree."
1382
+ ] : [
1383
+ "PR-first plan artifacts (when authoring or revising docs/superpowers/plans/):",
1384
+ "- Before substantial plan drafting: create a feature branch, open a GitHub PR (draft OK), commit and push the plan file \u2014 do not leave the canonical plan only in this harness worktree.",
1385
+ "- Iterate review on that PR branch; link prUrl on the AgentOS task and plan progress evidence (`--evidence pr:<url>`).",
1386
+ "- See docs/superpowers/plans/2026-05-25-pr-first-plan-artifact-preservation.md for the full checklist."
1387
+ ];
1388
+ return [
1389
+ "You are running under the Kynver AgentOS runtime.",
1390
+ "Immediately state your plan before editing.",
1391
+ ownership,
1392
+ `Worktree: ${input.worktreePath}`,
1393
+ `Progress heartbeat file: ${input.heartbeatPath}`,
1394
+ "After each major step, append one JSON line to the heartbeat file with fields: ts, phase, summary, changedFiles, blocker.",
1395
+ "Final response must include files changed, verification commands, and unresolved risks.",
1396
+ "",
1397
+ ...progressLines,
1398
+ "",
1399
+ ...planArtifactLines,
1400
+ "",
1401
+ "Task:",
1402
+ input.task
1403
+ ].join("\n");
1404
+ }
1405
+
1106
1406
  // src/providers/cursor.ts
1107
1407
  import { closeSync as closeSync2, existsSync as existsSync8, openSync as openSync2 } from "node:fs";
1108
1408
  import { spawn as spawn2 } from "node:child_process";
@@ -1306,9 +1606,13 @@ function persistCompletionBlocker(worker, reason) {
1306
1606
  else delete worker.completionBlocker;
1307
1607
  saveWorker(worker.runId, worker);
1308
1608
  }
1609
+ function workerStatusOptions(run) {
1610
+ return run ? { base: run.base, baseCommit: run.baseCommit } : {};
1611
+ }
1309
1612
  async function tryCompleteWorker(args) {
1310
1613
  const worker = loadWorker(String(args.run), String(args.name));
1311
- const status = computeWorkerStatus(worker);
1614
+ const run = loadRun(worker.runId);
1615
+ const status = computeWorkerStatus(worker, workerStatusOptions(run));
1312
1616
  const agentOsId = (args.agentOsId ? String(args.agentOsId) : worker.agentOsId) || "";
1313
1617
  const taskId = (args.taskId ? String(args.taskId) : worker.taskId) || null;
1314
1618
  if (!agentOsId) {
@@ -1352,7 +1656,8 @@ async function tryCompleteWorker(args) {
1352
1656
  async function completeWorker(args) {
1353
1657
  try {
1354
1658
  const worker = loadWorker(String(args.run), String(args.name));
1355
- const status = computeWorkerStatus(worker);
1659
+ const run = loadRun(worker.runId);
1660
+ const status = computeWorkerStatus(worker, workerStatusOptions(run));
1356
1661
  const agentOsId = (args.agentOsId ? String(args.agentOsId) : worker.agentOsId) || "";
1357
1662
  const taskId = (args.taskId ? String(args.taskId) : worker.taskId) || null;
1358
1663
  if (!agentOsId) {
@@ -1399,12 +1704,13 @@ async function completeWorker(args) {
1399
1704
  }
1400
1705
  function workerStatus(args) {
1401
1706
  const worker = loadWorker(String(args.run), String(args.name));
1402
- const status = computeWorkerStatus(worker);
1707
+ const run = loadRun(worker.runId);
1708
+ const status = computeWorkerStatus(worker, workerStatusOptions(run));
1403
1709
  writeJson(path8.join(worker.workerDir, "last-status.json"), status);
1404
1710
  console.log(JSON.stringify(status, null, 2));
1405
1711
  }
1406
- function runStatus(args) {
1407
- const run = loadRun(String(args.run));
1712
+ function buildRunBoard(runId) {
1713
+ const run = loadRun(runId);
1408
1714
  const names = Object.keys(run.workers || {});
1409
1715
  const workers = names.map((name) => {
1410
1716
  const worker = readJson(
@@ -1414,14 +1720,21 @@ function runStatus(args) {
1414
1720
  if (!worker) {
1415
1721
  return { worker: name, status: "missing", attention: "needs_attention", attentionReason: "worker.json not found" };
1416
1722
  }
1417
- const status = computeWorkerStatus(worker, { base: run.base });
1723
+ const status = computeWorkerStatus(worker, {
1724
+ base: run.base,
1725
+ baseCommit: run.baseCommit
1726
+ });
1727
+ const headCommit = status.gitAncestry.headIsAncestorOfBase === false && status.gitAncestry.head ? status.gitAncestry.head : void 0;
1418
1728
  const rawBlocker = worker.completionBlocker;
1419
1729
  const completionBlocker = typeof rawBlocker === "string" && rawBlocker ? rawBlocker : void 0;
1730
+ const boardStatus = completionBlocker ? "blocked" : status.status;
1731
+ const boardAttention = completionBlocker ? "blocked" : status.attention.state;
1420
1732
  return {
1421
1733
  worker: status.worker,
1422
- status: completionBlocker ? "blocked" : status.status,
1423
- attention: completionBlocker ? "blocked" : status.attention.state,
1734
+ status: boardStatus,
1735
+ attention: boardAttention,
1424
1736
  attentionReason: completionBlocker ?? status.attention.reason,
1737
+ landingBlocked: status.finalResult ? boardAttention === "needs_attention" || boardAttention === "blocked" : false,
1425
1738
  pid: status.pid,
1426
1739
  alive: status.alive,
1427
1740
  currentTool: status.currentTool,
@@ -1430,7 +1743,14 @@ function runStatus(args) {
1430
1743
  lastHeartbeatSummary: status.lastHeartbeatSummary,
1431
1744
  heartbeatBlocker: status.heartbeatBlocker,
1432
1745
  changedFileCount: status.changedFiles.length,
1746
+ changedFiles: status.changedFiles,
1433
1747
  branch: status.branch,
1748
+ model: typeof worker.model === "string" ? worker.model : void 0,
1749
+ routingRule: typeof worker.routingRule === "string" ? worker.routingRule : void 0,
1750
+ requestedModel: typeof worker.requestedModel === "string" ? worker.requestedModel : void 0,
1751
+ headCommit,
1752
+ gitAncestry: status.gitAncestry,
1753
+ finalResult: status.finalResult,
1434
1754
  ancestry: status.gitAncestry.relation,
1435
1755
  ancestryChecked: status.gitAncestry.checked
1436
1756
  };
@@ -1445,6 +1765,34 @@ function runStatus(args) {
1445
1765
  workers
1446
1766
  };
1447
1767
  writeJson(path8.join(runDirectory(run.id), "last-board.json"), board);
1768
+ return board;
1769
+ }
1770
+ async function publishHarnessBoardSnapshot(args, source) {
1771
+ const runId = String(args.run || "");
1772
+ const agentOsId = String(args.agentOsId || "");
1773
+ if (!runId || !agentOsId) return null;
1774
+ const board = buildRunBoard(runId);
1775
+ const base = resolveBaseUrl(args.baseUrl ? String(args.baseUrl) : void 0);
1776
+ const secret = await resolveCallbackSecretWithMint(args.secret ? String(args.secret) : void 0, agentOsId, {
1777
+ baseUrl: base
1778
+ });
1779
+ const url = `${base}/api/agent-os/by-id/${encodeURIComponent(agentOsId)}/harness/snapshot`;
1780
+ const res = await postJsonWithCredentialRefresh(
1781
+ url,
1782
+ secret,
1783
+ { agentOsId, runId, source, snapshot: board },
1784
+ { agentOsId, baseUrl: base }
1785
+ );
1786
+ return {
1787
+ ok: res.ok,
1788
+ httpStatus: res.status,
1789
+ response: res.response,
1790
+ authRefreshed: res.refreshedAuth,
1791
+ authRefreshFailure: res.authRefreshFailure
1792
+ };
1793
+ }
1794
+ function runStatus(args) {
1795
+ const board = buildRunBoard(String(args.run));
1448
1796
  console.log(JSON.stringify(board, null, 2));
1449
1797
  }
1450
1798
  function tailWorker(args) {
@@ -1646,8 +1994,17 @@ function spawnWorkerProcess(run, opts) {
1646
1994
  const name = safeSlug(rawName);
1647
1995
  if (run.workers?.[name]) throw new Error(`worker already exists in run ${run.id}: ${name}`);
1648
1996
  if (!opts.task) throw new Error(`missing task text for worker ${name}`);
1649
- const provider = resolveWorkerProvider(opts.provider);
1650
- let launchModel = opts.model;
1997
+ const routing = opts.routingRule || opts.requestedModel ? {
1998
+ provider: opts.provider || "claude",
1999
+ model: opts.model,
2000
+ rule: opts.routingRule || "explicit:spawn",
2001
+ requestedModel: opts.requestedModel ?? opts.model
2002
+ } : resolveWorkerLaunch({
2003
+ explicitModel: opts.model,
2004
+ explicitProvider: opts.provider
2005
+ });
2006
+ const provider = resolveWorkerProvider(routing.provider);
2007
+ let launchModel = routing.model;
1651
2008
  if (provider.preflightModel) {
1652
2009
  const preflight = provider.preflightModel(opts.model);
1653
2010
  if (!preflight.ok) {
@@ -1675,7 +2032,10 @@ function spawnWorkerProcess(run, opts) {
1675
2032
  task: opts.task,
1676
2033
  ownedPaths: opts.ownedPaths || [],
1677
2034
  worktreePath,
1678
- heartbeatPath
2035
+ heartbeatPath,
2036
+ planId: opts.planId,
2037
+ taskId: opts.taskId,
2038
+ model: launchModel
1679
2039
  });
1680
2040
  let started;
1681
2041
  try {
@@ -1697,7 +2057,7 @@ function spawnWorkerProcess(run, opts) {
1697
2057
  git(run.repo, ["branch", "-D", branch], { allowFailure: true });
1698
2058
  throw error;
1699
2059
  }
1700
- const model = started.model || launchModel || provider.defaultModel || "claude-opus-4-7";
2060
+ const model = resolveModelFallback(started.model, launchModel, provider.defaultModel);
1701
2061
  const worker = {
1702
2062
  name,
1703
2063
  runId: run.id,
@@ -1716,6 +2076,8 @@ function spawnWorkerProcess(run, opts) {
1716
2076
  ...opts.planId ? { planId: String(opts.planId) } : {},
1717
2077
  ...opts.leaseOwner ? { leaseOwner: String(opts.leaseOwner) } : {},
1718
2078
  ...opts.dispatched ? { dispatched: true } : {},
2079
+ routingRule: routing.rule,
2080
+ ...routing.requestedModel ? { requestedModel: routing.requestedModel } : {},
1719
2081
  startedAt: (/* @__PURE__ */ new Date()).toISOString()
1720
2082
  };
1721
2083
  saveWorker(run.id, worker);
@@ -1723,14 +2085,23 @@ function spawnWorkerProcess(run, opts) {
1723
2085
  run.status = "running";
1724
2086
  saveRun(run);
1725
2087
  if (worker.agentOsId && worker.taskId) {
2088
+ let sidecarSpawned;
1726
2089
  try {
1727
- spawnCompletionSidecar({
2090
+ sidecarSpawned = spawnCompletionSidecar({
1728
2091
  runId: run.id,
1729
2092
  workerName: name,
1730
2093
  workerDir,
1731
2094
  agentOsId: worker.agentOsId
1732
2095
  });
1733
- } catch {
2096
+ } catch (error) {
2097
+ const reason = `completion sidecar failed to spawn: ${error.message}`;
2098
+ worker.completionBlocker = reason;
2099
+ saveWorker(run.id, worker);
2100
+ }
2101
+ if (!sidecarSpawned) {
2102
+ const reason = "completion sidecar failed to spawn (CLI not found or spawn error)";
2103
+ worker.completionBlocker = reason;
2104
+ saveWorker(run.id, worker);
1734
2105
  }
1735
2106
  }
1736
2107
  return worker;
@@ -1814,9 +2185,10 @@ async function dispatchRun(args) {
1814
2185
  runnerDiskGate,
1815
2186
  runnerResourceGate,
1816
2187
  ...args.lane ? { lane: String(args.lane) } : {},
2188
+ executor: args.executor ? String(args.executor) : "harness",
1817
2189
  ...args.diskPath ? { diskPath: String(args.diskPath) } : {}
1818
2190
  };
1819
- const dispatch = await postJson(dispatchUrl, secret, body);
2191
+ const dispatch = await postJsonWithCredentialRefresh(dispatchUrl, secret, body, { agentOsId, baseUrl: base });
1820
2192
  const responseBody = dispatch.response;
1821
2193
  if (!dispatch.ok || !responseBody?.result) {
1822
2194
  const failure = {
@@ -1824,7 +2196,9 @@ async function dispatchRun(args) {
1824
2196
  agentOsId,
1825
2197
  action: "dispatch",
1826
2198
  httpStatus: dispatch.status,
1827
- response: dispatch.response
2199
+ response: dispatch.response,
2200
+ authRefreshed: dispatch.refreshedAuth === true,
2201
+ authRefreshFailure: dispatch.authRefreshFailure
1828
2202
  };
1829
2203
  if (pipeline) return { ok: false, ...failure };
1830
2204
  console.log(JSON.stringify(failure, null, 2));
@@ -1865,17 +2239,34 @@ async function dispatchRun(args) {
1865
2239
  console.log(JSON.stringify(summary2, null, 2));
1866
2240
  return;
1867
2241
  }
2242
+ const retryLimits = readHarnessRetryLimits();
1868
2243
  const outcomes = [];
1869
2244
  for (const decision of result.started) {
1870
2245
  const task = decision.task;
2246
+ const attempt = Number(task.attempt) || 1;
2247
+ if (attempt > retryLimits.maxTaskAttempts) {
2248
+ outcomes.push({
2249
+ taskId: task.id,
2250
+ started: false,
2251
+ error: `task attempt ${attempt} exceeds KYNVER_MAX_TASK_ATTEMPTS (${retryLimits.maxTaskAttempts})`
2252
+ });
2253
+ continue;
2254
+ }
1871
2255
  const name = safeSlug(`t-${task.id}-a${task.attempt}`);
2256
+ const routing = resolveWorkerLaunch({
2257
+ explicitModel: args.model ? String(args.model) : void 0,
2258
+ task: enrichTaskForModelRouting(task)
2259
+ });
1872
2260
  try {
1873
2261
  const planId = task.planId ? String(task.planId) : void 0;
1874
2262
  const worker = spawnWorkerProcess(run, {
1875
2263
  name,
1876
2264
  task: buildDispatchTaskText(task, agentOsId),
1877
2265
  ownedPaths: args.owned ? String(args.owned).split(",").map((s) => s.trim()).filter(Boolean) : [],
1878
- model: args.model ? String(args.model) : void 0,
2266
+ model: routing.model,
2267
+ provider: routing.provider,
2268
+ routingRule: routing.rule,
2269
+ requestedModel: routing.requestedModel,
1879
2270
  agentOsId,
1880
2271
  taskId: String(task.id),
1881
2272
  planId,
@@ -1887,13 +2278,16 @@ async function dispatchRun(args) {
1887
2278
  started: true,
1888
2279
  worker: worker.name,
1889
2280
  pid: worker.pid,
1890
- branch: worker.branch
2281
+ branch: worker.branch,
2282
+ model: worker.model,
2283
+ provider: routing.provider,
2284
+ routingRule: routing.rule
1891
2285
  });
1892
2286
  } catch (error) {
1893
2287
  const releaseUrl = `${base}/api/agent-os/by-id/${encodeURIComponent(agentOsId)}/tasks/${encodeURIComponent(String(task.id))}/release`;
1894
2288
  let release;
1895
2289
  try {
1896
- release = await postJson(releaseUrl, secret, { agentOsId, leaseOwner });
2290
+ release = await postJsonWithCredentialRefresh(releaseUrl, secret, { agentOsId, leaseOwner }, { agentOsId, baseUrl: base });
1897
2291
  } catch (relErr) {
1898
2292
  release = { ok: false, error: relErr.message };
1899
2293
  }
@@ -1942,6 +2336,7 @@ async function sweepRun(args) {
1942
2336
  const base = resolveBaseUrl(args.baseUrl ? String(args.baseUrl) : void 0);
1943
2337
  const secret = await resolveCallbackSecretWithMint(args.secret ? String(args.secret) : void 0, agentOsId, { baseUrl: base });
1944
2338
  const leaseOwner = `openclaw-harness:${run.id}`;
2339
+ const snapshotPublished = await publishHarnessBoardSnapshot({ run: run.id, agentOsId, ...args }, "run_sweep");
1945
2340
  const releasedLocalOrphans = [];
1946
2341
  for (const name of Object.keys(run.workers || {})) {
1947
2342
  const worker = readJson(
@@ -1951,11 +2346,11 @@ async function sweepRun(args) {
1951
2346
  if (!worker || !worker.dispatched || !worker.taskId) continue;
1952
2347
  const status = computeWorkerStatus(worker);
1953
2348
  if (status.alive) continue;
1954
- if (status.finalResult) continue;
2349
+ if (status.finalResult || worker.completionReportedAt) continue;
1955
2350
  const releaseUrl = `${base}/api/agent-os/by-id/${encodeURIComponent(agentOsId)}/tasks/${encodeURIComponent(String(worker.taskId))}/release`;
1956
2351
  let release;
1957
2352
  try {
1958
- release = await postJson(releaseUrl, secret, { agentOsId, leaseOwner });
2353
+ release = await postJsonWithCredentialRefresh(releaseUrl, secret, { agentOsId, leaseOwner }, { agentOsId, baseUrl: base });
1959
2354
  } catch (relErr) {
1960
2355
  release = { ok: false, error: relErr.message };
1961
2356
  }
@@ -1970,14 +2365,14 @@ async function sweepRun(args) {
1970
2365
  const reapUrl = `${base}/api/agent-os/by-id/${encodeURIComponent(agentOsId)}/tasks/reap`;
1971
2366
  let reap;
1972
2367
  try {
1973
- reap = await postJson(reapUrl, secret, {
2368
+ reap = await postJsonWithCredentialRefresh(reapUrl, secret, {
1974
2369
  agentOsId,
1975
2370
  ...Number(args.graceMs) >= 0 && args.graceMs !== void 0 && args.graceMs !== true ? { graceMs: Math.floor(Number(args.graceMs)) } : {}
1976
- });
2371
+ }, { agentOsId, baseUrl: base });
1977
2372
  } catch (reapErr) {
1978
2373
  reap = { ok: false, error: reapErr.message };
1979
2374
  }
1980
- const summary = { runId: run.id, agentOsId, leaseOwner, releasedLocalOrphans, reap: reap.response ?? reap };
2375
+ const summary = { runId: run.id, agentOsId, leaseOwner, snapshotPublished, releasedLocalOrphans, reap: reap.response ?? reap };
1981
2376
  if (pipeline) return { ok: true, ...summary };
1982
2377
  console.log(JSON.stringify(summary, null, 2));
1983
2378
  } catch (error) {
@@ -2045,7 +2440,10 @@ function failExists(message) {
2045
2440
  }
2046
2441
 
2047
2442
  // src/pipeline-tick.ts
2048
- import path16 from "node:path";
2443
+ import path17 from "node:path";
2444
+
2445
+ // src/stale-reconcile.ts
2446
+ import path15 from "node:path";
2049
2447
 
2050
2448
  // src/finalize.ts
2051
2449
  import path14 from "node:path";
@@ -2056,13 +2454,17 @@ function terminalStatusFor(run) {
2056
2454
  let anyAlive = false;
2057
2455
  let anyResult = false;
2058
2456
  let anyCompletionBlocked = false;
2457
+ let anyLandingBlocked = false;
2059
2458
  for (const name of names) {
2060
2459
  const worker = readJson(
2061
2460
  path14.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
2062
2461
  void 0
2063
2462
  );
2064
2463
  if (!worker) continue;
2065
- const status = computeWorkerStatus(worker);
2464
+ const status = computeWorkerStatus(worker, {
2465
+ base: run.base,
2466
+ baseCommit: run.baseCommit
2467
+ });
2066
2468
  if (status.alive && !status.finalResult) {
2067
2469
  anyAlive = true;
2068
2470
  break;
@@ -2070,10 +2472,14 @@ function terminalStatusFor(run) {
2070
2472
  if (typeof worker.completionBlocker === "string" && worker.completionBlocker) {
2071
2473
  anyCompletionBlocked = true;
2072
2474
  }
2073
- if (status.finalResult) anyResult = true;
2475
+ if (isLandingBlockedWorkerStatus(status)) {
2476
+ anyLandingBlocked = true;
2477
+ }
2478
+ if (status.finalResult && status.attention.state === "done") anyResult = true;
2074
2479
  }
2075
2480
  if (anyAlive) return null;
2076
2481
  if (anyCompletionBlocked) return null;
2482
+ if (anyLandingBlocked) return null;
2077
2483
  return anyResult ? "completed" : "failed";
2078
2484
  }
2079
2485
  function finalizeStaleRuns() {
@@ -2090,20 +2496,94 @@ function finalizeStaleRuns() {
2090
2496
  return finalized;
2091
2497
  }
2092
2498
 
2499
+ // src/stale-reconcile.ts
2500
+ var STALE_RECONCILE_HEARTBEAT_MS = 15 * 60 * 1e3;
2501
+ function staleReconcileDisabled() {
2502
+ return process.env.KYNVER_NO_STALE_CLEANUP === "1";
2503
+ }
2504
+ function reconcileStaleWorkers() {
2505
+ if (staleReconcileDisabled()) {
2506
+ return { workers: [], finalizedRuns: finalizeStaleRuns() };
2507
+ }
2508
+ const outcomes = [];
2509
+ const now = Date.now();
2510
+ for (const run of listRunRecords()) {
2511
+ for (const name of Object.keys(run.workers || {})) {
2512
+ const workerPath = path15.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json");
2513
+ const worker = readJson(workerPath, void 0);
2514
+ if (!worker || worker.status !== "running") {
2515
+ outcomes.push({
2516
+ runId: run.id,
2517
+ worker: name,
2518
+ action: "skipped",
2519
+ reason: worker ? `worker status is ${worker.status}` : "worker.json missing"
2520
+ });
2521
+ continue;
2522
+ }
2523
+ const status = computeWorkerStatus(worker, { base: run.base, baseCommit: run.baseCommit });
2524
+ if (status.finalResult) {
2525
+ outcomes.push({ runId: run.id, worker: name, action: "skipped", reason: "final result present" });
2526
+ continue;
2527
+ }
2528
+ if (!status.alive) {
2529
+ const nextStatus = status.attention.state === "blocked" ? "blocked" : status.status === "done" ? "done" : "exited";
2530
+ worker.status = nextStatus;
2531
+ worker.reconciledAt = (/* @__PURE__ */ new Date()).toISOString();
2532
+ worker.reconcileReason = status.attention.reason;
2533
+ saveWorker(run.id, worker);
2534
+ outcomes.push({
2535
+ runId: run.id,
2536
+ worker: name,
2537
+ action: "marked_exited",
2538
+ reason: status.attention.reason
2539
+ });
2540
+ continue;
2541
+ }
2542
+ if (status.attention.state === "stale" && worker.pid && isPidAlive(worker.pid)) {
2543
+ const hbMs = status.lastHeartbeatAt ? Date.parse(status.lastHeartbeatAt) : NaN;
2544
+ const actMs = status.lastActivityAt ? Date.parse(status.lastActivityAt) : NaN;
2545
+ const hbStale = !Number.isFinite(hbMs) || now - hbMs > STALE_RECONCILE_HEARTBEAT_MS;
2546
+ const actStale = Number.isFinite(actMs) && now - actMs > STALE_MS;
2547
+ if (hbStale && actStale) {
2548
+ killWorkerProcess(worker.pid, "SIGTERM");
2549
+ worker.status = "exited";
2550
+ worker.reconciledAt = (/* @__PURE__ */ new Date()).toISOString();
2551
+ worker.reconcileReason = `reconciled stale worker: ${status.attention.reason}`;
2552
+ saveWorker(run.id, worker);
2553
+ outcomes.push({
2554
+ runId: run.id,
2555
+ worker: name,
2556
+ action: "killed_stale",
2557
+ reason: status.attention.reason
2558
+ });
2559
+ continue;
2560
+ }
2561
+ }
2562
+ outcomes.push({
2563
+ runId: run.id,
2564
+ worker: name,
2565
+ action: "skipped",
2566
+ reason: status.attention.reason
2567
+ });
2568
+ }
2569
+ }
2570
+ return { workers: outcomes, finalizedRuns: finalizeStaleRuns() };
2571
+ }
2572
+
2093
2573
  // src/plan-progress-daemon-sync.ts
2094
- import path15 from "node:path";
2574
+ import path16 from "node:path";
2095
2575
 
2096
2576
  // src/plan-progress-sync.ts
2097
2577
  async function syncPlanProgress(args) {
2098
2578
  const base = resolveBaseUrl(args.baseUrl);
2099
2579
  const secret = await resolveCallbackSecretWithMint(args.secret, args.agentOsId, { baseUrl: base });
2100
2580
  const url = `${base}/api/agent-os/by-id/${encodeURIComponent(args.agentOsId)}/tasks/${encodeURIComponent(args.taskId)}/plan-progress-sync`;
2101
- const res = await postJson(url, secret, {
2581
+ const res = await postJsonWithCredentialRefresh(url, secret, {
2102
2582
  phase: args.phase,
2103
2583
  taskId: args.taskId,
2104
2584
  blocker: args.blocker,
2105
2585
  artifact: args.artifact
2106
- });
2586
+ }, { agentOsId: args.agentOsId, baseUrl: base });
2107
2587
  return { ok: res.ok, status: res.status, response: res.response };
2108
2588
  }
2109
2589
 
@@ -2115,7 +2595,7 @@ async function syncActiveWorkerPlanProgress(runId, args) {
2115
2595
  const outcomes = [];
2116
2596
  for (const name of Object.keys(run.workers || {})) {
2117
2597
  const worker = readJson(
2118
- path15.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
2598
+ path16.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
2119
2599
  void 0
2120
2600
  );
2121
2601
  if (!worker?.dispatched || !worker.taskId) continue;
@@ -2169,7 +2649,7 @@ async function completeFinishedWorkers(runId, args) {
2169
2649
  const outcomes = [];
2170
2650
  for (const name of Object.keys(run.workers || {})) {
2171
2651
  const worker = readJson(
2172
- path16.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
2652
+ path17.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
2173
2653
  void 0
2174
2654
  );
2175
2655
  if (!worker?.taskId) continue;
@@ -2194,6 +2674,7 @@ async function postOperatorTick(agentOsId, runId, resourceGate, args) {
2194
2674
  agentOsId,
2195
2675
  runId,
2196
2676
  ingestHarness: true,
2677
+ harnessBoardSnapshot: buildRunBoard(runId),
2197
2678
  resourceGate
2198
2679
  });
2199
2680
  return { ok: res.ok, httpStatus: res.status, response: res.response };
@@ -2204,7 +2685,7 @@ async function runPipelineTick(args) {
2204
2685
  const execute = args.execute !== false && args.execute !== "false";
2205
2686
  runStatus({ run: runId });
2206
2687
  const completedWorkers = await completeFinishedWorkers(runId, args);
2207
- const finalizedStaleRuns = finalizeStaleRuns();
2688
+ const staleReconcile = reconcileStaleWorkers();
2208
2689
  const planProgressSync = await syncActiveWorkerPlanProgress(runId, args);
2209
2690
  const workspacePrefs = await fetchWorkspaceRuntimePreferences(agentOsId, args);
2210
2691
  const resourceGate = observeRunnerResourceGate({
@@ -2245,7 +2726,7 @@ async function runPipelineTick(args) {
2245
2726
  execute,
2246
2727
  resourceGate,
2247
2728
  completedWorkers,
2248
- finalizedStaleRuns,
2729
+ staleReconcile,
2249
2730
  planProgressSync,
2250
2731
  operatorTick,
2251
2732
  sweep,
@@ -2405,7 +2886,7 @@ function usage(code = 0) {
2405
2886
  " kynver run create --repo /path/repo [--name name] [--base origin/main]",
2406
2887
  " kynver run list",
2407
2888
  " kynver run status --run RUN_ID",
2408
- " kynver run dispatch --run RUN_ID --agent-os-id AOS_ID [--base-url URL] [--secret SECRET] [--execute] [--lane any|implementation|review|landing] [--max-starts 1] [--lease-ms MS] [--owned path[,path]] [--model claude-opus-4-7] [--disk-path /]",
2889
+ " kynver run dispatch --run RUN_ID --agent-os-id AOS_ID [--base-url URL] [--secret SECRET] [--execute] [--lane any|implementation|review|landing] [--executor harness] [--max-starts 1] [--lease-ms MS] [--owned path[,path]] [--model claude-opus-4-7] [--disk-path /]",
2409
2890
  " kynver run sweep --run RUN_ID --agent-os-id AOS_ID [--base-url URL] [--secret SECRET] [--grace-ms MS]",
2410
2891
  ' kynver worker start --run RUN_ID --name worker --task "..." [--owned path[,path]] [--model MODEL] [--provider claude|cursor] [--agent-os-id AOS_ID] [--task-id TASK_ID]',
2411
2892
  " kynver worker status --run RUN_ID --name worker",