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