@kynver-app/runtime 0.1.61 → 0.1.66

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.
Files changed (49) hide show
  1. package/README.md +3 -3
  2. package/dist/box-resource-snapshot-shared.d.ts +2 -0
  3. package/dist/box-resource-snapshot.d.ts +45 -0
  4. package/dist/cleanup-active-worktrees.d.ts +14 -0
  5. package/dist/cleanup-build-cache-paths.d.ts +5 -0
  6. package/dist/cleanup-dependency-scan.d.ts +12 -0
  7. package/dist/cleanup-disk-pressure.d.ts +15 -0
  8. package/dist/cleanup-duplicate-worktrees.d.ts +7 -0
  9. package/dist/cleanup-execute.d.ts +4 -0
  10. package/dist/cleanup-guards-helpers.d.ts +1 -1
  11. package/dist/cleanup-guards.d.ts +13 -0
  12. package/dist/cleanup-harness-roots.d.ts +6 -0
  13. package/dist/cleanup-retention-config.d.ts +3 -0
  14. package/dist/cleanup-scan.d.ts +1 -0
  15. package/dist/cleanup-types.d.ts +12 -1
  16. package/dist/cleanup-worktree-index.d.ts +2 -0
  17. package/dist/cli.js +1639 -558
  18. package/dist/cli.js.map +4 -4
  19. package/dist/harness-notice/harness-notice.auto-complete.d.ts +3 -0
  20. package/dist/harness-notice/harness-notice.monitor-tick.d.ts +6 -0
  21. package/dist/harness-notice/harness-notice.parse.d.ts +3 -0
  22. package/dist/harness-notice/harness-notice.tool-response.d.ts +3 -0
  23. package/dist/harness-notice/harness-notice.types.d.ts +17 -0
  24. package/dist/harness-notice/harness-notice.worker-complete.d.ts +3 -0
  25. package/dist/harness-notice/harness-notice.worker-status.d.ts +2 -0
  26. package/dist/harness-notice/index.d.ts +7 -0
  27. package/dist/harness-repair-target.d.ts +23 -0
  28. package/dist/heartbeat.d.ts +3 -0
  29. package/dist/index.d.ts +9 -2
  30. package/dist/index.js +2079 -700
  31. package/dist/index.js.map +4 -4
  32. package/dist/landing-contract-gate.d.ts +3 -1
  33. package/dist/model-routing.d.ts +2 -0
  34. package/dist/pr-handoff/pr-handoff-assess.d.ts +2 -0
  35. package/dist/pr-handoff/pr-handoff.types.d.ts +1 -1
  36. package/dist/prompt.d.ts +2 -0
  37. package/dist/repair-target-worktree.d.ts +4 -0
  38. package/dist/resource-gate.d.ts +5 -0
  39. package/dist/run-list.d.ts +38 -0
  40. package/dist/run-store.d.ts +2 -0
  41. package/dist/runner-identity.d.ts +12 -0
  42. package/dist/scheduler-cutover-cli.d.ts +2 -0
  43. package/dist/scheduler-cutover.d.ts +22 -0
  44. package/dist/status.d.ts +3 -0
  45. package/dist/supervisor.d.ts +2 -0
  46. package/dist/vercel/index.d.ts +1 -1
  47. package/dist/vercel/vercel-url.d.ts +2 -0
  48. package/dist/worker-provider-policy.d.ts +26 -0
  49. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -87,10 +87,6 @@ function tailFile(file, lines) {
87
87
  function readMaybeFile(file) {
88
88
  return file ? readFileSync(path.resolve(file), "utf8") : "";
89
89
  }
90
- function listRunIds(runsDir) {
91
- if (!existsSync(runsDir)) return [];
92
- return readdirSync(runsDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
93
- }
94
90
  function sleepMs(ms) {
95
91
  Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
96
92
  }
@@ -611,7 +607,7 @@ async function runSetup(args) {
611
607
  ...existing,
612
608
  ...inferSetupFields(existing, args),
613
609
  ...maxWorkersRaw ? { maxConcurrentWorkers: Math.max(1, Math.floor(Number(maxWorkersRaw))) } : {},
614
- workerProvider: typeof args.provider === "string" ? args.provider : existing.workerProvider || "claude"
610
+ workerProvider: typeof args.provider === "string" ? args.provider : existing.workerProvider || "cursor"
615
611
  });
616
612
  saveUserConfig(config);
617
613
  let runnerCredentialNote;
@@ -733,23 +729,23 @@ function isWslHost() {
733
729
  function observeWslHostDisk(options = {}) {
734
730
  const wsl = options.forceWsl === void 0 ? isWslHost() : options.forceWsl;
735
731
  if (!wsl) return null;
736
- const path43 = options.wslHostMount?.trim() || process.env.KYNVER_WSL_HOST_MOUNT?.trim() || DEFAULT_WSL_HOST_MOUNT;
732
+ const path50 = options.wslHostMount?.trim() || process.env.KYNVER_WSL_HOST_MOUNT?.trim() || DEFAULT_WSL_HOST_MOUNT;
737
733
  const warnBelowBytes = options.wslHostFreeWarnBytes ?? DEFAULT_WSL_HOST_WARN_FREE_BYTES;
738
734
  const criticalBelowBytes = options.wslHostFreeCriticalBytes ?? DEFAULT_WSL_HOST_CRITICAL_FREE_BYTES;
739
735
  const statfs = options.statfs ?? statfsSync;
740
736
  let stats;
741
737
  try {
742
- stats = statfs(path43);
738
+ stats = statfs(path50);
743
739
  } catch (error) {
744
740
  return {
745
741
  ok: false,
746
- path: path43,
742
+ path: path50,
747
743
  freeBytes: 0,
748
744
  totalBytes: 0,
749
745
  usedPercent: 100,
750
746
  warnBelowBytes,
751
747
  criticalBelowBytes,
752
- reason: `Windows host disk probe failed at ${path43}: ${error.message}`,
748
+ reason: `Windows host disk probe failed at ${path50}: ${error.message}`,
753
749
  probeError: error.message
754
750
  };
755
751
  }
@@ -763,11 +759,11 @@ function observeWslHostDisk(options = {}) {
763
759
  let reason = null;
764
760
  if (!ok) {
765
761
  const tag = criticalFree ? "critical" : "warning";
766
- reason = `Windows host disk ${path43} at ${tag}: ${freeGiB} GiB free (<${(criticalFree ? criticalBelowBytes : warnBelowBytes) / 1024 / 1024 / 1024} GiB); WSL VHDX cannot grow safely. ${summarizeWslRecoverySteps()}`;
762
+ reason = `Windows host disk ${path50} at ${tag}: ${freeGiB} GiB free (<${(criticalFree ? criticalBelowBytes : warnBelowBytes) / 1024 / 1024 / 1024} GiB); WSL VHDX cannot grow safely. ${summarizeWslRecoverySteps()}`;
767
763
  }
768
764
  return {
769
765
  ok,
770
- path: path43,
766
+ path: path50,
771
767
  freeBytes,
772
768
  totalBytes,
773
769
  usedPercent,
@@ -787,12 +783,12 @@ var DEFAULT_CRITICAL_FREE_BYTES = 15 * 1024 * 1024 * 1024;
787
783
  var DEFAULT_MAX_USED_PERCENT = 80;
788
784
  var DEFAULT_HARD_MAX_USED_PERCENT = 90;
789
785
  function observeRunnerDiskGate(input = {}) {
790
- const path43 = input.diskPath?.trim() || "/";
786
+ const path50 = input.diskPath?.trim() || "/";
791
787
  const warnBelowBytes = input.diskFreeWarnBytes ?? DEFAULT_WARN_FREE_BYTES;
792
788
  const criticalBelowBytes = input.diskFreeCriticalBytes ?? DEFAULT_CRITICAL_FREE_BYTES;
793
789
  const maxUsedPercent = input.diskMaxUsedPercent ?? DEFAULT_MAX_USED_PERCENT;
794
790
  const hardMaxUsedPercent = input.diskHardMaxUsedPercent ?? DEFAULT_HARD_MAX_USED_PERCENT;
795
- const stats = statfsSync2(path43);
791
+ const stats = statfsSync2(path50);
796
792
  const freeBytes = Number(stats.bavail) * Number(stats.bsize);
797
793
  const totalBytes = Number(stats.blocks) * Number(stats.bsize);
798
794
  const usedPercent = totalBytes > 0 ? (totalBytes - freeBytes) / totalBytes * 100 : 100;
@@ -815,7 +811,7 @@ function observeRunnerDiskGate(input = {}) {
815
811
  }
816
812
  return {
817
813
  ok,
818
- path: path43,
814
+ path: path50,
819
815
  freeBytes,
820
816
  totalBytes,
821
817
  usedPercent,
@@ -895,6 +891,12 @@ function loadRun(id) {
895
891
  }
896
892
  function listRunRecords() {
897
893
  const { runsDir } = getPaths();
894
+ return listRunRecordsAt(runsDir);
895
+ }
896
+ function listRunRecordsForHarnessRoot(harnessRoot) {
897
+ return listRunRecordsAt(path6.join(harnessRoot, "runs"));
898
+ }
899
+ function listRunRecordsAt(runsDir) {
898
900
  if (!existsSync6(runsDir)) return [];
899
901
  const runs = [];
900
902
  for (const entry of readdirSync2(runsDir, { withFileTypes: true })) {
@@ -923,7 +925,10 @@ function saveWorker(runId, worker) {
923
925
  }
924
926
  function runDirectory(id) {
925
927
  const { runsDir } = getPaths();
926
- return runDir(runsDir, safeSlug(id));
928
+ return runDirectoryAt(runsDir, id);
929
+ }
930
+ function runDirectoryAt(harnessRoot, id) {
931
+ return runDir(path6.join(harnessRoot, "runs"), safeSlug(id));
927
932
  }
928
933
 
929
934
  // src/heartbeat.ts
@@ -944,7 +949,9 @@ function parseHeartbeat(file) {
944
949
  lastHeartbeatPhase: null,
945
950
  lastHeartbeatSummary: null,
946
951
  heartbeatBlocker: null,
947
- timestampAnomalies: []
952
+ timestampAnomalies: [],
953
+ lastBoxResourceSnapshot: null,
954
+ lastPrEvidence: []
948
955
  };
949
956
  if (!existsSync7(file)) return result;
950
957
  const maxFutureMs = Date.now() + HEARTBEAT_FUTURE_SKEW_MS;
@@ -971,6 +978,14 @@ function parseHeartbeat(file) {
971
978
  if (row.phase !== void 0 && row.phase !== null) result.lastHeartbeatPhase = String(row.phase);
972
979
  if (row.summary !== void 0 && row.summary !== null) result.lastHeartbeatSummary = String(row.summary);
973
980
  result.heartbeatBlocker = row.blocker ? String(row.blocker) : null;
981
+ if (row.boxResourceSnapshot && typeof row.boxResourceSnapshot === "object" && !Array.isArray(row.boxResourceSnapshot)) {
982
+ result.lastBoxResourceSnapshot = row.boxResourceSnapshot;
983
+ }
984
+ if (Array.isArray(row.prEvidence)) {
985
+ result.lastPrEvidence = row.prEvidence.filter(
986
+ (entry2) => !!entry2 && typeof entry2 === "object" && typeof entry2.prUrl === "string"
987
+ );
988
+ }
974
989
  }
975
990
  return result;
976
991
  }
@@ -1488,10 +1503,35 @@ function workerPrUrls(snapshot, finalResult) {
1488
1503
  function assessWorkerLandingContract(input) {
1489
1504
  const { contract, snapshot } = input;
1490
1505
  const finalResult = input.finalResult ?? snapshot.finalResult;
1491
- if (!contract.landingOnly && contract.targetPrUrls.length === 0) {
1506
+ if (!contract.landingOnly && contract.targetPrUrls.length === 0 && !contract.repairEnforceOriginalPr) {
1492
1507
  return { blocked: false };
1493
1508
  }
1494
1509
  if (!hasFinalResult3(finalResult)) return { blocked: false };
1510
+ const repairTarget = contract.repairEnforceOriginalPr ? normalizePrUrl(trimOrNull3(contract.targetPrUrl) ?? "") ?? (contract.targetPrUrls.length === 1 ? normalizePrUrl(contract.targetPrUrls[0]) : null) : null;
1511
+ if (repairTarget) {
1512
+ const workerPrs2 = workerPrUrls(snapshot, finalResult);
1513
+ const supersedes = finalResult && typeof finalResult === "object" && !Array.isArray(finalResult) && finalResult.supersedesOriginalTargetPr === true;
1514
+ if (!supersedes) {
1515
+ for (const pr of workerPrs2) {
1516
+ if (pr !== repairTarget) {
1517
+ return {
1518
+ blocked: true,
1519
+ reason: "duplicate_repair_pr",
1520
+ detail: `Repair worker opened or attached PR ${pr} instead of canonical target ${repairTarget}`
1521
+ };
1522
+ }
1523
+ }
1524
+ }
1525
+ const reconciliation2 = parseReconciliation(finalResult);
1526
+ const entry = reconciliation2.find((r) => r.prUrl === repairTarget);
1527
+ if (!entry || entry.outcome !== "merged" && !(entry.reason?.trim() && (entry.outcome === "skipped" || entry.outcome === "blocked"))) {
1528
+ return {
1529
+ blocked: true,
1530
+ reason: "missing_repair_target_reconciliation",
1531
+ detail: `Repair worker must reconcile target PR ${repairTarget}`
1532
+ };
1533
+ }
1534
+ }
1495
1535
  const reconciliation = parseReconciliation(finalResult);
1496
1536
  const byUrl = new Map(reconciliation.map((r) => [r.prUrl, r]));
1497
1537
  const targetSet = new Set(
@@ -1646,6 +1686,12 @@ function computeWorkerStatus(worker, options = {}) {
1646
1686
  ]);
1647
1687
  const error = parsed.error || (!alive && !finalResult ? tailFile(worker.stderrPath, 10).trim() || void 0 : void 0);
1648
1688
  const completionBlocker = typeof worker.completionBlocker === "string" && worker.completionBlocker.trim() ? worker.completionBlocker.trim() : null;
1689
+ const landingContract = worker.repairTargetPrUrl ? {
1690
+ landingOnly: false,
1691
+ targetPrUrls: [worker.repairTargetPrUrl],
1692
+ targetPrUrl: worker.repairTargetPrUrl,
1693
+ repairEnforceOriginalPr: true
1694
+ } : null;
1649
1695
  const attention = computeAttention({
1650
1696
  alive,
1651
1697
  finalResult,
@@ -1658,7 +1704,9 @@ function computeWorkerStatus(worker, options = {}) {
1658
1704
  error,
1659
1705
  changedFiles,
1660
1706
  gitAncestry,
1661
- completionBlocker
1707
+ completionBlocker,
1708
+ landingContract,
1709
+ prUrl: worker.repairTargetPrUrl ?? worker.taskPrUrl ?? null
1662
1710
  });
1663
1711
  const workerStatusLabel = completionBlocker || attention.state === "blocked" ? "blocked" : completionAcknowledged || attention.state === "done" ? "done" : finalResult ? "exited" : alive ? "running" : "exited";
1664
1712
  return {
@@ -1780,9 +1828,16 @@ function observeRunnerResourceGate(input) {
1780
1828
  const slotsByCapacity = Math.max(0, maxConcurrentWorkers - activeWorkers);
1781
1829
  const slotsByFreeMem = capacityFromFree;
1782
1830
  let slotsAvailable = Math.min(slotsByCapacity, slotsByFreeMem);
1831
+ const skipDisk = input.skipDiskGate || process.env.KYNVER_RESOURCE_GATE_SKIP_DISK === "1";
1832
+ const diskGate = skipDisk ? void 0 : observeRunnerDiskGate({
1833
+ diskPath: input.diskPath?.trim() || process.env.KYNVER_DISK_GUARD_PATH?.trim() || "/"
1834
+ });
1835
+ if (diskGate && !diskGate.ok) slotsAvailable = 0;
1783
1836
  let reason = null;
1784
1837
  if (slotsAvailable <= 0) {
1785
- if (activeWorkers >= maxConcurrentWorkers) {
1838
+ if (diskGate && !diskGate.ok) {
1839
+ reason = diskGate.reason ?? "disk gate blocked worker admission";
1840
+ } else if (activeWorkers >= maxConcurrentWorkers) {
1786
1841
  reason = `at worker limit (${activeWorkers}/${maxConcurrentWorkers} running)`;
1787
1842
  } else if (capacityFromFree <= 0) {
1788
1843
  reason = "insufficient free memory \u2014 waiting for workers to finish";
@@ -1802,7 +1857,8 @@ function observeRunnerResourceGate(input) {
1802
1857
  maxConcurrentWorkers,
1803
1858
  activeWorkers,
1804
1859
  slotsAvailable,
1805
- reason
1860
+ reason,
1861
+ ...diskGate ? { diskGate } : {}
1806
1862
  };
1807
1863
  }
1808
1864
 
@@ -1990,10 +2046,76 @@ var claudeProvider = {
1990
2046
  }
1991
2047
  };
1992
2048
 
2049
+ // src/worker-provider-policy.ts
2050
+ var DEFAULT_WORKER_PROVIDER = "cursor";
2051
+ var CLAUDE_FAMILY = /* @__PURE__ */ new Set(["claude", "opus", "anthropic"]);
2052
+ var TASK_OVERRIDE_MARKERS = [
2053
+ /\[worker-provider:\s*claude\]/i,
2054
+ /\[use-claude-worker\]/i,
2055
+ /\[operator-worker-provider:\s*claude\]/i
2056
+ ];
2057
+ function taskString2(task, key) {
2058
+ const v = task[key];
2059
+ return typeof v === "string" ? v.trim() : "";
2060
+ }
2061
+ function isClaudeFamilyProvider(provider) {
2062
+ if (!provider?.trim()) return false;
2063
+ const normalized = provider.trim().toLowerCase();
2064
+ if (CLAUDE_FAMILY.has(normalized)) return true;
2065
+ return normalized.includes("claude") || normalized.includes("opus");
2066
+ }
2067
+ function taskAllowsClaudeWorker(task) {
2068
+ if (!task) return false;
2069
+ const override = task.workerProviderOverride;
2070
+ if (typeof override === "string" && isClaudeFamilyProvider(override)) return true;
2071
+ const ref = taskString2(task, "executorRef").toLowerCase();
2072
+ if (ref === "provider:claude" || ref.startsWith("provider:claude:")) return true;
2073
+ if (ref.includes("claude-worker-override") || ref.includes("operator-claude")) return true;
2074
+ const description = taskString2(task, "description");
2075
+ if (TASK_OVERRIDE_MARKERS.some((re) => re.test(description))) return true;
2076
+ const title = taskString2(task, "title");
2077
+ if (/\[use-claude-worker\]/i.test(title)) return true;
2078
+ return false;
2079
+ }
2080
+ function coerceCursorModel(model, ruleSuffix) {
2081
+ const coerced = {
2082
+ provider: DEFAULT_WORKER_PROVIDER,
2083
+ model: CURSOR_DEFAULT_MODEL,
2084
+ rule: `policy:cursor_default${ruleSuffix}`,
2085
+ requestedModel: model
2086
+ };
2087
+ return coerced;
2088
+ }
2089
+ function enforceCursorWorkerProvider(input) {
2090
+ const { routing, task } = input;
2091
+ const explicit = input.explicitProvider?.trim().toLowerCase();
2092
+ if (input.explicitProviderIsOperatorOverride && isClaudeFamilyProvider(explicit)) {
2093
+ return {
2094
+ ...routing,
2095
+ provider: "claude",
2096
+ rule: routing.rule.startsWith("explicit:") ? routing.rule : "explicit:operator_provider"
2097
+ };
2098
+ }
2099
+ if (taskAllowsClaudeWorker(task)) {
2100
+ return routing;
2101
+ }
2102
+ if (!isClaudeFamilyProvider(routing.provider)) {
2103
+ return routing;
2104
+ }
2105
+ const suffix = routing.rule && routing.rule !== "default:global" ? `:${routing.rule.replace(/:/g, "_")}` : "";
2106
+ return coerceCursorModel(routing.model, suffix);
2107
+ }
2108
+ function resolveConfiguredWorkerProvider(configured, fallback = DEFAULT_WORKER_PROVIDER) {
2109
+ const trimmed = configured?.trim();
2110
+ if (!trimmed) return fallback;
2111
+ if (isClaudeFamilyProvider(trimmed)) return DEFAULT_WORKER_PROVIDER;
2112
+ return trimmed;
2113
+ }
2114
+
1993
2115
  // src/model-routing.ts
1994
2116
  var GLOBAL_DEFAULT_MODEL = "claude-sonnet-4-6";
1995
2117
  var CURSOR_DEFAULT_MODEL = "composer-2.5";
1996
- function taskString2(task, key) {
2118
+ function taskString3(task, key) {
1997
2119
  const v = task[key];
1998
2120
  return typeof v === "string" ? v.trim() : "";
1999
2121
  }
@@ -2009,11 +2131,14 @@ function resolveGlobalDefaultModel(config = loadUserConfig()) {
2009
2131
  }
2010
2132
  function inferProviderFromModel(model) {
2011
2133
  const m = (model ?? "").toLowerCase();
2012
- if (!m) return "claude";
2134
+ if (!m) return "cursor";
2013
2135
  if (m.includes("composer") || m.includes("cursor") || m.includes("codex") || m.startsWith("gpt-") || m.startsWith("gpt5")) {
2014
2136
  return "cursor";
2015
2137
  }
2016
- return "claude";
2138
+ if (/^claude[-_]/i.test(m) || /^(?:opus|sonnet|haiku)\b/i.test(m)) {
2139
+ return "claude";
2140
+ }
2141
+ return "cursor";
2017
2142
  }
2018
2143
  function normalizeProviderAliasModel(model, explicitProvider) {
2019
2144
  const alias = model.trim().toLowerCase();
@@ -2047,41 +2172,33 @@ function isOpusLane(ref, title) {
2047
2172
  return false;
2048
2173
  }
2049
2174
  function inferModelRoutingFromTask(task) {
2050
- const ref = normalizeRef(taskString2(task, "executorRef"));
2051
- const title = taskString2(task, "title").toLowerCase();
2052
- const priority = taskString2(task, "priority") || "normal";
2053
- const roleLane = normalizeRef(taskString2(task, "roleLane"));
2175
+ const ref = normalizeRef(taskString3(task, "executorRef"));
2176
+ const title = taskString3(task, "title").toLowerCase();
2177
+ const priority = taskString3(task, "priority") || "normal";
2178
+ const roleLane = normalizeRef(taskString3(task, "roleLane"));
2054
2179
  if (ref.includes("cursor") || ref.includes("codex") || ref.includes("composer") || ref.includes("copilot") || roleLane === "implementer" || roleLane === "repair_implementer") {
2055
2180
  return { provider: "cursor", rule: "lane:implementation" };
2056
2181
  }
2057
2182
  if (ref.includes("landing") || title.startsWith("land:") || title.includes(" merge")) {
2058
- return {
2059
- model: "claude-haiku-4-5-20251001",
2060
- provider: "claude",
2061
- rule: "lane:landing"
2062
- };
2183
+ return { provider: "cursor", rule: "lane:landing" };
2063
2184
  }
2064
2185
  if (ref.includes("review") || /^review[\s:]/.test(title) || roleLane.includes("review")) {
2065
2186
  if (isOpusLane(ref, title) || roleLane === "deep_reviewer") {
2066
- return { model: "claude-opus-4-7", provider: "claude", rule: "lane:deep_review" };
2187
+ return { provider: "cursor", rule: "lane:deep_review" };
2067
2188
  }
2068
- return { model: "claude-sonnet-4-6", provider: "claude", rule: "lane:review" };
2189
+ return { provider: "cursor", rule: "lane:review" };
2069
2190
  }
2070
2191
  if (isOpusLane(ref, title) || roleLane === "plan_author") {
2071
- return { model: "claude-opus-4-7", provider: "claude", rule: "lane:planning" };
2192
+ return { provider: "cursor", rule: "lane:planning" };
2072
2193
  }
2073
2194
  if (priority === "critical") {
2074
- return { model: "claude-opus-4-7", provider: "claude", rule: "priority:critical" };
2195
+ return { provider: "cursor", rule: "priority:critical" };
2075
2196
  }
2076
2197
  if (priority === "high") {
2077
- return { model: "claude-sonnet-4-6", provider: "claude", rule: "priority:high" };
2198
+ return { provider: "cursor", rule: "priority:high" };
2078
2199
  }
2079
2200
  if (priority === "low") {
2080
- return {
2081
- model: "claude-haiku-4-5-20251001",
2082
- provider: "claude",
2083
- rule: "priority:low"
2084
- };
2201
+ return { provider: "cursor", rule: "priority:low" };
2085
2202
  }
2086
2203
  const model = resolveGlobalDefaultModel();
2087
2204
  return {
@@ -2091,31 +2208,41 @@ function inferModelRoutingFromTask(task) {
2091
2208
  };
2092
2209
  }
2093
2210
  function resolveWorkerLaunch(input) {
2211
+ let decision;
2094
2212
  if (input.explicitModel?.trim()) {
2095
- const model2 = input.explicitModel.trim();
2096
- const providerAlias = normalizeProviderAliasModel(model2, input.explicitProvider);
2097
- if (providerAlias) return providerAlias;
2098
- return {
2099
- model: model2,
2100
- provider: input.explicitProvider?.trim() || inferProviderFromModel(model2),
2101
- rule: "explicit:cli",
2102
- requestedModel: model2
2103
- };
2104
- }
2105
- if (input.task && Object.keys(input.task).length > 0) {
2213
+ const model = input.explicitModel.trim();
2214
+ const providerAlias = normalizeProviderAliasModel(model, input.explicitProvider);
2215
+ if (providerAlias) {
2216
+ decision = providerAlias;
2217
+ } else {
2218
+ decision = {
2219
+ model,
2220
+ provider: input.explicitProvider?.trim() || inferProviderFromModel(model),
2221
+ rule: "explicit:cli",
2222
+ requestedModel: model
2223
+ };
2224
+ }
2225
+ } else if (input.task && Object.keys(input.task).length > 0) {
2106
2226
  const inferred = inferModelRoutingFromTask(input.task);
2107
- return {
2227
+ decision = {
2108
2228
  ...inferred,
2109
2229
  requestedModel: inferred.model
2110
2230
  };
2231
+ } else {
2232
+ const model = resolveGlobalDefaultModel();
2233
+ decision = {
2234
+ model,
2235
+ provider: input.explicitProvider?.trim() || inferProviderFromModel(model),
2236
+ rule: "default:global",
2237
+ requestedModel: model
2238
+ };
2111
2239
  }
2112
- const model = resolveGlobalDefaultModel();
2113
- return {
2114
- model,
2115
- provider: input.explicitProvider?.trim() || inferProviderFromModel(model),
2116
- rule: "default:global",
2117
- requestedModel: model
2118
- };
2240
+ return enforceCursorWorkerProvider({
2241
+ routing: decision,
2242
+ task: input.task,
2243
+ explicitProvider: input.explicitProvider,
2244
+ explicitProviderIsOperatorOverride: input.explicitProviderIsOperatorOverride
2245
+ });
2119
2246
  }
2120
2247
  function resolveModelFallback(startedModel, launchModel, providerDefault) {
2121
2248
  return startedModel || launchModel || providerDefault || resolveGlobalDefaultModel() || CLAUDE_DEFAULT_MODEL;
@@ -2207,6 +2334,76 @@ function hasLiveWorkerForTask(runId, taskId) {
2207
2334
  import { existsSync as existsSync12, mkdirSync as mkdirSync3 } from "node:fs";
2208
2335
  import path14 from "node:path";
2209
2336
 
2337
+ // src/harness-repair-target.ts
2338
+ var HARNESS_CONTRACT_RE = /<!--\s*harness-contract:\s*(\{[\s\S]*?\})\s*-->/i;
2339
+ var FIX_EXECUTOR_REF_PREFIX = "next-action-fix:";
2340
+ function trimOrNull4(value) {
2341
+ if (typeof value !== "string") return null;
2342
+ const t = value.trim();
2343
+ return t.length ? t : null;
2344
+ }
2345
+ function normalizePrUrl2(url) {
2346
+ const m = url.trim().match(/github\.com\/([^/]+\/[^/]+)\/(?:pull|pulls)\/(\d+)/i);
2347
+ if (!m) return trimOrNull4(url);
2348
+ return `https://github.com/${m[1]}/pull/${m[2]}`;
2349
+ }
2350
+ function isHarnessRepairTask(task) {
2351
+ const title = (task.title ?? "").trim().toLowerCase();
2352
+ if (title.startsWith("fix:") || title.startsWith("repair:")) return true;
2353
+ const ref = (task.executorRef ?? "").toLowerCase();
2354
+ if (ref.startsWith(FIX_EXECUTOR_REF_PREFIX)) return true;
2355
+ if (ref.includes("repair") || ref.includes("unblock")) return true;
2356
+ return false;
2357
+ }
2358
+ function parseRepairTargetContractFromDescription(description) {
2359
+ const empty = {
2360
+ repairEnforceOriginalPr: false,
2361
+ targetPrUrl: null,
2362
+ targetPrBranch: null
2363
+ };
2364
+ if (!description) return empty;
2365
+ const m = description.match(HARNESS_CONTRACT_RE);
2366
+ if (!m?.[1]) return empty;
2367
+ try {
2368
+ const parsed = JSON.parse(m[1]);
2369
+ const url = trimOrNull4(
2370
+ String(parsed.targetPrUrl ?? parsed.target_pr_url ?? "")
2371
+ );
2372
+ const branch = trimOrNull4(
2373
+ String(parsed.targetPrBranch ?? parsed.target_pr_branch ?? "")
2374
+ );
2375
+ const enforce = parsed.repairEnforceOriginalPr === true || parsed.repair_enforce_original_pr === true;
2376
+ return {
2377
+ repairEnforceOriginalPr: enforce || Boolean(url),
2378
+ targetPrUrl: url ? normalizePrUrl2(url) : null,
2379
+ targetPrBranch: branch
2380
+ };
2381
+ } catch {
2382
+ return empty;
2383
+ }
2384
+ }
2385
+ function resolveHarnessRepairTargetFromTask(task) {
2386
+ if (!isHarnessRepairTask(task)) return null;
2387
+ const block = parseRepairTargetContractFromDescription(task.description);
2388
+ const taskPr = task.prUrl ? normalizePrUrl2(task.prUrl) : null;
2389
+ const targetPrUrl = block.targetPrUrl ?? taskPr;
2390
+ if (!targetPrUrl) return null;
2391
+ return {
2392
+ targetPrUrl,
2393
+ targetPrBranch: block.targetPrBranch ?? trimOrNull4(task.branch)
2394
+ };
2395
+ }
2396
+ function repairTargetPromptLines(target) {
2397
+ return [
2398
+ "Repair target PR policy:",
2399
+ `- Work on the existing target PR branch \u2014 do not open a duplicate repair PR by default.`,
2400
+ `- Canonical target PR: ${target.targetPrUrl}`,
2401
+ ...target.targetPrBranch ? [`- Canonical target branch: \`${target.targetPrBranch}\` (checkout is already on this branch).`] : [],
2402
+ `- Reconcile ${target.targetPrUrl} in structured finalResult.targetPrReconciliation.`,
2403
+ `- Only supersede the original when the branch is inaccessible: set supersedesOriginalTargetPr: true with reason and close/comment on the original PR.`
2404
+ ];
2405
+ }
2406
+
2210
2407
  // src/prompt.ts
2211
2408
  function buildPrompt(input) {
2212
2409
  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.";
@@ -2251,7 +2448,7 @@ function buildPrompt(input) {
2251
2448
  `Progress heartbeat file: ${input.heartbeatPath}`,
2252
2449
  "After each major step, append one JSON line to the heartbeat file with fields: ts, phase, summary, changedFiles, blocker.",
2253
2450
  "Final response must include files changed, verification commands, and unresolved risks.",
2254
- "Structured final result (recommended): record completion as JSON with summary, laneExpertise { whatChanged, why, files, prUrls, verification, risks, blockers, lessonsLearned, laneGuidance }, and targetPrReconciliation [{ prUrl, outcome: merged|skipped|blocked, mergeCommit?, reason? }] for every target PR on landing-only tasks.",
2451
+ "Structured final result (recommended): record completion as JSON with summary, laneExpertise { whatChanged, why, files, prUrls, verification, risks, blockers, lessonsLearned, laneGuidance }, and targetPrReconciliation [{ prUrl, outcome: merged|skipped|blocked, mergeCommit?, reason? }] for every target PR on landing-only tasks. Persona-attributed tasks: put repeatable lane lessons in lessonsLearned/laneGuidance (with evidence); substantive rows auto-persist as persona-scoped Lane A rules \u2014 global cross-lane policy stays in owner memory, not worker lessons.",
2255
2452
  "Completion handoff (required): before you stop, ensure the harness records a final result \u2014 summarize outcome in your last message and append a heartbeat line with phase `complete`. If you leave uncommitted changes or committed work without a PR, the orchestrator blocks completion until a GitHub PR exists (or you discard/commit cleanly). One-off helper scripts must be removed (`kynver worker discard-disposable --path <file>`) or committed before completion \u2014 maintenance/board-drain workers are not exempt. Exiting with only dirty files and no PR routes to salvage review, not production review.",
2256
2453
  "PR-ready handoff: for substantial implementation work, commit, push, and open a GitHub PR (draft OK) on your branch before finishing \u2014 or rely on the harness to run `gh pr create` at completion when `gh` is authenticated.",
2257
2454
  "Expert review / production-review workers (Dalton/Lorentz, plan-review-task, scheduledJob reviewer children): do NOT open new implementation PRs \u2014 review the parent task's existing PR and record reviewVerdict in finalResult; landing-contract targetPrReconciliation does not apply.",
@@ -2268,6 +2465,13 @@ function buildPrompt(input) {
2268
2465
  "",
2269
2466
  ...input.personaMarkdown?.trim() ? [input.personaMarkdown.trim(), ""] : [],
2270
2467
  ...input.instructionPolicyMarkdown?.trim() ? ["Operating rules (Lane A \u2014 from AgentOS memory policy):", input.instructionPolicyMarkdown.trim(), ""] : [],
2468
+ ...input.repairTargetPrUrl ? [
2469
+ ...repairTargetPromptLines({
2470
+ targetPrUrl: input.repairTargetPrUrl,
2471
+ targetPrBranch: input.repairTargetBranch ?? null
2472
+ }),
2473
+ ""
2474
+ ] : [],
2271
2475
  "Task:",
2272
2476
  input.task
2273
2477
  ].join("\n");
@@ -2433,7 +2637,15 @@ var BUILTIN = {
2433
2637
  var overrideProvider = null;
2434
2638
  function resolveWorkerProvider(name) {
2435
2639
  if (overrideProvider) return overrideProvider;
2436
- const configured = (name || loadUserConfig().workerProvider || "claude").trim();
2640
+ const explicit = name?.trim();
2641
+ if (explicit) {
2642
+ const provider2 = BUILTIN[explicit];
2643
+ if (!provider2) {
2644
+ throw new Error(`unknown worker provider "${explicit}" \u2014 supported: ${Object.keys(BUILTIN).join(", ")}`);
2645
+ }
2646
+ return provider2;
2647
+ }
2648
+ const configured = resolveConfiguredWorkerProvider(loadUserConfig().workerProvider);
2437
2649
  const provider = BUILTIN[configured];
2438
2650
  if (!provider) {
2439
2651
  throw new Error(`unknown worker provider "${configured}" \u2014 supported: ${Object.keys(BUILTIN).join(", ")}`);
@@ -2529,7 +2741,7 @@ var NO_PR_COMMITS_BETWEEN_RE = /no commits between/i;
2529
2741
  function isGhNoCommitsBetweenError(detail) {
2530
2742
  return Boolean(detail && NO_PR_COMMITS_BETWEEN_RE.test(detail));
2531
2743
  }
2532
- function trimOrNull4(value) {
2744
+ function trimOrNull5(value) {
2533
2745
  if (typeof value !== "string") return null;
2534
2746
  const trimmed = value.trim();
2535
2747
  return trimmed.length ? trimmed : null;
@@ -2537,7 +2749,7 @@ function trimOrNull4(value) {
2537
2749
  function committedHead(ancestry) {
2538
2750
  if (!ancestry?.checked) return null;
2539
2751
  if (ancestry.headIsAncestorOfBase !== false) return null;
2540
- return trimOrNull4(ancestry.head);
2752
+ return trimOrNull5(ancestry.head);
2541
2753
  }
2542
2754
  function extractPrUrlFromText(value) {
2543
2755
  if (value === void 0 || value === null) return null;
@@ -2545,7 +2757,7 @@ function extractPrUrlFromText(value) {
2545
2757
  const m = text.match(
2546
2758
  /https?:\/\/[^\s)>"]+\/(?:pull|pulls|merge_requests|pull-requests)\/\d+/i
2547
2759
  );
2548
- return m ? trimOrNull4(m[0]) : null;
2760
+ return m ? trimOrNull5(m[0]) : null;
2549
2761
  }
2550
2762
  function countCommitsAheadOfBase(worktreePath, baseRef, exec) {
2551
2763
  const base = baseRef.trim();
@@ -2557,21 +2769,21 @@ function countCommitsAheadOfBase(worktreePath, baseRef, exec) {
2557
2769
  }
2558
2770
  function isReviewArtifactWorker(worker, snapshot) {
2559
2771
  if (snapshot.changedFiles.length > 0) return false;
2560
- const persona = trimOrNull4(worker.personaSlug)?.toLowerCase();
2772
+ const persona = trimOrNull5(worker.personaSlug)?.toLowerCase();
2561
2773
  if (persona && REVIEW_PERSONA_SLUGS.has(persona)) return true;
2562
- const rule = trimOrNull4(worker.routingRule) ?? "";
2774
+ const rule = trimOrNull5(worker.routingRule) ?? "";
2563
2775
  if (rule && REVIEW_LANE_RULE.test(rule)) return true;
2564
2776
  return false;
2565
2777
  }
2566
2778
  function hasWorkProduct(snapshot, options) {
2567
2779
  if (snapshot.changedFiles.length > 0) return true;
2568
- const baseRef = trimOrNull4(options?.baseRef);
2780
+ const baseRef = trimOrNull5(options?.baseRef);
2569
2781
  if (baseRef && options?.exec && options.worktreePath) {
2570
2782
  const ahead = countCommitsAheadOfBase(options.worktreePath, baseRef, options.exec);
2571
2783
  if (ahead === 0) return false;
2572
2784
  if (ahead !== null && ahead > 0) return true;
2573
2785
  }
2574
- if (trimOrNull4(snapshot.headCommit)) return true;
2786
+ if (trimOrNull5(snapshot.headCommit)) return true;
2575
2787
  if (committedHead(snapshot.gitAncestry)) return true;
2576
2788
  return false;
2577
2789
  }
@@ -2587,7 +2799,7 @@ function assessPrHandoffRequirement(input) {
2587
2799
  })) {
2588
2800
  return { required: false, reason: "expert_review_task" };
2589
2801
  }
2590
- const rule = trimOrNull4(input.routingRule) ?? "";
2802
+ const rule = trimOrNull5(input.routingRule) ?? "";
2591
2803
  if (rule && REVIEW_LANE_RULE.test(rule)) {
2592
2804
  return { required: false, reason: "review_lane" };
2593
2805
  }
@@ -2598,10 +2810,14 @@ function assessPrHandoffRequirement(input) {
2598
2810
  if (isReviewArtifactWorker(workerCtx, input.snapshot)) {
2599
2811
  return { required: false, reason: "review_artifact" };
2600
2812
  }
2601
- if (trimOrNull4(input.patchPath) || trimOrNull4(input.artifactBundlePath)) {
2813
+ if (trimOrNull5(input.patchPath) || trimOrNull5(input.artifactBundlePath)) {
2602
2814
  return { required: false, reason: "patch_or_bundle" };
2603
2815
  }
2604
- const prUrl = trimOrNull4(input.prUrl) ?? trimOrNull4(input.taskPrUrl) ?? trimOrNull4(input.snapshot.prUrl);
2816
+ const repairTarget = trimOrNull5(input.repairTargetPrUrl);
2817
+ if (repairTarget) {
2818
+ return { required: false, reason: "repair_target_pr" };
2819
+ }
2820
+ const prUrl = trimOrNull5(input.prUrl) ?? trimOrNull5(input.taskPrUrl) ?? trimOrNull5(input.snapshot.prUrl);
2605
2821
  if (prUrl) {
2606
2822
  return { required: false, reason: "already_has_pr" };
2607
2823
  }
@@ -2622,8 +2838,8 @@ function buildPrHandoffSnapshotFromStatus(status, extras) {
2622
2838
  worktreePath: status.worktreePath,
2623
2839
  gitAncestry: status.gitAncestry,
2624
2840
  finalResult: status.finalResult,
2625
- headCommit: trimOrNull4(extras?.headCommit) ?? committedHead(status.gitAncestry),
2626
- prUrl: trimOrNull4(extras?.prUrl) ?? null
2841
+ headCommit: trimOrNull5(extras?.headCommit) ?? committedHead(status.gitAncestry),
2842
+ prUrl: trimOrNull5(extras?.prUrl) ?? null
2627
2843
  };
2628
2844
  }
2629
2845
 
@@ -2829,6 +3045,7 @@ function ensurePrReadyHandoff(input, exec = defaultPrHandoffExec) {
2829
3045
  executorRef: input.worker.executorRef,
2830
3046
  parentTaskId: input.worker.parentTaskId,
2831
3047
  taskPrUrl: input.worker.taskPrUrl,
3048
+ repairTargetPrUrl: input.worker.repairTargetPrUrl,
2832
3049
  baseRef,
2833
3050
  exec,
2834
3051
  worker: input.worker,
@@ -2857,6 +3074,48 @@ function ensurePrReadyHandoff(input, exec = defaultPrHandoffExec) {
2857
3074
  nextAction: "Ensure `origin` points at GitHub, push the branch, open a PR, and rerun `kynver worker complete`."
2858
3075
  };
2859
3076
  }
3077
+ const repairTarget = input.worker.repairTargetPrUrl?.trim();
3078
+ if (repairTarget) {
3079
+ let committed2 = false;
3080
+ let pushed2 = false;
3081
+ let headCommit2 = snapshot.headCommit ?? resolveHeadCommit(snapshot.worktreePath, exec) ?? void 0;
3082
+ if (snapshot.changedFiles.length > 0) {
3083
+ const pushResult2 = commitAndPushBranch({
3084
+ worktreePath: snapshot.worktreePath,
3085
+ branch: snapshot.branch,
3086
+ commitMessage: `fix(harness): repair target PR ${repairTarget}`,
3087
+ hasDirtyFiles: true,
3088
+ exec
3089
+ });
3090
+ if (!pushResult2.ok) {
3091
+ return {
3092
+ ok: false,
3093
+ reason: `PR-ready handoff blocked: ${pushResult2.detail ?? "git commit/push failed"}`,
3094
+ nextAction: "Commit and push to the target PR branch, then rerun `kynver worker complete`."
3095
+ };
3096
+ }
3097
+ committed2 = pushResult2.committed;
3098
+ pushed2 = pushResult2.pushed;
3099
+ headCommit2 = pushResult2.headCommit ?? headCommit2;
3100
+ } else {
3101
+ const pushOnly = exec.git(snapshot.worktreePath, ["push", "-u", "origin", snapshot.branch]);
3102
+ if (pushOnly.status !== 0 && !/already up to date/i.test(pushOnly.stderr || pushOnly.stdout)) {
3103
+ return {
3104
+ ok: false,
3105
+ reason: `PR-ready handoff blocked: ${pushOnly.stderr || pushOnly.stdout || "git push failed"}`,
3106
+ nextAction: "Push the target branch to origin, then rerun `kynver worker complete`."
3107
+ };
3108
+ }
3109
+ pushed2 = pushOnly.status === 0;
3110
+ }
3111
+ return {
3112
+ ok: true,
3113
+ prUrl: repairTarget,
3114
+ headCommit: headCommit2,
3115
+ committed: committed2,
3116
+ pushed: pushed2
3117
+ };
3118
+ }
2860
3119
  const existing = findOpenPrUrl(snapshot.worktreePath, repo, snapshot.branch, exec);
2861
3120
  if (existing) {
2862
3121
  return {
@@ -2984,13 +3243,13 @@ function dirtyPathsCoveredByDisposableRemoval(changedFiles, removed) {
2984
3243
  if (removed.length === 0) return false;
2985
3244
  const removedSet = new Set(removed.map((p) => normalizeRelativePath(p)));
2986
3245
  return material.every((line) => {
2987
- const path43 = normalizeRelativePath(pathFromGitStatusLine(line));
2988
- return removedSet.has(path43);
3246
+ const path50 = normalizeRelativePath(pathFromGitStatusLine(line));
3247
+ return removedSet.has(path50);
2989
3248
  });
2990
3249
  }
2991
3250
 
2992
3251
  // src/worktree-completion-handoff.ts
2993
- function trimOrNull5(value) {
3252
+ function trimOrNull6(value) {
2994
3253
  if (typeof value !== "string") return null;
2995
3254
  const t = value.trim();
2996
3255
  return t.length ? t : null;
@@ -3017,7 +3276,7 @@ function assessWorktreeCompletionHandoff(input) {
3017
3276
  const materialDirty = materialWorktreeChanges(rawDirty);
3018
3277
  const removed = mergedDisposableRemoved(input);
3019
3278
  const effectivelyClean = materialDirty.length === 0 || dirtyPathsCoveredByDisposableRemoval(rawDirty, removed);
3020
- if (trimOrNull5(input.prUrl)) {
3279
+ if (trimOrNull6(input.prUrl)) {
3021
3280
  if (!effectivelyClean) {
3022
3281
  return {
3023
3282
  allowed: false,
@@ -3028,7 +3287,7 @@ function assessWorktreeCompletionHandoff(input) {
3028
3287
  }
3029
3288
  return { allowed: true, state: "pr_handoff", materialDirtyCount: 0 };
3030
3289
  }
3031
- if (trimOrNull5(input.headCommit)) {
3290
+ if (trimOrNull6(input.headCommit)) {
3032
3291
  if (!effectivelyClean) {
3033
3292
  return {
3034
3293
  allowed: false,
@@ -3039,7 +3298,7 @@ function assessWorktreeCompletionHandoff(input) {
3039
3298
  }
3040
3299
  return { allowed: true, state: "commit_handoff", materialDirtyCount: 0 };
3041
3300
  }
3042
- if (trimOrNull5(input.artifactBundlePath) || trimOrNull5(input.patchPath)) {
3301
+ if (trimOrNull6(input.artifactBundlePath) || trimOrNull6(input.patchPath)) {
3043
3302
  if (!effectivelyClean) {
3044
3303
  return {
3045
3304
  allowed: false,
@@ -3769,6 +4028,19 @@ function spawnCompletionSidecar(opts) {
3769
4028
  }
3770
4029
  }
3771
4030
 
4031
+ // src/repair-target-worktree.ts
4032
+ function addWorktreeForRepairBranch(repo, worktreePath, branch) {
4033
+ git(repo, ["fetch", "origin", branch, "--prune"], { allowFailure: true });
4034
+ const remoteRef = `origin/${branch}`;
4035
+ const added = gitCapture(repo, ["worktree", "add", "-B", branch, worktreePath, remoteRef]);
4036
+ if (added.status === 0) return;
4037
+ const fallback = gitCapture(repo, ["worktree", "add", "-b", branch, worktreePath, "HEAD"]);
4038
+ if (fallback.status !== 0) {
4039
+ const detail = added.stderr || added.stdout || fallback.stderr || fallback.stdout || "git worktree add failed for repair target branch";
4040
+ throw new Error(detail);
4041
+ }
4042
+ }
4043
+
3772
4044
  // src/supervisor.ts
3773
4045
  function spawnWorkerProcess(run, opts) {
3774
4046
  const rawName = typeof opts.name === "string" ? opts.name.trim() : "";
@@ -3779,13 +4051,14 @@ function spawnWorkerProcess(run, opts) {
3779
4051
  if (run.workers?.[name]) throw new Error(`worker already exists in run ${run.id}: ${name}`);
3780
4052
  if (!opts.task) throw new Error(`missing task text for worker ${name}`);
3781
4053
  const routing = opts.routingRule || opts.requestedModel ? {
3782
- provider: opts.provider || "claude",
4054
+ provider: opts.provider || DEFAULT_WORKER_PROVIDER,
3783
4055
  model: opts.model,
3784
4056
  rule: opts.routingRule || "explicit:spawn",
3785
4057
  requestedModel: opts.requestedModel ?? opts.model
3786
4058
  } : resolveWorkerLaunch({
3787
4059
  explicitModel: opts.model,
3788
- explicitProvider: opts.provider
4060
+ explicitProvider: opts.provider,
4061
+ explicitProviderIsOperatorOverride: Boolean(opts.provider?.trim())
3789
4062
  });
3790
4063
  const provider = resolveWorkerProvider(routing.provider);
3791
4064
  let launchModel = routing.model;
@@ -3805,10 +4078,15 @@ function spawnWorkerProcess(run, opts) {
3805
4078
  const workerDir = path14.join(runDirectory(run.id), "workers", name);
3806
4079
  mkdirSync3(workerDir, { recursive: true });
3807
4080
  const worktreePath = path14.join(worktreesDir, run.id, name);
3808
- const branch = opts.branch || `agent/${run.id}/${name}`;
4081
+ const repairBranch = opts.repairTargetBranch?.trim() || void 0;
4082
+ const branch = repairBranch || opts.branch || `agent/${run.id}/${name}`;
3809
4083
  if (existsSync12(worktreePath)) throw new Error(`worktree path already exists: ${worktreePath}`);
3810
4084
  git(run.repo, ["fetch", "origin", "--prune"], { allowFailure: true });
3811
- git(run.repo, ["worktree", "add", "-b", branch, worktreePath, run.baseCommit], { throwError: true });
4085
+ if (repairBranch) {
4086
+ addWorktreeForRepairBranch(run.repo, worktreePath, repairBranch);
4087
+ } else {
4088
+ git(run.repo, ["worktree", "add", "-b", branch, worktreePath, run.baseCommit], { throwError: true });
4089
+ }
3812
4090
  const stdoutPath = path14.join(workerDir, "stdout.jsonl");
3813
4091
  const stderrPath = path14.join(workerDir, "stderr.log");
3814
4092
  const heartbeatPath = path14.join(workerDir, "heartbeat.jsonl");
@@ -3821,7 +4099,9 @@ function spawnWorkerProcess(run, opts) {
3821
4099
  taskId: opts.taskId,
3822
4100
  instructionPolicyMarkdown: opts.instructionPolicyMarkdown,
3823
4101
  personaMarkdown: opts.personaMarkdown,
3824
- model: launchModel
4102
+ model: launchModel,
4103
+ repairTargetPrUrl: opts.repairTargetPrUrl,
4104
+ repairTargetBranch: opts.repairTargetBranch ?? (repairBranch || void 0)
3825
4105
  });
3826
4106
  let started;
3827
4107
  try {
@@ -3874,6 +4154,8 @@ function spawnWorkerProcess(run, opts) {
3874
4154
  ...opts.parentTaskId ? { parentTaskId: String(opts.parentTaskId) } : {},
3875
4155
  ...opts.taskTitle ? { taskTitle: String(opts.taskTitle) } : {},
3876
4156
  ...opts.taskPrUrl ? { taskPrUrl: String(opts.taskPrUrl) } : {},
4157
+ ...opts.repairTargetPrUrl ? { repairTargetPrUrl: String(opts.repairTargetPrUrl) } : {},
4158
+ ...opts.repairTargetBranch ? { repairTargetBranch: String(opts.repairTargetBranch) } : {},
3877
4159
  startedAt: (/* @__PURE__ */ new Date()).toISOString()
3878
4160
  };
3879
4161
  saveWorker(run.id, worker);
@@ -4545,6 +4827,24 @@ function extractPlanOutboxFromTask(task) {
4545
4827
  };
4546
4828
  }
4547
4829
 
4830
+ // src/runner-identity.ts
4831
+ import os3 from "node:os";
4832
+ function trimOrNull7(value) {
4833
+ if (!value?.trim()) return null;
4834
+ return value.trim();
4835
+ }
4836
+ function resolveRunnerPresencePayload(input = {}) {
4837
+ const env = input.env ?? process.env;
4838
+ const runnerId = trimOrNull7(env.KYNVER_RUNTIME_ID) ?? trimOrNull7(env.OPENCLAW_RUNTIME_ID) ?? trimOrNull7(env.HOSTNAME) ?? os3.hostname();
4839
+ return {
4840
+ runnerId,
4841
+ hostname: trimOrNull7(env.HOSTNAME) ?? os3.hostname(),
4842
+ profile: trimOrNull7(env.KYNVER_RUNNER_PROFILE) ?? trimOrNull7(env.OPENCLAW_RUNNER_PROFILE),
4843
+ harnessRepo: trimOrNull7(env.KYNVER_HARNESS_REPO) ?? trimOrNull7(env.KYNVER_DEFAULT_REPO),
4844
+ runId: input.runId ?? null
4845
+ };
4846
+ }
4847
+
4548
4848
  // src/dispatch.ts
4549
4849
  var DEFAULT_DISPATCH_LEASE_MS = 60 * 60 * 1e3;
4550
4850
  function readHarnessWorkerContext(decision) {
@@ -4609,7 +4909,8 @@ async function dispatchRun(args) {
4609
4909
  const secret = await resolveCallbackSecretWithMint(args.secret ? String(args.secret) : void 0, agentOsId, { baseUrl: base });
4610
4910
  const execute = args.execute === true || args.execute === "true";
4611
4911
  const dryRun = !execute;
4612
- const leaseOwner = `kynver-harness:${run.id}`;
4912
+ const runnerPresence = resolveRunnerPresencePayload({ runId: run.id });
4913
+ const leaseOwner = `kynver-harness:${run.id}@runner:${runnerPresence.runnerId}`;
4613
4914
  const runnerDiskGate = args.diskPath ? observeRunnerDiskGate({ diskPath: String(args.diskPath) }) : observeRunnerDiskGate({ diskPath: run.repo });
4614
4915
  const runnerResourceGate = observeRunnerResourceGate({ runId: run.id });
4615
4916
  const requestedStarts = Number(args.maxStarts) > 0 ? Math.floor(Number(args.maxStarts)) : 1;
@@ -4621,11 +4922,18 @@ async function dispatchRun(args) {
4621
4922
  void 0
4622
4923
  );
4623
4924
  if (!worker?.taskId || !isPidAlive(worker.pid)) continue;
4925
+ const ownedPaths = Array.isArray(worker.ownedPaths) ? worker.ownedPaths.filter((p) => typeof p === "string") : [];
4926
+ const writeSetPrefixes = Array.isArray(
4927
+ worker.writeSetPrefixes
4928
+ ) ? (worker.writeSetPrefixes ?? []).filter((p) => typeof p === "string") : [];
4624
4929
  activeHarnessWorkers.push({
4625
4930
  runId: run.id,
4626
4931
  workerName: name,
4627
4932
  taskId: worker.taskId,
4628
- pid: worker.pid
4933
+ pid: worker.pid,
4934
+ ...ownedPaths.length ? { ownedPaths } : {},
4935
+ ...writeSetPrefixes.length ? { writeSetPrefixes } : {},
4936
+ ...worker.allowConcurrentHotspot ? { allowConcurrentHotspot: true } : {}
4629
4937
  });
4630
4938
  }
4631
4939
  const dispatchUrl = `${base}/api/agent-os/by-id/${encodeURIComponent(agentOsId)}/tasks/dispatch-next`;
@@ -4638,6 +4946,7 @@ async function dispatchRun(args) {
4638
4946
  runnerDiskGate,
4639
4947
  runnerResourceGate,
4640
4948
  activeHarnessWorkers,
4949
+ runnerPresence,
4641
4950
  harnessBoardSnapshot: buildRunBoard(run.id),
4642
4951
  ...args.lane ? { lane: String(args.lane) } : {},
4643
4952
  executor: args.executor ? String(args.executor) : "harness",
@@ -4730,10 +5039,19 @@ async function dispatchRun(args) {
4730
5039
  const name = safeSlug(`t-${task.id}-a${task.attempt}`);
4731
5040
  const routing = resolveWorkerLaunch({
4732
5041
  explicitModel: args.model ? String(args.model) : void 0,
5042
+ explicitProvider: args.provider ? String(args.provider) : void 0,
5043
+ explicitProviderIsOperatorOverride: Boolean(args.provider),
4733
5044
  task: enrichTaskForModelRouting(task)
4734
5045
  });
4735
5046
  try {
4736
5047
  const planId = task.planId ? String(task.planId) : void 0;
5048
+ const repairTarget = resolveHarnessRepairTargetFromTask({
5049
+ title: task.title ? String(task.title) : void 0,
5050
+ description: task.description ? String(task.description) : null,
5051
+ executorRef: task.executorRef ? String(task.executorRef) : null,
5052
+ prUrl: task.prUrl ? String(task.prUrl) : null,
5053
+ branch: task.branch ? String(task.branch) : null
5054
+ });
4737
5055
  const worker = spawnWorkerProcess(run, {
4738
5056
  name,
4739
5057
  task: buildDispatchTaskText(task, agentOsId),
@@ -4745,10 +5063,13 @@ async function dispatchRun(args) {
4745
5063
  agentOsId,
4746
5064
  taskId: String(task.id),
4747
5065
  planId,
5066
+ branch: repairTarget?.targetPrBranch ?? void 0,
4748
5067
  executorRef: task.executorRef ? String(task.executorRef) : void 0,
4749
5068
  parentTaskId: task.parentTaskId ? String(task.parentTaskId) : void 0,
4750
5069
  taskTitle: task.title ? String(task.title) : void 0,
4751
- taskPrUrl: task.prUrl ? String(task.prUrl) : void 0,
5070
+ taskPrUrl: repairTarget?.targetPrUrl ?? (task.prUrl ? String(task.prUrl) : void 0),
5071
+ repairTargetPrUrl: repairTarget?.targetPrUrl,
5072
+ repairTargetBranch: repairTarget?.targetPrBranch ?? void 0,
4752
5073
  instructionPolicyMarkdown: harnessContext?.instructionPolicyMarkdown ?? null,
4753
5074
  instructionPolicyFingerprint: harnessContext?.instructionPolicyFingerprint ?? null,
4754
5075
  instructionPolicyEvidence: harnessContext?.instructionPolicyEvidence ?? null,
@@ -4883,106 +5204,384 @@ async function sweepRun(args) {
4883
5204
  }
4884
5205
 
4885
5206
  // src/worktree.ts
4886
- import { existsSync as existsSync15, mkdirSync as mkdirSync5 } from "node:fs";
4887
- import path22 from "node:path";
5207
+ import { existsSync as existsSync16, mkdirSync as mkdirSync5 } from "node:fs";
5208
+ import path25 from "node:path";
4888
5209
 
4889
- // src/default-repo.ts
4890
- import path20 from "node:path";
4891
- function expandConfiguredRepo(value) {
4892
- return path20.resolve(resolveUserPath(value.trim()));
4893
- }
4894
- function fromConfigured(value, source, persistedInConfig) {
4895
- const trimmed = value?.trim();
4896
- if (!trimmed) return null;
4897
- return {
4898
- repo: expandConfiguredRepo(trimmed),
4899
- source,
4900
- persistedInConfig
4901
- };
4902
- }
4903
- function resolveDefaultRepo(opts = {}) {
4904
- const env = opts.env ?? process.env;
4905
- const config = opts.config ?? loadUserConfig();
4906
- const fromConfig = fromConfigured(config.defaultRepo, "config", true);
4907
- if (fromConfig) return fromConfig;
4908
- const fromDefaultEnv = fromConfigured(env.KYNVER_DEFAULT_REPO, "env_default_repo", false);
4909
- if (fromDefaultEnv) return fromDefaultEnv;
4910
- const fromHarnessEnv = fromConfigured(env.KYNVER_HARNESS_REPO, "env_harness_repo", false);
4911
- if (fromHarnessEnv) return fromHarnessEnv;
4912
- const discovered = discoverDefaultRepo({
4913
- cwd: opts.cwd,
4914
- runtimeModuleUrl: opts.runtimeModuleUrl
4915
- });
4916
- if (!discovered) return null;
4917
- return {
4918
- repo: discovered.repo,
4919
- source: discovered.source,
4920
- persistedInConfig: false
4921
- };
4922
- }
4923
- function formatResolvedDefaultRepo(resolved) {
4924
- return {
4925
- defaultRepo: displayUserPath(resolved.repo),
4926
- source: resolved.source,
4927
- persistedInConfig: resolved.persistedInConfig
4928
- };
4929
- }
5210
+ // src/run-list.ts
5211
+ import { existsSync as existsSync15, readFileSync as readFileSync9 } from "node:fs";
5212
+ import path22 from "node:path";
4930
5213
 
4931
- // src/validate.ts
5214
+ // src/stale-reconcile.ts
4932
5215
  import path21 from "node:path";
4933
- var RUN_ID_RE = /^[a-z0-9][a-z0-9._-]{0,127}$/i;
4934
- function validateRunId(runId) {
4935
- const trimmed = runId.trim();
4936
- if (!RUN_ID_RE.test(trimmed)) throw new Error(`invalid run id: ${runId}`);
4937
- return trimmed;
5216
+
5217
+ // src/finalize.ts
5218
+ import path20 from "node:path";
5219
+ var ACTIVE_RUN_STATUSES = /* @__PURE__ */ new Set([
5220
+ "running",
5221
+ "dispatching",
5222
+ "pending",
5223
+ "queued",
5224
+ "needs_attention"
5225
+ ]);
5226
+ var TERMINAL_RUN_STATUSES = /* @__PURE__ */ new Set(["completed", "failed", "cancelled", "done"]);
5227
+ function deriveTerminalRunStatus(run) {
5228
+ const names = Object.keys(run.workers || {});
5229
+ if (names.length === 0) return "failed";
5230
+ let anyAlive = false;
5231
+ let anyResult = false;
5232
+ let anyCompletionBlocked = false;
5233
+ let anyLandingBlocked = false;
5234
+ for (const name of names) {
5235
+ const worker = readJson(
5236
+ path20.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
5237
+ void 0
5238
+ );
5239
+ if (!worker) continue;
5240
+ const status = computeWorkerStatus(worker, {
5241
+ base: run.base,
5242
+ baseCommit: run.baseCommit
5243
+ });
5244
+ if (status.alive && !status.finalResult) {
5245
+ anyAlive = true;
5246
+ break;
5247
+ }
5248
+ if (typeof worker.completionBlocker === "string" && worker.completionBlocker) {
5249
+ anyCompletionBlocked = true;
5250
+ }
5251
+ if (isLandingBlockedWorkerStatus(status)) {
5252
+ anyLandingBlocked = true;
5253
+ }
5254
+ if (status.finalResult && status.attention.state === "done") anyResult = true;
5255
+ }
5256
+ if (anyAlive) return null;
5257
+ if (anyCompletionBlocked) return null;
5258
+ if (anyLandingBlocked) return null;
5259
+ return anyResult ? "completed" : "failed";
4938
5260
  }
4939
- function validateRepo(repo) {
4940
- const resolved = path21.resolve(repo);
4941
- if (resolved.includes("..")) throw new Error("repo path must not contain .. segments");
4942
- return resolved;
5261
+ function finalizeStaleRuns() {
5262
+ const finalized = [];
5263
+ for (const run of listRunRecords()) {
5264
+ if (!ACTIVE_RUN_STATUSES.has(run.status)) continue;
5265
+ const next = deriveTerminalRunStatus(run);
5266
+ if (!next || next === run.status) continue;
5267
+ const from = run.status;
5268
+ run.status = next;
5269
+ saveRun(run);
5270
+ finalized.push({ runId: run.id, from, to: next });
5271
+ }
5272
+ return finalized;
4943
5273
  }
4944
5274
 
4945
- // src/worktree.ts
4946
- function resolveCreateRunRepo(args) {
4947
- const explicit = typeof args.repo === "string" ? args.repo.trim() : "";
4948
- if (explicit) return explicit;
4949
- const resolved = resolveDefaultRepo();
4950
- if (resolved) return resolved.repo;
4951
- required("", "--repo (or set defaultRepo via `kynver setup` / KYNVER_DEFAULT_REPO)");
4952
- return "";
5275
+ // src/stale-reconcile.ts
5276
+ var STALE_RECONCILE_HEARTBEAT_MS = 15 * 60 * 1e3;
5277
+ function staleReconcileDisabled() {
5278
+ return process.env.KYNVER_NO_STALE_CLEANUP === "1";
4953
5279
  }
4954
- function createRun(args) {
4955
- const repo = validateRepo(resolveCreateRunRepo(args));
4956
- ensureGitRepo(repo);
4957
- const id = args.id ? validateRunId(String(args.id)) : timestampSlug(String(args.name || "run"));
4958
- const dir = runDirectory(id);
4959
- if (existsSync15(dir)) failExists(`run already exists: ${id}`);
4960
- mkdirSync5(dir, { recursive: true });
4961
- const base = String(args.base || "origin/main");
4962
- const baseCommit = git(repo, ["rev-parse", base]).trim();
4963
- const run = {
4964
- id,
4965
- name: String(args.name || id),
4966
- repo,
4967
- base,
4968
- baseCommit,
5280
+ function reconcileStaleWorkers() {
5281
+ if (staleReconcileDisabled()) {
5282
+ return { workers: [], finalizedRuns: finalizeStaleRuns() };
5283
+ }
5284
+ const outcomes = [];
5285
+ const now = Date.now();
5286
+ for (const run of listRunRecords()) {
5287
+ for (const name of Object.keys(run.workers || {})) {
5288
+ const workerPath = path21.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json");
5289
+ const worker = readJson(workerPath, void 0);
5290
+ if (!worker || worker.status !== "running") {
5291
+ outcomes.push({
5292
+ runId: run.id,
5293
+ worker: name,
5294
+ action: "skipped",
5295
+ reason: worker ? `worker status is ${worker.status}` : "worker.json missing"
5296
+ });
5297
+ continue;
5298
+ }
5299
+ const status = computeWorkerStatus(worker, { base: run.base, baseCommit: run.baseCommit });
5300
+ if (status.finalResult) {
5301
+ if (worker.status === "running") {
5302
+ const nextStatus = status.attention.state === "blocked" ? "blocked" : status.attention.state === "done" || status.status === "done" ? "done" : "exited";
5303
+ worker.status = nextStatus;
5304
+ worker.reconciledAt = (/* @__PURE__ */ new Date()).toISOString();
5305
+ worker.reconcileReason = "synced finished worker record after terminal stdout/heartbeat";
5306
+ saveWorker(run.id, worker);
5307
+ outcomes.push({
5308
+ runId: run.id,
5309
+ worker: name,
5310
+ action: "marked_exited",
5311
+ reason: worker.reconcileReason
5312
+ });
5313
+ } else {
5314
+ outcomes.push({ runId: run.id, worker: name, action: "skipped", reason: "final result present" });
5315
+ }
5316
+ continue;
5317
+ }
5318
+ if (!status.alive) {
5319
+ const nextStatus = status.attention.state === "blocked" ? "blocked" : status.status === "done" ? "done" : "exited";
5320
+ worker.status = nextStatus;
5321
+ worker.reconciledAt = (/* @__PURE__ */ new Date()).toISOString();
5322
+ worker.reconcileReason = status.attention.reason;
5323
+ saveWorker(run.id, worker);
5324
+ outcomes.push({
5325
+ runId: run.id,
5326
+ worker: name,
5327
+ action: "marked_exited",
5328
+ reason: status.attention.reason
5329
+ });
5330
+ continue;
5331
+ }
5332
+ if (status.attention.state === "stale" && worker.pid && isPidAlive(worker.pid)) {
5333
+ const hbMs = status.lastHeartbeatAt ? Date.parse(status.lastHeartbeatAt) : NaN;
5334
+ const actMs = status.lastActivityAt ? Date.parse(status.lastActivityAt) : NaN;
5335
+ const hbStale = !Number.isFinite(hbMs) || now - hbMs > STALE_RECONCILE_HEARTBEAT_MS;
5336
+ const actStale = Number.isFinite(actMs) && now - actMs > STALE_MS;
5337
+ if (hbStale && actStale) {
5338
+ killWorkerProcess(worker.pid, "SIGTERM");
5339
+ worker.status = "exited";
5340
+ worker.reconciledAt = (/* @__PURE__ */ new Date()).toISOString();
5341
+ worker.reconcileReason = `reconciled stale worker: ${status.attention.reason}`;
5342
+ saveWorker(run.id, worker);
5343
+ outcomes.push({
5344
+ runId: run.id,
5345
+ worker: name,
5346
+ action: "killed_stale",
5347
+ reason: status.attention.reason
5348
+ });
5349
+ continue;
5350
+ }
5351
+ }
5352
+ outcomes.push({
5353
+ runId: run.id,
5354
+ worker: name,
5355
+ action: "skipped",
5356
+ reason: status.attention.reason
5357
+ });
5358
+ }
5359
+ }
5360
+ return { workers: outcomes, finalizedRuns: finalizeStaleRuns() };
5361
+ }
5362
+ function reconcileRunsCli() {
5363
+ const result = reconcileStaleWorkers();
5364
+ const markedExited = result.workers.filter((w) => w.action === "marked_exited").length;
5365
+ const killedStale = result.workers.filter((w) => w.action === "killed_stale").length;
5366
+ const skipped = result.workers.filter((w) => w.action === "skipped").length;
5367
+ console.log(
5368
+ JSON.stringify(
5369
+ {
5370
+ ok: true,
5371
+ workers: { markedExited, killedStale, skipped, total: result.workers.length },
5372
+ finalizedRuns: result.finalizedRuns.length,
5373
+ details: { workers: result.workers, finalizedRuns: result.finalizedRuns }
5374
+ },
5375
+ null,
5376
+ 2
5377
+ )
5378
+ );
5379
+ }
5380
+
5381
+ // src/run-list.ts
5382
+ function heartbeatByteLength(heartbeatPath) {
5383
+ if (!heartbeatPath || !existsSync15(heartbeatPath)) return 0;
5384
+ try {
5385
+ return readFileSync9(heartbeatPath, "utf8").trim().length;
5386
+ } catch {
5387
+ return 0;
5388
+ }
5389
+ }
5390
+ function workerEvidence(run, workerName) {
5391
+ const workerPath = path22.join(runDirectory(run.id), "workers", safeSlug(workerName), "worker.json");
5392
+ const worker = readJson(workerPath, void 0);
5393
+ if (!worker) {
5394
+ return {
5395
+ worker: workerName,
5396
+ workerStatus: "missing",
5397
+ attention: "needs_attention",
5398
+ attentionReason: "worker.json missing",
5399
+ missingHeartbeat: true,
5400
+ missingFinalResult: true,
5401
+ landingBlocked: false,
5402
+ completionBlocked: false
5403
+ };
5404
+ }
5405
+ const status = computeWorkerStatus(worker, { base: run.base, baseCommit: run.baseCommit });
5406
+ const missingHeartbeat = heartbeatByteLength(worker.heartbeatPath) === 0;
5407
+ const missingFinalResult = !status.finalResult && !status.alive;
5408
+ const completionBlocked = typeof worker.completionBlocker === "string" && worker.completionBlocker.length > 0;
5409
+ return {
5410
+ worker: workerName,
5411
+ workerStatus: worker.status,
5412
+ attention: status.attention.state,
5413
+ attentionReason: status.attention.reason,
5414
+ missingHeartbeat,
5415
+ missingFinalResult,
5416
+ landingBlocked: isLandingBlockedWorkerStatus(status),
5417
+ completionBlocked
5418
+ };
5419
+ }
5420
+ function deriveFinalizeBlockedReason(input) {
5421
+ if (input.openWorkerCount > 0) return "active_workers";
5422
+ if (input.workers.some((w) => w.completionBlocked)) return "completion_blocked";
5423
+ if (input.workers.some((w) => w.landingBlocked)) return "landing_blocked";
5424
+ return null;
5425
+ }
5426
+ function aggregateRunAttention(workers) {
5427
+ const rank = {
5428
+ blocked: 5,
5429
+ needs_attention: 4,
5430
+ stale: 3,
5431
+ done: 2,
5432
+ ok: 1
5433
+ };
5434
+ let best = "ok";
5435
+ let reason;
5436
+ for (const w of workers) {
5437
+ const state = w.attention;
5438
+ if ((rank[state] ?? 0) >= (rank[best] ?? 0)) {
5439
+ best = state;
5440
+ reason = w.attentionReason;
5441
+ }
5442
+ }
5443
+ return { attention: best, attentionReason: reason };
5444
+ }
5445
+ function countOpenWorkers(run) {
5446
+ let open = 0;
5447
+ for (const name of Object.keys(run.workers || {})) {
5448
+ const workerPath = path22.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json");
5449
+ const worker = readJson(workerPath, void 0);
5450
+ if (!worker) continue;
5451
+ const status = computeWorkerStatus(worker, { base: run.base, baseCommit: run.baseCommit });
5452
+ if (status.alive && !status.finalResult) open += 1;
5453
+ }
5454
+ return open;
5455
+ }
5456
+ function buildRunListRows() {
5457
+ reconcileStaleWorkers();
5458
+ return listRunRecords().map((run) => {
5459
+ const workerNames = Object.keys(run.workers || {});
5460
+ const workers = workerNames.map((name) => workerEvidence(run, name));
5461
+ const missingHeartbeatWorkers = workers.filter((w) => w.missingHeartbeat).map((w) => w.worker);
5462
+ const missingFinalResultWorkers = workers.filter((w) => w.missingFinalResult).map((w) => w.worker);
5463
+ const landingBlockedWorkers = workers.filter((w) => w.landingBlocked).map((w) => w.worker);
5464
+ const completionBlockedWorkers = workers.filter((w) => w.completionBlocked).map((w) => w.worker);
5465
+ const { attention, attentionReason } = aggregateRunAttention(workers);
5466
+ const openWorkerCount = countOpenWorkers(run);
5467
+ const effectiveStatus = deriveRunStatus(
5468
+ run.status,
5469
+ workers.map((w) => ({ attention: w.attention, status: w.workerStatus }))
5470
+ );
5471
+ return {
5472
+ id: run.id,
5473
+ name: run.name,
5474
+ status: run.status,
5475
+ effectiveStatus,
5476
+ repo: run.repo,
5477
+ createdAt: run.createdAt,
5478
+ openWorkerCount,
5479
+ attention,
5480
+ attentionReason,
5481
+ finalizeBlockedReason: deriveFinalizeBlockedReason({ run, workers, openWorkerCount }),
5482
+ evidence: {
5483
+ missingHeartbeatWorkers,
5484
+ missingFinalResultWorkers,
5485
+ landingBlockedWorkers,
5486
+ completionBlockedWorkers,
5487
+ workers
5488
+ }
5489
+ };
5490
+ });
5491
+ }
5492
+ function listRunsCli() {
5493
+ console.log(JSON.stringify(buildRunListRows(), null, 2));
5494
+ }
5495
+
5496
+ // src/default-repo.ts
5497
+ import path23 from "node:path";
5498
+ function expandConfiguredRepo(value) {
5499
+ return path23.resolve(resolveUserPath(value.trim()));
5500
+ }
5501
+ function fromConfigured(value, source, persistedInConfig) {
5502
+ const trimmed = value?.trim();
5503
+ if (!trimmed) return null;
5504
+ return {
5505
+ repo: expandConfiguredRepo(trimmed),
5506
+ source,
5507
+ persistedInConfig
5508
+ };
5509
+ }
5510
+ function resolveDefaultRepo(opts = {}) {
5511
+ const env = opts.env ?? process.env;
5512
+ const config = opts.config ?? loadUserConfig();
5513
+ const fromConfig = fromConfigured(config.defaultRepo, "config", true);
5514
+ if (fromConfig) return fromConfig;
5515
+ const fromDefaultEnv = fromConfigured(env.KYNVER_DEFAULT_REPO, "env_default_repo", false);
5516
+ if (fromDefaultEnv) return fromDefaultEnv;
5517
+ const fromHarnessEnv = fromConfigured(env.KYNVER_HARNESS_REPO, "env_harness_repo", false);
5518
+ if (fromHarnessEnv) return fromHarnessEnv;
5519
+ const discovered = discoverDefaultRepo({
5520
+ cwd: opts.cwd,
5521
+ runtimeModuleUrl: opts.runtimeModuleUrl
5522
+ });
5523
+ if (!discovered) return null;
5524
+ return {
5525
+ repo: discovered.repo,
5526
+ source: discovered.source,
5527
+ persistedInConfig: false
5528
+ };
5529
+ }
5530
+ function formatResolvedDefaultRepo(resolved) {
5531
+ return {
5532
+ defaultRepo: displayUserPath(resolved.repo),
5533
+ source: resolved.source,
5534
+ persistedInConfig: resolved.persistedInConfig
5535
+ };
5536
+ }
5537
+
5538
+ // src/validate.ts
5539
+ import path24 from "node:path";
5540
+ var RUN_ID_RE = /^[a-z0-9][a-z0-9._-]{0,127}$/i;
5541
+ function validateRunId(runId) {
5542
+ const trimmed = runId.trim();
5543
+ if (!RUN_ID_RE.test(trimmed)) throw new Error(`invalid run id: ${runId}`);
5544
+ return trimmed;
5545
+ }
5546
+ function validateRepo(repo) {
5547
+ const resolved = path24.resolve(repo);
5548
+ if (resolved.includes("..")) throw new Error("repo path must not contain .. segments");
5549
+ return resolved;
5550
+ }
5551
+
5552
+ // src/worktree.ts
5553
+ function resolveCreateRunRepo(args) {
5554
+ const explicit = typeof args.repo === "string" ? args.repo.trim() : "";
5555
+ if (explicit) return explicit;
5556
+ const resolved = resolveDefaultRepo();
5557
+ if (resolved) return resolved.repo;
5558
+ required("", "--repo (or set defaultRepo via `kynver setup` / KYNVER_DEFAULT_REPO)");
5559
+ return "";
5560
+ }
5561
+ function createRun(args) {
5562
+ const repo = validateRepo(resolveCreateRunRepo(args));
5563
+ ensureGitRepo(repo);
5564
+ const id = args.id ? validateRunId(String(args.id)) : timestampSlug(String(args.name || "run"));
5565
+ const dir = runDirectory(id);
5566
+ if (existsSync16(dir)) failExists(`run already exists: ${id}`);
5567
+ mkdirSync5(dir, { recursive: true });
5568
+ const base = String(args.base || "origin/main");
5569
+ const baseCommit = git(repo, ["rev-parse", base]).trim();
5570
+ const run = {
5571
+ id,
5572
+ name: String(args.name || id),
5573
+ repo,
5574
+ base,
5575
+ baseCommit,
4969
5576
  status: "created",
4970
5577
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
4971
5578
  workers: {}
4972
5579
  };
4973
- writeJson(path22.join(dir, "run.json"), run);
5580
+ writeJson(path25.join(dir, "run.json"), run);
4974
5581
  console.log(JSON.stringify({ runId: id, runDir: dir, repo, base, baseCommit }, null, 2));
4975
5582
  }
4976
5583
  function listRuns() {
4977
- const { runsDir } = getPaths();
4978
- const rows = listRunIds(runsDir).map((id) => readJson(path22.join(runDirectory(id), "run.json"), void 0)).filter(Boolean).map((run) => ({
4979
- id: run.id,
4980
- name: run.name,
4981
- status: run.status,
4982
- repo: run.repo,
4983
- createdAt: run.createdAt
4984
- }));
4985
- console.log(JSON.stringify(rows, null, 2));
5584
+ listRunsCli();
4986
5585
  }
4987
5586
  function failExists(message) {
4988
5587
  console.error(message);
@@ -4990,8 +5589,8 @@ function failExists(message) {
4990
5589
  }
4991
5590
 
4992
5591
  // src/discard-disposable.ts
4993
- import { existsSync as existsSync16, rmSync } from "node:fs";
4994
- import path23 from "node:path";
5592
+ import { existsSync as existsSync17, rmSync } from "node:fs";
5593
+ import path26 from "node:path";
4995
5594
  function normalizeRelativePath2(value) {
4996
5595
  const normalized = value.replace(/\\/g, "/").replace(/^\.\//, "").trim();
4997
5596
  if (!normalized || normalized.startsWith("/") || normalized.includes("..")) {
@@ -5012,15 +5611,15 @@ function discardDisposableArtifacts(args) {
5012
5611
  if (paths.length === 0) {
5013
5612
  return { ok: false, removed: [], reason: "requires at least one --path" };
5014
5613
  }
5015
- const worktreeRoot = path23.resolve(worker.worktreePath);
5614
+ const worktreeRoot = path26.resolve(worker.worktreePath);
5016
5615
  const removed = [];
5017
5616
  for (const raw of paths) {
5018
5617
  const rel = normalizeRelativePath2(raw);
5019
- const abs = path23.resolve(worktreeRoot, rel);
5020
- if (!abs.startsWith(worktreeRoot + path23.sep) && abs !== worktreeRoot) {
5618
+ const abs = path26.resolve(worktreeRoot, rel);
5619
+ if (!abs.startsWith(worktreeRoot + path26.sep) && abs !== worktreeRoot) {
5021
5620
  return { ok: false, removed, reason: `path escapes worktree: ${raw}` };
5022
5621
  }
5023
- if (!existsSync16(abs)) {
5622
+ if (!existsSync17(abs)) {
5024
5623
  return { ok: false, removed, reason: `path not found: ${raw}` };
5025
5624
  }
5026
5625
  rmSync(abs, { recursive: true, force: true });
@@ -5043,7 +5642,7 @@ function discardDisposableCli(args) {
5043
5642
  }
5044
5643
 
5045
5644
  // src/pipeline-tick.ts
5046
- import path35 from "node:path";
5645
+ import path41 from "node:path";
5047
5646
 
5048
5647
  // src/pipeline-dispatch.ts
5049
5648
  var RESERVED_REVIEW_STARTS = 1;
@@ -5123,175 +5722,47 @@ function resolvePipelineMaxStarts(resourceGate, operatorTick) {
5123
5722
  };
5124
5723
  }
5125
5724
 
5126
- // src/stale-reconcile.ts
5127
- import path25 from "node:path";
5725
+ // src/box-resource-snapshot.ts
5726
+ import os5 from "node:os";
5128
5727
 
5129
- // src/finalize.ts
5130
- import path24 from "node:path";
5131
- var ACTIVE_RUN_STATUSES = /* @__PURE__ */ new Set([
5132
- "running",
5133
- "dispatching",
5134
- "pending",
5135
- "queued",
5136
- "needs_attention"
5137
- ]);
5138
- var TERMINAL_RUN_STATUSES = /* @__PURE__ */ new Set(["completed", "failed", "cancelled", "done"]);
5139
- function deriveTerminalRunStatus(run) {
5140
- const names = Object.keys(run.workers || {});
5141
- if (names.length === 0) return "failed";
5142
- let anyAlive = false;
5143
- let anyResult = false;
5144
- let anyCompletionBlocked = false;
5145
- let anyLandingBlocked = false;
5146
- for (const name of names) {
5147
- const worker = readJson(
5148
- path24.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
5149
- void 0
5150
- );
5151
- if (!worker) continue;
5152
- const status = computeWorkerStatus(worker, {
5153
- base: run.base,
5154
- baseCommit: run.baseCommit
5155
- });
5156
- if (status.alive && !status.finalResult) {
5157
- anyAlive = true;
5158
- break;
5159
- }
5160
- if (typeof worker.completionBlocker === "string" && worker.completionBlocker) {
5161
- anyCompletionBlocked = true;
5162
- }
5163
- if (isLandingBlockedWorkerStatus(status)) {
5164
- anyLandingBlocked = true;
5165
- }
5166
- if (status.finalResult && status.attention.state === "done") anyResult = true;
5167
- }
5168
- if (anyAlive) return null;
5169
- if (anyCompletionBlocked) return null;
5170
- if (anyLandingBlocked) return null;
5171
- return anyResult ? "completed" : "failed";
5728
+ // src/box-resource-snapshot-shared.ts
5729
+ import os4 from "node:os";
5730
+ function resolveBoxKindFromEnv(env = process.env) {
5731
+ const kind = (env.KYNVER_BOX_KIND ?? env.KYNVER_AGENT_OS_SLUG ?? "forge").trim().toLowerCase();
5732
+ if (kind === "ghost" || kind === "forge") return kind;
5733
+ return kind || "forge";
5172
5734
  }
5173
- function finalizeStaleRuns() {
5174
- const finalized = [];
5175
- for (const run of listRunRecords()) {
5176
- if (!ACTIVE_RUN_STATUSES.has(run.status)) continue;
5177
- const next = deriveTerminalRunStatus(run);
5178
- if (!next || next === run.status) continue;
5179
- const from = run.status;
5180
- run.status = next;
5181
- saveRun(run);
5182
- finalized.push({ runId: run.id, from, to: next });
5183
- }
5184
- return finalized;
5735
+ function defaultBoxId(boxKind, hostLabel) {
5736
+ const host = (hostLabel ?? os4.hostname()).trim().toLowerCase().replace(/\s+/g, "-") || "unknown-host";
5737
+ return `${boxKind}:${host}`;
5185
5738
  }
5186
5739
 
5187
- // src/stale-reconcile.ts
5188
- var STALE_RECONCILE_HEARTBEAT_MS = 15 * 60 * 1e3;
5189
- function staleReconcileDisabled() {
5190
- return process.env.KYNVER_NO_STALE_CLEANUP === "1";
5191
- }
5192
- function reconcileStaleWorkers() {
5193
- if (staleReconcileDisabled()) {
5194
- return { workers: [], finalizedRuns: finalizeStaleRuns() };
5195
- }
5196
- const outcomes = [];
5197
- const now = Date.now();
5198
- for (const run of listRunRecords()) {
5199
- for (const name of Object.keys(run.workers || {})) {
5200
- const workerPath = path25.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json");
5201
- const worker = readJson(workerPath, void 0);
5202
- if (!worker || worker.status !== "running") {
5203
- outcomes.push({
5204
- runId: run.id,
5205
- worker: name,
5206
- action: "skipped",
5207
- reason: worker ? `worker status is ${worker.status}` : "worker.json missing"
5208
- });
5209
- continue;
5210
- }
5211
- const status = computeWorkerStatus(worker, { base: run.base, baseCommit: run.baseCommit });
5212
- if (status.finalResult) {
5213
- if (worker.status === "running") {
5214
- const nextStatus = status.attention.state === "blocked" ? "blocked" : status.attention.state === "done" || status.status === "done" ? "done" : "exited";
5215
- worker.status = nextStatus;
5216
- worker.reconciledAt = (/* @__PURE__ */ new Date()).toISOString();
5217
- worker.reconcileReason = "synced finished worker record after terminal stdout/heartbeat";
5218
- saveWorker(run.id, worker);
5219
- outcomes.push({
5220
- runId: run.id,
5221
- worker: name,
5222
- action: "marked_exited",
5223
- reason: worker.reconcileReason
5224
- });
5225
- } else {
5226
- outcomes.push({ runId: run.id, worker: name, action: "skipped", reason: "final result present" });
5227
- }
5228
- continue;
5229
- }
5230
- if (!status.alive) {
5231
- const nextStatus = status.attention.state === "blocked" ? "blocked" : status.status === "done" ? "done" : "exited";
5232
- worker.status = nextStatus;
5233
- worker.reconciledAt = (/* @__PURE__ */ new Date()).toISOString();
5234
- worker.reconcileReason = status.attention.reason;
5235
- saveWorker(run.id, worker);
5236
- outcomes.push({
5237
- runId: run.id,
5238
- worker: name,
5239
- action: "marked_exited",
5240
- reason: status.attention.reason
5241
- });
5242
- continue;
5243
- }
5244
- if (status.attention.state === "stale" && worker.pid && isPidAlive(worker.pid)) {
5245
- const hbMs = status.lastHeartbeatAt ? Date.parse(status.lastHeartbeatAt) : NaN;
5246
- const actMs = status.lastActivityAt ? Date.parse(status.lastActivityAt) : NaN;
5247
- const hbStale = !Number.isFinite(hbMs) || now - hbMs > STALE_RECONCILE_HEARTBEAT_MS;
5248
- const actStale = Number.isFinite(actMs) && now - actMs > STALE_MS;
5249
- if (hbStale && actStale) {
5250
- killWorkerProcess(worker.pid, "SIGTERM");
5251
- worker.status = "exited";
5252
- worker.reconciledAt = (/* @__PURE__ */ new Date()).toISOString();
5253
- worker.reconcileReason = `reconciled stale worker: ${status.attention.reason}`;
5254
- saveWorker(run.id, worker);
5255
- outcomes.push({
5256
- runId: run.id,
5257
- worker: name,
5258
- action: "killed_stale",
5259
- reason: status.attention.reason
5260
- });
5261
- continue;
5262
- }
5263
- }
5264
- outcomes.push({
5265
- runId: run.id,
5266
- worker: name,
5267
- action: "skipped",
5268
- reason: status.attention.reason
5269
- });
5270
- }
5271
- }
5272
- return { workers: outcomes, finalizedRuns: finalizeStaleRuns() };
5273
- }
5274
- function reconcileRunsCli() {
5275
- const result = reconcileStaleWorkers();
5276
- const markedExited = result.workers.filter((w) => w.action === "marked_exited").length;
5277
- const killedStale = result.workers.filter((w) => w.action === "killed_stale").length;
5278
- const skipped = result.workers.filter((w) => w.action === "skipped").length;
5279
- console.log(
5280
- JSON.stringify(
5281
- {
5282
- ok: true,
5283
- workers: { markedExited, killedStale, skipped, total: result.workers.length },
5284
- finalizedRuns: result.finalizedRuns.length,
5285
- details: { workers: result.workers, finalizedRuns: result.finalizedRuns }
5286
- },
5287
- null,
5288
- 2
5289
- )
5290
- );
5740
+ // src/box-resource-snapshot.ts
5741
+ function buildBoxResourceSnapshotFromGate(gate, input = {}) {
5742
+ const boxKind = (input.boxKind ?? resolveBoxKindFromEnv()).trim().toLowerCase() || "forge";
5743
+ const hostLabel = input.hostLabel ?? os5.hostname();
5744
+ const boxId = input.boxId ?? defaultBoxId(boxKind, hostLabel);
5745
+ return {
5746
+ boxId,
5747
+ boxKind,
5748
+ displayName: input.displayName ?? null,
5749
+ hostLabel,
5750
+ observedAt: input.observedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
5751
+ totalMemBytes: gate.totalMemBytes,
5752
+ freeMemBytes: gate.freeMemBytes,
5753
+ activeWorkers: gate.activeWorkers,
5754
+ maxConcurrentWorkers: gate.maxConcurrentWorkers,
5755
+ autoCap: gate.autoCap,
5756
+ slotsAvailable: gate.slotsAvailable,
5757
+ harnessRunId: input.harnessRunId ?? null,
5758
+ queuedTasks: input.queuedTasks ?? null,
5759
+ reason: gate.reason,
5760
+ ...input.prEvidence?.length ? { prEvidence: input.prEvidence } : {}
5761
+ };
5291
5762
  }
5292
5763
 
5293
5764
  // src/plan-progress-daemon-sync.ts
5294
- import path26 from "node:path";
5765
+ import path27 from "node:path";
5295
5766
 
5296
5767
  // src/plan-progress-sync.ts
5297
5768
  async function syncPlanProgress(args) {
@@ -5315,7 +5786,7 @@ async function syncActiveWorkerPlanProgress(runId, args) {
5315
5786
  const outcomes = [];
5316
5787
  for (const name of Object.keys(run.workers || {})) {
5317
5788
  const worker = readJson(
5318
- path26.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
5789
+ path27.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
5319
5790
  void 0
5320
5791
  );
5321
5792
  if (!worker?.dispatched || !worker.taskId) continue;
@@ -5364,7 +5835,10 @@ async function fetchWorkspaceRuntimePreferences(agentOsId, args) {
5364
5835
  }
5365
5836
 
5366
5837
  // src/cleanup.ts
5367
- import path33 from "node:path";
5838
+ import path39 from "node:path";
5839
+
5840
+ // src/cleanup-guards.ts
5841
+ import path28 from "node:path";
5368
5842
 
5369
5843
  // src/cleanup-run-liveness.ts
5370
5844
  function isWorkerProcessLive(indexed) {
@@ -5385,12 +5859,30 @@ function runBlocksWorktreeRemoval(indexed) {
5385
5859
  return deriveTerminalRunStatus(indexed.run) === null;
5386
5860
  }
5387
5861
 
5862
+ // src/cleanup-build-cache-paths.ts
5863
+ var HARNESS_BUILD_CACHE_RELATIVE_PATHS = [
5864
+ ".next",
5865
+ ".turbo",
5866
+ "dist",
5867
+ "build",
5868
+ ".cache",
5869
+ "node_modules/.cache"
5870
+ ];
5871
+ function isGeneratedHarnessPath(pathPart) {
5872
+ const normalized = pathPart.replace(/\\/g, "/").replace(/\/+$/, "");
5873
+ if (normalized === "node_modules" || normalized.startsWith("node_modules/")) return true;
5874
+ for (const rel of HARNESS_BUILD_CACHE_RELATIVE_PATHS) {
5875
+ if (normalized === rel || normalized.startsWith(`${rel}/`)) return true;
5876
+ }
5877
+ return false;
5878
+ }
5879
+
5388
5880
  // src/cleanup-guards-helpers.ts
5389
5881
  function materialWorktreeChanges2(changedFiles) {
5390
5882
  return changedFiles.filter((line) => {
5391
5883
  const trimmed = line.trim();
5392
5884
  const pathPart = trimmed.startsWith("??") ? trimmed.slice(2).trim() : trimmed.length > 3 ? trimmed.slice(3).trim() : trimmed;
5393
- return pathPart !== "node_modules" && !pathPart.startsWith("node_modules/");
5885
+ return !isGeneratedHarnessPath(pathPart);
5394
5886
  });
5395
5887
  }
5396
5888
 
@@ -5455,35 +5947,27 @@ function skipWorktreeRemoval(input) {
5455
5947
  }
5456
5948
  return null;
5457
5949
  }
5458
- function skipNodeModulesRemoval(input) {
5459
- const { indexed, includeOrphans, nodeModulesAgeMs, ageMs } = input;
5460
- if (ageMs < nodeModulesAgeMs) return "below_age_threshold";
5461
- if (!indexed) return includeOrphans ? null : "orphan_without_flag";
5462
- if (isWorkerProcessLive(indexed)) return "active_worker";
5463
- if (indexed.worker.completionBlocker) return "completion_blocked";
5464
- if (!isFinishedWorkerStatus(indexed.status)) return "run_still_active";
5465
- if (hasUnrestorableWorktreeChanges(indexed.status)) return "dirty_worktree";
5466
- const landing = assessWorkerLanding({
5467
- finalResult: indexed.status.finalResult,
5468
- changedFiles: indexed.status.changedFiles,
5469
- gitAncestry: indexed.status.gitAncestry,
5470
- prUrl: prUrlFromFinalResult(indexed.status.finalResult)
5471
- });
5472
- if (landing.blocked && materialWorktreeChanges2(indexed.status.changedFiles).length > 0) {
5473
- return "landing_blocked";
5474
- }
5950
+ function skipDependencyCacheRemoval(input) {
5951
+ const { indexed, nodeModulesAgeMs, ageMs, worktreePath, activeWorktreePaths, diskPressure } = input;
5952
+ if (!diskPressure && ageMs < nodeModulesAgeMs) return "below_age_threshold";
5953
+ if (activeWorktreePaths.has(path28.resolve(worktreePath))) return "active_worker";
5954
+ if (indexed && isWorkerProcessLive(indexed)) return "active_worker";
5955
+ if (indexed && hasUnrestorableWorktreeChanges(indexed.status)) return "dirty_worktree";
5475
5956
  return null;
5476
5957
  }
5958
+ function skipBuildCacheRemoval(input) {
5959
+ return skipDependencyCacheRemoval(input);
5960
+ }
5477
5961
 
5478
5962
  // src/cleanup-execute.ts
5479
- import { existsSync as existsSync18, rmSync as rmSync2 } from "node:fs";
5480
- import path28 from "node:path";
5963
+ import { existsSync as existsSync19, rmSync as rmSync2 } from "node:fs";
5964
+ import path30 from "node:path";
5481
5965
 
5482
5966
  // src/cleanup-dir-size.ts
5483
- import { existsSync as existsSync17, readdirSync as readdirSync5, statSync as statSync2 } from "node:fs";
5484
- import path27 from "node:path";
5967
+ import { existsSync as existsSync18, readdirSync as readdirSync5, statSync as statSync2 } from "node:fs";
5968
+ import path29 from "node:path";
5485
5969
  function directorySizeBytes(root, maxEntries = 5e4) {
5486
- if (!existsSync17(root)) return 0;
5970
+ if (!existsSync18(root)) return 0;
5487
5971
  let total = 0;
5488
5972
  let seen = 0;
5489
5973
  const stack = [root];
@@ -5497,7 +5981,7 @@ function directorySizeBytes(root, maxEntries = 5e4) {
5497
5981
  }
5498
5982
  for (const name of entries) {
5499
5983
  if (seen++ > maxEntries) return null;
5500
- const full = path27.join(current, name);
5984
+ const full = path29.join(current, name);
5501
5985
  let st;
5502
5986
  try {
5503
5987
  st = statSync2(full);
@@ -5512,8 +5996,8 @@ function directorySizeBytes(root, maxEntries = 5e4) {
5512
5996
  }
5513
5997
 
5514
5998
  // src/cleanup-execute.ts
5515
- function removeNodeModules(candidate, execute) {
5516
- if (!existsSync18(candidate.path)) {
5999
+ function removeDependencyCache(candidate, execute) {
6000
+ if (!existsSync19(candidate.path)) {
5517
6001
  return {
5518
6002
  ...candidate,
5519
6003
  executed: false,
@@ -5543,8 +6027,17 @@ function removeNodeModules(candidate, execute) {
5543
6027
  };
5544
6028
  }
5545
6029
  }
6030
+ function removeNodeModules(candidate, execute) {
6031
+ return removeDependencyCache(candidate, execute);
6032
+ }
6033
+ function removeNextCache(candidate, execute) {
6034
+ return removeDependencyCache(candidate, execute);
6035
+ }
6036
+ function removeBuildCache(candidate, execute) {
6037
+ return removeDependencyCache(candidate, execute);
6038
+ }
5546
6039
  function removeWorktree(candidate, execute) {
5547
- if (!existsSync18(candidate.path)) {
6040
+ if (!existsSync19(candidate.path)) {
5548
6041
  return {
5549
6042
  ...candidate,
5550
6043
  executed: false,
@@ -5561,7 +6054,7 @@ function removeWorktree(candidate, execute) {
5561
6054
  if (repo) {
5562
6055
  git(repo, ["worktree", "remove", "--force", candidate.path], { allowFailure: true });
5563
6056
  }
5564
- if (existsSync18(candidate.path)) {
6057
+ if (existsSync19(candidate.path)) {
5565
6058
  rmSync2(candidate.path, { recursive: true, force: true });
5566
6059
  }
5567
6060
  return {
@@ -5580,21 +6073,37 @@ function removeWorktree(candidate, execute) {
5580
6073
  };
5581
6074
  }
5582
6075
  }
6076
+ function isHarnessDependencyCachePath(targetPath, harnessRoot, worktreesDir, cacheDirName) {
6077
+ const resolved = path30.resolve(targetPath);
6078
+ const suffix = `${path30.sep}${cacheDirName}`;
6079
+ const cachePath = resolved.endsWith(suffix) ? resolved : null;
6080
+ if (!cachePath) return "path_outside_harness";
6081
+ const rel = path30.relative(worktreesDir, cachePath);
6082
+ if (rel.startsWith("..") || path30.isAbsolute(rel)) return "path_outside_harness";
6083
+ const parts = rel.split(path30.sep);
6084
+ if (parts.length < 3 || parts[parts.length - 1] !== cacheDirName) return "path_outside_harness";
6085
+ if (!resolved.startsWith(path30.resolve(harnessRoot))) return "path_outside_harness";
6086
+ return null;
6087
+ }
5583
6088
  function isHarnessNodeModulesPath(targetPath, harnessRoot, worktreesDir) {
5584
- const resolved = path28.resolve(targetPath);
5585
- const nm = resolved.endsWith(`${path28.sep}node_modules`) ? resolved : null;
5586
- if (!nm) return "path_outside_harness";
5587
- const rel = path28.relative(worktreesDir, nm);
5588
- if (rel.startsWith("..") || path28.isAbsolute(rel)) return "path_outside_harness";
5589
- const parts = rel.split(path28.sep);
5590
- if (parts.length < 3 || parts[parts.length - 1] !== "node_modules") return "path_outside_harness";
5591
- if (!resolved.startsWith(path28.resolve(harnessRoot))) return "path_outside_harness";
6089
+ return isHarnessDependencyCachePath(targetPath, harnessRoot, worktreesDir, "node_modules");
6090
+ }
6091
+ function isHarnessNextCachePath(targetPath, harnessRoot, worktreesDir) {
6092
+ return isHarnessDependencyCachePath(targetPath, harnessRoot, worktreesDir, ".next");
6093
+ }
6094
+ function isHarnessBuildCachePath(targetPath, harnessRoot, worktreesDir) {
6095
+ const resolved = path30.resolve(targetPath);
6096
+ const relToWt = path30.relative(worktreesDir, resolved);
6097
+ if (relToWt.startsWith("..") || path30.isAbsolute(relToWt)) return "path_outside_harness";
6098
+ const parts = relToWt.split(path30.sep);
6099
+ if (parts.length < 3) return "path_outside_harness";
6100
+ if (!resolved.startsWith(path30.resolve(harnessRoot))) return "path_outside_harness";
5592
6101
  return null;
5593
6102
  }
5594
6103
 
5595
6104
  // src/cleanup-scan.ts
5596
- import { existsSync as existsSync19, readdirSync as readdirSync6, statSync as statSync3 } from "node:fs";
5597
- import path29 from "node:path";
6105
+ import { existsSync as existsSync20, readdirSync as readdirSync6, statSync as statSync3 } from "node:fs";
6106
+ import path31 from "node:path";
5598
6107
  function pathAgeMs(target, now) {
5599
6108
  try {
5600
6109
  const mtime = statSync3(target).mtimeMs;
@@ -5604,50 +6113,57 @@ function pathAgeMs(target, now) {
5604
6113
  }
5605
6114
  }
5606
6115
  function isPathInside(child, parent) {
5607
- const rel = path29.relative(parent, child);
5608
- return rel === "" || !rel.startsWith("..") && !path29.isAbsolute(rel);
6116
+ const rel = path31.relative(parent, child);
6117
+ return rel === "" || !rel.startsWith("..") && !path31.isAbsolute(rel);
5609
6118
  }
5610
- function scanNodeModulesCandidates(opts) {
5611
- const candidates = [];
5612
- const seen = /* @__PURE__ */ new Set();
5613
- for (const entry of opts.index.values()) {
5614
- if (opts.runIdFilter && entry.runId !== opts.runIdFilter) continue;
5615
- const nm = path29.join(entry.worktreePath, "node_modules");
5616
- if (!existsSync19(nm)) continue;
5617
- const resolved = path29.resolve(nm);
6119
+ function collectBuildCacheForWorktree(worktreePath, opts, seen, meta) {
6120
+ const out = [];
6121
+ for (const rel of HARNESS_BUILD_CACHE_RELATIVE_PATHS) {
6122
+ if (rel === ".next") continue;
6123
+ const target = path31.join(worktreePath, rel);
6124
+ if (!existsSync20(target)) continue;
6125
+ const resolved = path31.resolve(target);
5618
6126
  if (seen.has(resolved)) continue;
6127
+ if (!isPathInside(resolved, opts.harnessRoot)) continue;
5619
6128
  seen.add(resolved);
5620
- candidates.push({
5621
- kind: "remove_node_modules",
6129
+ out.push({
6130
+ kind: "remove_build_cache",
5622
6131
  path: resolved,
5623
6132
  bytes: null,
5624
- runId: entry.runId,
5625
- worker: entry.workerName,
5626
- repo: entry.run.repo,
6133
+ runId: meta.runId,
6134
+ worker: meta.worker,
6135
+ repo: meta.repo,
5627
6136
  ageMs: pathAgeMs(resolved, opts.now)
5628
6137
  });
5629
6138
  }
5630
- if (!opts.includeOrphans || !existsSync19(opts.worktreesDir)) return candidates;
6139
+ return out;
6140
+ }
6141
+ function scanBuildCacheCandidates(opts) {
6142
+ const candidates = [];
6143
+ const seen = /* @__PURE__ */ new Set();
6144
+ for (const entry of opts.index.values()) {
6145
+ if (opts.runIdFilter && entry.runId !== opts.runIdFilter) continue;
6146
+ candidates.push(
6147
+ ...collectBuildCacheForWorktree(entry.worktreePath, opts, seen, {
6148
+ runId: entry.runId,
6149
+ worker: entry.workerName,
6150
+ repo: entry.run.repo
6151
+ })
6152
+ );
6153
+ }
6154
+ if (!opts.includeOrphans || !existsSync20(opts.worktreesDir)) return candidates;
5631
6155
  for (const runEntry of readdirSync6(opts.worktreesDir, { withFileTypes: true })) {
5632
6156
  if (!runEntry.isDirectory()) continue;
5633
- const runPath = path29.join(opts.worktreesDir, runEntry.name);
6157
+ const runPath = path31.join(opts.worktreesDir, runEntry.name);
5634
6158
  for (const workerEntry of readdirSync6(runPath, { withFileTypes: true })) {
5635
6159
  if (!workerEntry.isDirectory()) continue;
5636
- const worktreePath = path29.join(runPath, workerEntry.name);
5637
- const nm = path29.join(worktreePath, "node_modules");
5638
- if (!existsSync19(nm)) continue;
5639
- const resolved = path29.resolve(nm);
5640
- if (seen.has(resolved)) continue;
5641
- if (!isPathInside(resolved, opts.harnessRoot)) continue;
5642
- seen.add(resolved);
5643
- candidates.push({
5644
- kind: "remove_node_modules",
5645
- path: resolved,
5646
- bytes: null,
5647
- runId: runEntry.name,
5648
- worker: workerEntry.name,
5649
- ageMs: pathAgeMs(resolved, opts.now)
5650
- });
6160
+ const worktreePath = path31.join(runPath, workerEntry.name);
6161
+ candidates.push(
6162
+ ...collectBuildCacheForWorktree(worktreePath, opts, seen, {
6163
+ runId: runEntry.name,
6164
+ worker: workerEntry.name
6165
+ })
6166
+ );
5651
6167
  }
5652
6168
  }
5653
6169
  return candidates;
@@ -5662,7 +6178,7 @@ function scanWorktreeCandidates(opts) {
5662
6178
  for (const entry of opts.index.values()) {
5663
6179
  if (opts.runIdFilter && entry.runId !== opts.runIdFilter) continue;
5664
6180
  const resolved = entry.worktreePath;
5665
- if (!existsSync19(resolved)) continue;
6181
+ if (!existsSync20(resolved)) continue;
5666
6182
  if (seen.has(resolved)) continue;
5667
6183
  seen.add(resolved);
5668
6184
  candidates.push({
@@ -5676,15 +6192,15 @@ function scanWorktreeCandidates(opts) {
5676
6192
  });
5677
6193
  }
5678
6194
  }
5679
- if (!orphanEnabled || !existsSync19(opts.worktreesDir)) return candidates;
6195
+ if (!orphanEnabled || !existsSync20(opts.worktreesDir)) return candidates;
5680
6196
  const indexedPaths = /* @__PURE__ */ new Set();
5681
6197
  for (const entry of opts.index.values()) {
5682
- indexedPaths.add(path29.resolve(entry.worktreePath));
6198
+ indexedPaths.add(path31.resolve(entry.worktreePath));
5683
6199
  }
5684
6200
  for (const runEntry of readdirSync6(opts.worktreesDir, { withFileTypes: true })) {
5685
6201
  if (!runEntry.isDirectory()) continue;
5686
6202
  if (opts.runIdFilter && runEntry.name !== opts.runIdFilter) continue;
5687
- const runPath = path29.join(opts.worktreesDir, runEntry.name);
6203
+ const runPath = path31.join(opts.worktreesDir, runEntry.name);
5688
6204
  let workerEntries;
5689
6205
  try {
5690
6206
  workerEntries = readdirSync6(runPath, { withFileTypes: true });
@@ -5693,7 +6209,7 @@ function scanWorktreeCandidates(opts) {
5693
6209
  }
5694
6210
  for (const workerEntry of workerEntries) {
5695
6211
  if (!workerEntry.isDirectory()) continue;
5696
- const worktreePath = path29.resolve(path29.join(runPath, workerEntry.name));
6212
+ const worktreePath = path31.resolve(path31.join(runPath, workerEntry.name));
5697
6213
  if (seen.has(worktreePath)) continue;
5698
6214
  if (indexedPaths.has(worktreePath)) continue;
5699
6215
  if (!isPathInside(worktreePath, opts.harnessRoot)) continue;
@@ -5711,18 +6227,191 @@ function scanWorktreeCandidates(opts) {
5711
6227
  return candidates;
5712
6228
  }
5713
6229
 
6230
+ // src/cleanup-dependency-scan.ts
6231
+ import { existsSync as existsSync21, readdirSync as readdirSync7, statSync as statSync4 } from "node:fs";
6232
+ import path32 from "node:path";
6233
+ var DEPENDENCY_CACHE_DIRS = [
6234
+ { dirName: "node_modules", kind: "remove_node_modules" },
6235
+ { dirName: ".next", kind: "remove_next_cache" }
6236
+ ];
6237
+ function pathAgeMs2(target, now) {
6238
+ try {
6239
+ const mtime = statSync4(target).mtimeMs;
6240
+ return Math.max(0, now - mtime);
6241
+ } catch {
6242
+ return 0;
6243
+ }
6244
+ }
6245
+ function isPathInside2(child, parent) {
6246
+ const rel = path32.relative(parent, child);
6247
+ return rel === "" || !rel.startsWith("..") && !path32.isAbsolute(rel);
6248
+ }
6249
+ function pushCandidate2(candidates, seen, opts, targetPath, kind, meta) {
6250
+ if (!existsSync21(targetPath)) return;
6251
+ const resolved = path32.resolve(targetPath);
6252
+ if (seen.has(resolved)) return;
6253
+ if (!isPathInside2(resolved, opts.harnessRoot)) return;
6254
+ seen.add(resolved);
6255
+ candidates.push({
6256
+ kind,
6257
+ path: resolved,
6258
+ bytes: null,
6259
+ harnessRoot: opts.harnessRoot,
6260
+ runId: meta.runId,
6261
+ worker: meta.worker,
6262
+ repo: meta.repo,
6263
+ ageMs: pathAgeMs2(resolved, opts.now)
6264
+ });
6265
+ }
6266
+ function scanWorktreeDependencyCaches(candidates, seen, opts, worktreePath, meta) {
6267
+ for (const entry of DEPENDENCY_CACHE_DIRS) {
6268
+ pushCandidate2(candidates, seen, opts, path32.join(worktreePath, entry.dirName), entry.kind, meta);
6269
+ }
6270
+ }
6271
+ function scanDependencyCacheCandidates(opts) {
6272
+ const candidates = [];
6273
+ const seen = /* @__PURE__ */ new Set();
6274
+ for (const entry of opts.index.values()) {
6275
+ if (opts.runIdFilter && entry.runId !== opts.runIdFilter) continue;
6276
+ scanWorktreeDependencyCaches(candidates, seen, opts, entry.worktreePath, {
6277
+ runId: entry.runId,
6278
+ worker: entry.workerName,
6279
+ repo: entry.run.repo
6280
+ });
6281
+ }
6282
+ if (!existsSync21(opts.worktreesDir)) return candidates;
6283
+ for (const runEntry of readdirSync7(opts.worktreesDir, { withFileTypes: true })) {
6284
+ if (!runEntry.isDirectory()) continue;
6285
+ if (opts.runIdFilter && runEntry.name !== opts.runIdFilter) continue;
6286
+ const runPath = path32.join(opts.worktreesDir, runEntry.name);
6287
+ let workerEntries;
6288
+ try {
6289
+ workerEntries = readdirSync7(runPath, { withFileTypes: true });
6290
+ } catch {
6291
+ continue;
6292
+ }
6293
+ for (const workerEntry of workerEntries) {
6294
+ if (!workerEntry.isDirectory()) continue;
6295
+ const worktreePath = path32.join(runPath, workerEntry.name);
6296
+ scanWorktreeDependencyCaches(candidates, seen, opts, worktreePath, {
6297
+ runId: runEntry.name,
6298
+ worker: workerEntry.name
6299
+ });
6300
+ }
6301
+ }
6302
+ return candidates;
6303
+ }
6304
+
6305
+ // src/cleanup-duplicate-worktrees.ts
6306
+ import { existsSync as existsSync22, statSync as statSync5 } from "node:fs";
6307
+ import path33 from "node:path";
6308
+ function pathAgeMs3(target, now) {
6309
+ try {
6310
+ const mtime = statSync5(target).mtimeMs;
6311
+ return Math.max(0, now - mtime);
6312
+ } catch {
6313
+ return 0;
6314
+ }
6315
+ }
6316
+ function parseWorktreePorcelain(output) {
6317
+ const records = [];
6318
+ let current = null;
6319
+ for (const line of output.split("\n")) {
6320
+ if (!line.trim()) continue;
6321
+ const [key, ...rest] = line.split(" ");
6322
+ const value = rest.join(" ");
6323
+ if (key === "worktree") {
6324
+ if (current) records.push(current);
6325
+ current = { path: value };
6326
+ continue;
6327
+ }
6328
+ if (!current) continue;
6329
+ if (key === "branch") current.branch = value;
6330
+ if (key === "HEAD") current.head = value;
6331
+ if (key === "bare") current.bare = true;
6332
+ }
6333
+ if (current) records.push(current);
6334
+ return records;
6335
+ }
6336
+ function isUnderWorktreesDir(worktreePath, worktreesDir) {
6337
+ const rel = path33.relative(path33.resolve(worktreesDir), path33.resolve(worktreePath));
6338
+ return rel !== "" && !rel.startsWith("..") && !path33.isAbsolute(rel);
6339
+ }
6340
+ function isCleanWorktree(worktreePath, repoRoot) {
6341
+ try {
6342
+ const porcelain = git(repoRoot, ["-C", worktreePath, "status", "--porcelain"], {
6343
+ allowFailure: true
6344
+ });
6345
+ return !String(porcelain || "").trim();
6346
+ } catch {
6347
+ return false;
6348
+ }
6349
+ }
6350
+ function scanDuplicateWorktreeCandidates(opts) {
6351
+ if (!opts.includeOrphans || !existsSync22(opts.worktreesDir)) return [];
6352
+ const repos = /* @__PURE__ */ new Set();
6353
+ for (const entry of opts.index.values()) {
6354
+ if (entry.run.repo) repos.add(path33.resolve(entry.run.repo));
6355
+ }
6356
+ const indexedPaths = /* @__PURE__ */ new Set();
6357
+ for (const entry of opts.index.values()) {
6358
+ indexedPaths.add(path33.resolve(entry.worktreePath));
6359
+ }
6360
+ const candidates = [];
6361
+ const seen = /* @__PURE__ */ new Set();
6362
+ for (const repoRoot of repos) {
6363
+ let porcelain;
6364
+ try {
6365
+ porcelain = git(repoRoot, ["worktree", "list", "--porcelain"], { allowFailure: true });
6366
+ } catch {
6367
+ continue;
6368
+ }
6369
+ const worktrees = parseWorktreePorcelain(porcelain);
6370
+ for (const wt of worktrees) {
6371
+ const resolved = path33.resolve(wt.path);
6372
+ if (resolved === path33.resolve(repoRoot)) continue;
6373
+ if (!isUnderWorktreesDir(resolved, opts.worktreesDir)) continue;
6374
+ if (indexedPaths.has(resolved)) continue;
6375
+ if (seen.has(resolved)) continue;
6376
+ if (!existsSync22(resolved)) continue;
6377
+ if (!isCleanWorktree(resolved, repoRoot)) continue;
6378
+ const rel = path33.relative(opts.worktreesDir, resolved);
6379
+ const parts = rel.split(path33.sep);
6380
+ const runId = parts[0];
6381
+ const worker = parts[1] ?? "unknown";
6382
+ seen.add(resolved);
6383
+ candidates.push({
6384
+ kind: "remove_worktree",
6385
+ path: resolved,
6386
+ bytes: null,
6387
+ runId,
6388
+ worker,
6389
+ repo: repoRoot,
6390
+ ageMs: pathAgeMs3(resolved, opts.now)
6391
+ });
6392
+ }
6393
+ }
6394
+ return candidates;
6395
+ }
6396
+
5714
6397
  // src/cleanup-worktree-index.ts
5715
- import path30 from "node:path";
5716
- function buildWorktreeIndex() {
6398
+ import path34 from "node:path";
6399
+ function buildWorktreeIndexAt(harnessRoot) {
5717
6400
  const index = /* @__PURE__ */ new Map();
5718
- for (const run of listRunRecords()) {
6401
+ for (const run of listRunRecordsForHarnessRoot(harnessRoot)) {
5719
6402
  for (const name of Object.keys(run.workers || {})) {
5720
- const workerPath = path30.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json");
6403
+ const workerPath = path34.join(
6404
+ runDirectoryAt(harnessRoot, run.id),
6405
+ "workers",
6406
+ safeSlug(name),
6407
+ "worker.json"
6408
+ );
5721
6409
  const worker = readJson(workerPath, void 0);
5722
6410
  if (!worker?.worktreePath) continue;
5723
6411
  const status = computeWorkerStatus(worker, { base: run.base, baseCommit: run.baseCommit });
5724
- index.set(path30.resolve(worker.worktreePath), {
5725
- worktreePath: path30.resolve(worker.worktreePath),
6412
+ index.set(path34.resolve(worker.worktreePath), {
6413
+ harnessRoot,
6414
+ worktreePath: path34.resolve(worker.worktreePath),
5726
6415
  runId: run.id,
5727
6416
  workerName: name,
5728
6417
  run,
@@ -5781,15 +6470,15 @@ function resolvePipelineHarnessRetention(runId) {
5781
6470
  }
5782
6471
 
5783
6472
  // src/cleanup-orphan-safety.ts
5784
- import { existsSync as existsSync20, statSync as statSync4 } from "node:fs";
5785
- import path31 from "node:path";
6473
+ import { existsSync as existsSync23, statSync as statSync6 } from "node:fs";
6474
+ import path35 from "node:path";
5786
6475
  var DEFAULT_HEARTBEAT_FRESH_MS = 30 * 60 * 1e3;
5787
6476
  function assessOrphanWorktreeSafety(input) {
5788
6477
  const now = input.now ?? Date.now();
5789
6478
  const heartbeatFreshMs = input.heartbeatFreshMs ?? DEFAULT_HEARTBEAT_FRESH_MS;
5790
- if (!existsSync20(input.worktreePath)) return null;
6479
+ if (!existsSync23(input.worktreePath)) return null;
5791
6480
  if (input.runId && input.workerName) {
5792
- const heartbeatPath = path31.join(
6481
+ const heartbeatPath = path35.join(
5793
6482
  input.harnessRoot,
5794
6483
  "runs",
5795
6484
  input.runId,
@@ -5798,13 +6487,13 @@ function assessOrphanWorktreeSafety(input) {
5798
6487
  "heartbeat.jsonl"
5799
6488
  );
5800
6489
  try {
5801
- const mtime = statSync4(heartbeatPath).mtimeMs;
6490
+ const mtime = statSync6(heartbeatPath).mtimeMs;
5802
6491
  if (now - mtime < heartbeatFreshMs) return "active_worker";
5803
6492
  } catch {
5804
6493
  }
5805
6494
  }
5806
- const gitDir = path31.join(input.worktreePath, ".git");
5807
- if (!existsSync20(gitDir)) return null;
6495
+ const gitDir = path35.join(input.worktreePath, ".git");
6496
+ if (!existsSync23(gitDir)) return null;
5808
6497
  const porcelain = gitCapture(input.worktreePath, ["status", "--porcelain"]);
5809
6498
  if (porcelain.status !== 0) return "pr_or_unmerged_commits";
5810
6499
  const dirtyLines = porcelain.stdout.split("\n").map((line) => line.trim()).filter((line) => line.length > 0);
@@ -5833,14 +6522,14 @@ function assessOrphanWorktreeSafety(input) {
5833
6522
  }
5834
6523
 
5835
6524
  // src/harness-storage-snapshot.ts
5836
- import { existsSync as existsSync21, readdirSync as readdirSync7, statSync as statSync5 } from "node:fs";
5837
- import path32 from "node:path";
6525
+ import { existsSync as existsSync24, readdirSync as readdirSync8, statSync as statSync7 } from "node:fs";
6526
+ import path36 from "node:path";
5838
6527
  function harnessStorageSnapshot(opts = {}) {
5839
6528
  const harnessRoot = opts.harnessRoot ?? resolveHarnessRoot();
5840
- const worktreesDir = path32.join(harnessRoot, "worktrees");
6529
+ const worktreesDir = path36.join(harnessRoot, "worktrees");
5841
6530
  const now = opts.now ?? Date.now();
5842
6531
  const scannedAt = new Date(now).toISOString();
5843
- if (!existsSync21(worktreesDir)) {
6532
+ if (!existsSync24(worktreesDir)) {
5844
6533
  return {
5845
6534
  harnessRoot,
5846
6535
  worktreesDir,
@@ -5857,7 +6546,7 @@ function harnessStorageSnapshot(opts = {}) {
5857
6546
  let oldestMs = null;
5858
6547
  let entries;
5859
6548
  try {
5860
- entries = readdirSync7(worktreesDir, { withFileTypes: true });
6549
+ entries = readdirSync8(worktreesDir, { withFileTypes: true });
5861
6550
  } catch {
5862
6551
  return {
5863
6552
  harnessRoot,
@@ -5872,14 +6561,14 @@ function harnessStorageSnapshot(opts = {}) {
5872
6561
  for (const runEntry of entries) {
5873
6562
  if (!runEntry.isDirectory()) continue;
5874
6563
  runCount += 1;
5875
- const runPath = path32.join(worktreesDir, runEntry.name);
6564
+ const runPath = path36.join(worktreesDir, runEntry.name);
5876
6565
  try {
5877
- const st = statSync5(runPath);
6566
+ const st = statSync7(runPath);
5878
6567
  oldestMs = oldestMs === null ? st.mtimeMs : Math.min(oldestMs, st.mtimeMs);
5879
6568
  } catch {
5880
6569
  }
5881
6570
  try {
5882
- for (const workerEntry of readdirSync7(runPath, { withFileTypes: true })) {
6571
+ for (const workerEntry of readdirSync8(runPath, { withFileTypes: true })) {
5883
6572
  if (workerEntry.isDirectory()) workerCount += 1;
5884
6573
  }
5885
6574
  } catch {
@@ -5906,12 +6595,110 @@ function harnessStorageSnapshot(opts = {}) {
5906
6595
  };
5907
6596
  }
5908
6597
 
6598
+ // src/cleanup-harness-roots.ts
6599
+ import { existsSync as existsSync25 } from "node:fs";
6600
+ import { homedir as homedir6 } from "node:os";
6601
+ import path37 from "node:path";
6602
+ var WELL_KNOWN_HARNESS_SCAN_ROOTS = [
6603
+ "/var/tmp/kynver-harness",
6604
+ path37.join(homedir6(), ".openclaw", "harness")
6605
+ ];
6606
+ function addRoot(seen, roots, candidate) {
6607
+ if (!candidate?.trim()) return;
6608
+ const resolved = path37.resolve(resolveUserPath(candidate.trim()));
6609
+ if (seen.has(resolved)) return;
6610
+ seen.add(resolved);
6611
+ roots.push(resolved);
6612
+ }
6613
+ function shouldScanWellKnownRoots(options) {
6614
+ if (options.scanWellKnown != null) return options.scanWellKnown;
6615
+ if (process.env.VITEST === "true") return false;
6616
+ return process.env.KYNVER_CLEANUP_SCAN_WELL_KNOWN !== "0" && !["0", "false", "no"].includes((process.env.KYNVER_CLEANUP_SCAN_WELL_KNOWN ?? "").toLowerCase());
6617
+ }
6618
+ function resolveHarnessScanRoots(options = {}) {
6619
+ const seen = /* @__PURE__ */ new Set();
6620
+ const roots = [];
6621
+ addRoot(seen, roots, options.harnessRoot ?? resolveHarnessRoot());
6622
+ const extra = process.env.KYNVER_CLEANUP_EXTRA_ROOTS?.split(",").map((part) => part.trim()).filter(Boolean);
6623
+ for (const candidate of extra ?? []) addRoot(seen, roots, candidate);
6624
+ if (shouldScanWellKnownRoots(options)) {
6625
+ for (const candidate of WELL_KNOWN_HARNESS_SCAN_ROOTS) {
6626
+ const resolved = path37.resolve(candidate);
6627
+ if (!seen.has(resolved) && existsSync25(resolved)) addRoot(seen, roots, resolved);
6628
+ }
6629
+ }
6630
+ return roots;
6631
+ }
6632
+
6633
+ // src/cleanup-active-worktrees.ts
6634
+ import path38 from "node:path";
6635
+ function isActiveHarnessWorker2(worker, runBase, runBaseCommit) {
6636
+ const status = computeWorkerStatus(worker, { base: runBase, baseCommit: runBaseCommit });
6637
+ return status.alive && !status.finalResult && status.attention.state !== "done";
6638
+ }
6639
+ function collectActiveWorktreeGuards(harnessRoots) {
6640
+ const activeWorktreePaths = /* @__PURE__ */ new Set();
6641
+ const liveRunKeys = /* @__PURE__ */ new Set();
6642
+ for (const harnessRoot of harnessRoots) {
6643
+ for (const run of listRunRecordsForHarnessRoot(harnessRoot)) {
6644
+ let runHasLive = false;
6645
+ for (const name of Object.keys(run.workers || {})) {
6646
+ const worker = readJson(
6647
+ path38.join(runDirectoryAt(harnessRoot, run.id), "workers", safeSlug(name), "worker.json"),
6648
+ void 0
6649
+ );
6650
+ if (!worker?.worktreePath) continue;
6651
+ const worktreePath = path38.resolve(worker.worktreePath);
6652
+ if (!isActiveHarnessWorker2(worker, run.base, run.baseCommit)) continue;
6653
+ runHasLive = true;
6654
+ activeWorktreePaths.add(worktreePath);
6655
+ }
6656
+ if (runHasLive) liveRunKeys.add(`${harnessRoot}\0${run.id}`);
6657
+ }
6658
+ }
6659
+ return { activeWorktreePaths, liveRunKeys };
6660
+ }
6661
+
6662
+ // src/cleanup-disk-pressure.ts
6663
+ function envFlag2(name) {
6664
+ const v = process.env[name];
6665
+ return v === "1" || v === "true" || v === "yes";
6666
+ }
6667
+ function envNumber(name, fallback) {
6668
+ const raw = process.env[name];
6669
+ if (!raw) return fallback;
6670
+ const n = Number(raw);
6671
+ return Number.isFinite(n) ? n : fallback;
6672
+ }
6673
+ function observeCleanupDiskPressure(input = {}) {
6674
+ const diskPath = input.diskPath?.trim() || process.env.KYNVER_DISK_GUARD_PATH?.trim() || "/";
6675
+ const maxUsedPercent = envNumber("KYNVER_DISK_GUARD_MAX_USED_PERCENT", 75);
6676
+ const diskGate = observeRunnerDiskGate({
6677
+ ...input,
6678
+ diskPath,
6679
+ diskMaxUsedPercent: input.diskMaxUsedPercent ?? maxUsedPercent
6680
+ });
6681
+ const pressured = !diskGate.ok || diskGate.usedPercent >= maxUsedPercent;
6682
+ return { diskGate, pressured, maxUsedPercent };
6683
+ }
6684
+ function applyDiskPressureToRetention(retention, pressure) {
6685
+ if (!pressure.pressured) return retention;
6686
+ const executeOnPressure = retention.execute || envFlag2("KYNVER_CLEANUP_EXECUTE_ON_PRESSURE");
6687
+ return {
6688
+ ...retention,
6689
+ execute: executeOnPressure,
6690
+ nodeModulesAgeMs: 0,
6691
+ diskPressure: true,
6692
+ diskGate: pressure.diskGate
6693
+ };
6694
+ }
6695
+
5909
6696
  // src/cleanup.ts
5910
6697
  function resolvePaths(options = {}) {
5911
6698
  const harnessRoot = options.harnessRoot ? resolveUserPath(options.harnessRoot) : resolveHarnessRoot();
5912
- const { worktreesDir } = options.harnessRoot ? { worktreesDir: path33.join(harnessRoot, "worktrees") } : getHarnessPaths();
6699
+ const scanRoots = resolveHarnessScanRoots({ harnessRoot });
5913
6700
  const now = options.now ?? Date.now();
5914
- return { harnessRoot, worktreesDir, now };
6701
+ return { harnessRoot, scanRoots, now };
5915
6702
  }
5916
6703
  function normalizeGuardSkip(skip) {
5917
6704
  if (typeof skip === "string") return { reason: skip };
@@ -5936,72 +6723,145 @@ function tallySkipReasons(actions, skips) {
5936
6723
  }
5937
6724
  return counts;
5938
6725
  }
6726
+ function removeDependencyCacheAction(candidate, execute) {
6727
+ if (candidate.kind === "remove_next_cache") return removeNextCache(candidate, execute);
6728
+ return removeNodeModules(candidate, execute);
6729
+ }
6730
+ function pathGuardForDependencyCache(candidate, harnessRoot, worktreesDir) {
6731
+ if (candidate.kind === "remove_next_cache") {
6732
+ return isHarnessNextCachePath(candidate.path, harnessRoot, worktreesDir);
6733
+ }
6734
+ return isHarnessNodeModulesPath(candidate.path, harnessRoot, worktreesDir);
6735
+ }
6736
+ function mergeWorktreeIndexes(scanRoots) {
6737
+ const merged = /* @__PURE__ */ new Map();
6738
+ for (const root of scanRoots) {
6739
+ for (const [key, value] of buildWorktreeIndexAt(root)) merged.set(key, value);
6740
+ }
6741
+ return merged;
6742
+ }
6743
+ function worktreePathForCandidate(candidate, worktreesDir) {
6744
+ if (candidate.runId && candidate.worker) {
6745
+ return path39.join(worktreesDir, candidate.runId, candidate.worker);
6746
+ }
6747
+ return path39.resolve(candidate.path, "..");
6748
+ }
5939
6749
  function runHarnessCleanup(options = {}) {
5940
- const retention = resolveHarnessRetention(options);
6750
+ let retention = resolveHarnessRetention(options);
6751
+ const diskPressure = observeCleanupDiskPressure();
6752
+ retention = applyDiskPressureToRetention(retention, diskPressure);
5941
6753
  const paths = resolvePaths(options);
6754
+ const activeGuards = collectActiveWorktreeGuards(paths.scanRoots);
6755
+ const index = mergeWorktreeIndexes(paths.scanRoots);
5942
6756
  const finalizedRuns = retention.finalizeStaleRuns ? finalizeStaleRuns().map((f) => ({ runId: f.runId, from: f.from, to: f.to })) : [];
5943
- const index = buildWorktreeIndex();
5944
- const scanOpts = {
5945
- harnessRoot: paths.harnessRoot,
5946
- worktreesDir: paths.worktreesDir,
5947
- nodeModulesAgeMs: retention.nodeModulesAgeMs,
5948
- worktreesAgeMs: retention.worktreesAgeMs,
5949
- includeOrphans: retention.includeOrphans,
5950
- runIdFilter: retention.runIdFilter,
5951
- index,
5952
- now: paths.now
5953
- };
5954
6757
  const skips = [];
5955
6758
  const actions = [];
5956
- for (const raw of scanNodeModulesCandidates(scanOpts)) {
5957
- const candidate = attachCandidateBytes(raw, retention.accountBytes);
5958
- const pathSkip = isHarnessNodeModulesPath(candidate.path, paths.harnessRoot, paths.worktreesDir);
5959
- if (pathSkip) {
5960
- recordSkip(skips, candidate.path, pathSkip);
5961
- actions.push({ ...candidate, executed: false, skipped: true, skipReason: pathSkip });
5962
- continue;
5963
- }
5964
- const worktreePath = path33.resolve(candidate.path, "..");
5965
- const indexed = index.get(worktreePath) ?? null;
5966
- const guardReason = skipNodeModulesRemoval({
5967
- indexed,
5968
- includeOrphans: retention.includeOrphans,
6759
+ const processedPaths = /* @__PURE__ */ new Set();
6760
+ for (const harnessRoot of paths.scanRoots) {
6761
+ const worktreesDir = path39.join(harnessRoot, "worktrees");
6762
+ const scanOpts = {
6763
+ harnessRoot,
6764
+ worktreesDir,
5969
6765
  nodeModulesAgeMs: retention.nodeModulesAgeMs,
5970
- ageMs: candidate.ageMs
5971
- });
5972
- if (guardReason) {
5973
- recordSkip(skips, candidate.path, guardReason);
5974
- actions.push({ ...candidate, executed: false, skipped: true, skipReason: guardReason });
5975
- continue;
5976
- }
5977
- actions.push(removeNodeModules(candidate, retention.execute));
5978
- }
5979
- for (const raw of scanWorktreeCandidates(scanOpts)) {
5980
- const candidate = attachCandidateBytes(raw, retention.accountBytes);
5981
- const indexed = index.get(path33.resolve(candidate.path)) ?? null;
5982
- const orphanSafety = indexed ? null : assessOrphanWorktreeSafety({
5983
- worktreePath: candidate.path,
5984
- harnessRoot: paths.harnessRoot,
5985
- runId: candidate.runId,
5986
- workerName: candidate.worker,
5987
- now: paths.now
5988
- });
5989
- const guardSkip = skipWorktreeRemoval({
5990
- indexed,
5991
- worktreePath: path33.resolve(candidate.path),
5992
- includeOrphans: retention.includeOrphans,
5993
6766
  worktreesAgeMs: retention.worktreesAgeMs,
5994
- ageMs: candidate.ageMs,
5995
- orphanSafety,
5996
- worktreeRemovalGuard: options.worktreeRemovalGuard
5997
- });
5998
- if (guardSkip) {
5999
- const { reason: guardReason, detail: guardDetail } = normalizeGuardSkip(guardSkip);
6000
- recordSkip(skips, candidate.path, guardReason, guardDetail);
6001
- actions.push({ ...candidate, executed: false, skipped: true, skipReason: guardReason });
6002
- continue;
6767
+ includeOrphans: retention.includeOrphans,
6768
+ runIdFilter: retention.runIdFilter,
6769
+ index,
6770
+ now: paths.now
6771
+ };
6772
+ for (const raw of scanDependencyCacheCandidates(scanOpts)) {
6773
+ const candidate = attachCandidateBytes(raw, retention.accountBytes);
6774
+ const resolved = path39.resolve(candidate.path);
6775
+ if (processedPaths.has(resolved)) continue;
6776
+ processedPaths.add(resolved);
6777
+ const pathSkip = pathGuardForDependencyCache(candidate, harnessRoot, worktreesDir);
6778
+ if (pathSkip) {
6779
+ recordSkip(skips, candidate.path, pathSkip);
6780
+ actions.push({ ...candidate, executed: false, skipped: true, skipReason: pathSkip });
6781
+ continue;
6782
+ }
6783
+ const worktreePath = worktreePathForCandidate(candidate, worktreesDir);
6784
+ const indexed = index.get(path39.resolve(worktreePath)) ?? null;
6785
+ const guardReason = skipDependencyCacheRemoval({
6786
+ indexed,
6787
+ includeOrphans: true,
6788
+ nodeModulesAgeMs: retention.nodeModulesAgeMs,
6789
+ ageMs: candidate.ageMs,
6790
+ worktreePath,
6791
+ activeWorktreePaths: activeGuards.activeWorktreePaths,
6792
+ diskPressure: retention.diskPressure
6793
+ });
6794
+ if (guardReason) {
6795
+ recordSkip(skips, candidate.path, guardReason);
6796
+ actions.push({ ...candidate, executed: false, skipped: true, skipReason: guardReason });
6797
+ continue;
6798
+ }
6799
+ actions.push(removeDependencyCacheAction(candidate, retention.execute));
6800
+ }
6801
+ for (const raw of scanBuildCacheCandidates(scanOpts)) {
6802
+ const candidate = attachCandidateBytes(raw, retention.accountBytes);
6803
+ const resolved = path39.resolve(candidate.path);
6804
+ if (processedPaths.has(resolved)) continue;
6805
+ processedPaths.add(resolved);
6806
+ const pathSkip = isHarnessBuildCachePath(candidate.path, harnessRoot, worktreesDir);
6807
+ if (pathSkip) {
6808
+ recordSkip(skips, candidate.path, pathSkip);
6809
+ actions.push({ ...candidate, executed: false, skipped: true, skipReason: pathSkip });
6810
+ continue;
6811
+ }
6812
+ const worktreePath = worktreePathForCandidate(candidate, worktreesDir);
6813
+ const indexed = index.get(path39.resolve(worktreePath)) ?? null;
6814
+ const guardReason = skipBuildCacheRemoval({
6815
+ indexed,
6816
+ includeOrphans: true,
6817
+ nodeModulesAgeMs: retention.nodeModulesAgeMs,
6818
+ ageMs: candidate.ageMs,
6819
+ worktreePath,
6820
+ activeWorktreePaths: activeGuards.activeWorktreePaths,
6821
+ diskPressure: retention.diskPressure
6822
+ });
6823
+ if (guardReason) {
6824
+ recordSkip(skips, candidate.path, guardReason);
6825
+ actions.push({ ...candidate, executed: false, skipped: true, skipReason: guardReason });
6826
+ continue;
6827
+ }
6828
+ actions.push(removeBuildCache(candidate, retention.execute));
6829
+ }
6830
+ const worktreeCandidates = [
6831
+ ...scanWorktreeCandidates(scanOpts),
6832
+ ...scanDuplicateWorktreeCandidates(scanOpts)
6833
+ ];
6834
+ const worktreeSeen = /* @__PURE__ */ new Set();
6835
+ for (const raw of worktreeCandidates) {
6836
+ const resolved = path39.resolve(raw.path);
6837
+ if (worktreeSeen.has(resolved)) continue;
6838
+ worktreeSeen.add(resolved);
6839
+ const candidate = attachCandidateBytes({ ...raw, path: resolved }, retention.accountBytes);
6840
+ const indexed = index.get(path39.resolve(candidate.path)) ?? null;
6841
+ const orphanSafety = indexed ? null : assessOrphanWorktreeSafety({
6842
+ worktreePath: candidate.path,
6843
+ harnessRoot,
6844
+ runId: candidate.runId,
6845
+ workerName: candidate.worker,
6846
+ now: paths.now
6847
+ });
6848
+ const guardSkip = skipWorktreeRemoval({
6849
+ indexed,
6850
+ worktreePath: path39.resolve(candidate.path),
6851
+ includeOrphans: retention.includeOrphans,
6852
+ worktreesAgeMs: retention.worktreesAgeMs,
6853
+ ageMs: candidate.ageMs,
6854
+ orphanSafety,
6855
+ worktreeRemovalGuard: options.worktreeRemovalGuard
6856
+ });
6857
+ if (guardSkip) {
6858
+ const { reason: guardReason, detail: guardDetail } = normalizeGuardSkip(guardSkip);
6859
+ recordSkip(skips, candidate.path, guardReason, guardDetail);
6860
+ actions.push({ ...candidate, executed: false, skipped: true, skipReason: guardReason });
6861
+ continue;
6862
+ }
6863
+ actions.push(removeWorktree(candidate, retention.execute));
6003
6864
  }
6004
- actions.push(removeWorktree(candidate, retention.execute));
6005
6865
  }
6006
6866
  let candidateBytes = 0;
6007
6867
  let reclaimableBytes = 0;
@@ -6022,11 +6882,20 @@ function runHarnessCleanup(options = {}) {
6022
6882
  const storage = retention.accountBytes ? harnessStorageSnapshot({ harnessRoot: paths.harnessRoot, now: paths.now }) : void 0;
6023
6883
  return {
6024
6884
  harnessRoot: paths.harnessRoot,
6885
+ scanRoots: paths.scanRoots,
6025
6886
  dryRun: !retention.execute,
6026
6887
  execute: retention.execute,
6027
6888
  nodeModulesAgeMs: retention.nodeModulesAgeMs,
6028
6889
  worktreesAgeMs: retention.worktreesAgeMs,
6029
6890
  includeOrphans: retention.includeOrphans,
6891
+ diskPressure: retention.diskPressure,
6892
+ diskGate: retention.diskGate ? {
6893
+ ok: retention.diskGate.ok,
6894
+ path: retention.diskGate.path,
6895
+ freeBytes: retention.diskGate.freeBytes,
6896
+ usedPercent: retention.diskGate.usedPercent,
6897
+ reason: retention.diskGate.reason
6898
+ } : void 0,
6030
6899
  scannedAt: new Date(paths.now).toISOString(),
6031
6900
  finalizedRuns,
6032
6901
  actions,
@@ -6060,8 +6929,8 @@ function isPipelineCleanupEnabled() {
6060
6929
 
6061
6930
  // src/installed-package-versions.ts
6062
6931
  import { readFile } from "node:fs/promises";
6063
- import { homedir as homedir6 } from "node:os";
6064
- import path34 from "node:path";
6932
+ import { homedir as homedir7 } from "node:os";
6933
+ import path40 from "node:path";
6065
6934
  var MANAGED_PACKAGES = [
6066
6935
  "@kynver-app/runtime",
6067
6936
  "@kynver-app/openclaw-agent-os",
@@ -6075,13 +6944,13 @@ function unique(values) {
6075
6944
  return [...new Set(values.filter((value) => Boolean(value)))];
6076
6945
  }
6077
6946
  function moduleRoots() {
6078
- const home = homedir6();
6079
- const openClawPrefix = trim(process.env.KYNVER_OPENCLAW_NPM_ROOT) ?? trim(process.env.OPENCLAW_NPM_ROOT) ?? path34.join(home, ".openclaw", "npm");
6080
- const npmGlobalRoot = trim(process.env.KYNVER_NPM_GLOBAL_ROOT) ?? trim(process.env.KYNVER_NPM_GLOBAL_MODULES_ROOT) ?? (trim(process.env.NPM_CONFIG_PREFIX) ? path34.join(trim(process.env.NPM_CONFIG_PREFIX), "lib", "node_modules") : path34.join(home, ".npm-global", "lib", "node_modules"));
6947
+ const home = homedir7();
6948
+ const openClawPrefix = trim(process.env.KYNVER_OPENCLAW_NPM_ROOT) ?? trim(process.env.OPENCLAW_NPM_ROOT) ?? path40.join(home, ".openclaw", "npm");
6949
+ const npmGlobalRoot = trim(process.env.KYNVER_NPM_GLOBAL_ROOT) ?? trim(process.env.KYNVER_NPM_GLOBAL_MODULES_ROOT) ?? (trim(process.env.NPM_CONFIG_PREFIX) ? path40.join(trim(process.env.NPM_CONFIG_PREFIX), "lib", "node_modules") : path40.join(home, ".npm-global", "lib", "node_modules"));
6081
6950
  return unique([
6082
- path34.join(openClawPrefix, "lib", "node_modules"),
6083
- path34.join(openClawPrefix, "node_modules"),
6084
- npmGlobalRoot.endsWith("node_modules") ? npmGlobalRoot : path34.join(npmGlobalRoot, "lib", "node_modules")
6951
+ path40.join(openClawPrefix, "lib", "node_modules"),
6952
+ path40.join(openClawPrefix, "node_modules"),
6953
+ npmGlobalRoot.endsWith("node_modules") ? npmGlobalRoot : path40.join(npmGlobalRoot, "lib", "node_modules")
6085
6954
  ]);
6086
6955
  }
6087
6956
  async function readVersion(packageJsonPath) {
@@ -6097,7 +6966,7 @@ async function collectInstalledPackageVersions(observedAt = (/* @__PURE__ */ new
6097
6966
  const out = {};
6098
6967
  for (const packageName of MANAGED_PACKAGES) {
6099
6968
  for (const root of roots) {
6100
- const packageJsonPath = path34.join(root, packageName, "package.json");
6969
+ const packageJsonPath = path40.join(root, packageName, "package.json");
6101
6970
  const version = await readVersion(packageJsonPath);
6102
6971
  if (!version) continue;
6103
6972
  out[packageName] = { version, observedAt, path: packageJsonPath };
@@ -6113,7 +6982,7 @@ async function completeFinishedWorkers(runId, args) {
6113
6982
  const outcomes = [];
6114
6983
  for (const name of Object.keys(run.workers || {})) {
6115
6984
  const worker = readJson(
6116
- path35.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
6985
+ path41.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
6117
6986
  void 0
6118
6987
  );
6119
6988
  if (!worker?.taskId || worker.localOnly) continue;
@@ -6140,18 +7009,38 @@ async function completeFinishedWorkers(runId, args) {
6140
7009
  }
6141
7010
  return outcomes;
6142
7011
  }
6143
- async function postOperatorTick(agentOsId, runId, resourceGate, args) {
7012
+ async function postOperatorTick(agentOsId, runId, resourceGate, args, harnessCleanup) {
6144
7013
  const base = resolveBaseUrl(args.baseUrl ? String(args.baseUrl) : void 0);
6145
7014
  const secret = await resolveCallbackSecretWithMint(args.secret ? String(args.secret) : void 0, agentOsId, { baseUrl: base });
6146
7015
  const url = `${base}/api/agent-os/by-id/${encodeURIComponent(agentOsId)}/operator/tick`;
6147
7016
  const packageVersions = await collectInstalledPackageVersions();
7017
+ const activeHarnessWorkers = [];
7018
+ const run = loadRun(runId);
7019
+ for (const name of Object.keys(run.workers || {})) {
7020
+ const worker = readJson(
7021
+ path41.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
7022
+ void 0
7023
+ );
7024
+ if (!worker?.taskId) continue;
7025
+ activeHarnessWorkers.push({
7026
+ runId: run.id,
7027
+ workerName: name,
7028
+ taskId: worker.taskId,
7029
+ pid: worker.pid
7030
+ });
7031
+ }
6148
7032
  const res = await postJson(url, secret, {
6149
7033
  agentOsId,
6150
7034
  runId,
6151
7035
  ingestHarness: true,
6152
7036
  harnessBoardSnapshot: buildRunBoard(runId),
6153
7037
  resourceGate,
6154
- packageVersions
7038
+ boxResourceSnapshot: buildBoxResourceSnapshotFromGate(resourceGate, { harnessRunId: runId }),
7039
+ packageVersions,
7040
+ ...harnessCleanup ? { harnessCleanup } : {},
7041
+ runnerPresence: resolveRunnerPresencePayload({ runId }),
7042
+ activeHarnessWorkers,
7043
+ ...harnessCleanup ? { harnessCleanup } : {}
6155
7044
  });
6156
7045
  return { ok: res.ok, httpStatus: res.status, response: res.response };
6157
7046
  }
@@ -6165,12 +7054,12 @@ async function runPipelineTick(args) {
6165
7054
  runId,
6166
7055
  configuredMaxWorkersOverride: workspacePrefs?.maxConcurrentWorkers
6167
7056
  });
6168
- const operatorTick = await postOperatorTick(agentOsId, runId, resourceGate, args);
7057
+ const harnessCleanup = isPipelineCleanupEnabled() ? runPipelineHarnessCleanup(runId) : void 0;
7058
+ const operatorTick = await postOperatorTick(agentOsId, runId, resourceGate, args, harnessCleanup);
6169
7059
  const completionAckSync = syncCompletionAcknowledgedFromOperatorTick(runId, operatorTick);
6170
7060
  const leaseRenewal = await renewActiveTaskLeases(runId, args);
6171
7061
  const completedWorkers = await completeFinishedWorkers(runId, args);
6172
7062
  const staleReconcile = reconcileStaleWorkers();
6173
- const harnessCleanup = isPipelineCleanupEnabled() ? runPipelineHarnessCleanup(runId) : void 0;
6174
7063
  const planProgressSync = await syncActiveWorkerPlanProgress(runId, args);
6175
7064
  const maxStartsAdvice = resolvePipelineMaxStarts(resourceGate, operatorTick);
6176
7065
  let maxStarts = maxStartsAdvice.maxStarts;
@@ -6251,7 +7140,7 @@ async function runDaemon(args) {
6251
7140
  }
6252
7141
 
6253
7142
  // src/plan-progress.ts
6254
- import path36 from "node:path";
7143
+ import path42 from "node:path";
6255
7144
 
6256
7145
  // src/bounded-build/constants.ts
6257
7146
  var DEFAULT_BUILD_MEM_BUDGET_BYTES = 1536 * 1024 * 1024;
@@ -6501,7 +7390,8 @@ async function emitPlanProgress(args) {
6501
7390
  const secret = await resolveCallbackSecretWithMint(args.secret ? String(args.secret) : void 0, agentOsId, { baseUrl: base });
6502
7391
  const url = `${base}/api/agent-os/by-id/${encodeURIComponent(agentOsId)}/plans/${encodeURIComponent(planId)}/progress-events`;
6503
7392
  const cfg = loadUserConfig();
6504
- const provider = cfg.workerProvider ? `provider:${cfg.workerProvider}` : void 0;
7393
+ const workerProvider = resolveConfiguredWorkerProvider(cfg.workerProvider, DEFAULT_WORKER_PROVIDER);
7394
+ const provider = `provider:${workerProvider}`;
6505
7395
  const explicitProposed = args.proposed === true || args.proposed === "true" ? true : args.proposed === false || args.proposed === "false" ? false : void 0;
6506
7396
  const proposed = explicitProposed ?? (status !== "done" && (roleLane === "implementer" || roleLane === "repair_implementer"));
6507
7397
  const body = {
@@ -6537,7 +7427,7 @@ async function emitPlanProgress(args) {
6537
7427
  }
6538
7428
  function verifyPlanLocal(args) {
6539
7429
  const worktree = required(args.worktree ? String(args.worktree) : void 0, "worktree");
6540
- const cwd = path36.resolve(worktree);
7430
+ const cwd = path42.resolve(worktree);
6541
7431
  const summary = runHarnessVerifyCommands(cwd);
6542
7432
  const emitJson = args.json === true || args.json === "true";
6543
7433
  const payload = { passed: summary.passed, worktree: cwd, steps: summary.steps };
@@ -6586,9 +7476,9 @@ async function verifyPlan(args) {
6586
7476
  }
6587
7477
 
6588
7478
  // src/harness-verify-cli.ts
6589
- import path37 from "node:path";
7479
+ import path43 from "node:path";
6590
7480
  function runHarnessVerifyCli(args) {
6591
- const cwd = path37.resolve(required(args.worktree ? String(args.worktree) : void 0, "worktree"));
7481
+ const cwd = path43.resolve(required(args.worktree ? String(args.worktree) : void 0, "worktree"));
6592
7482
  const emitJson = args.json === true || args.json === "true" || args.emitJson === true || args.emitJson === "true";
6593
7483
  const commands = [];
6594
7484
  const rawCmd = args.command;
@@ -6632,7 +7522,7 @@ function runHarnessVerifyCli(args) {
6632
7522
  }
6633
7523
 
6634
7524
  // src/plan-persist-cli.ts
6635
- import { readFileSync as readFileSync9 } from "node:fs";
7525
+ import { readFileSync as readFileSync10 } from "node:fs";
6636
7526
  var OPERATIONS = ["create", "add_version", "update_metadata"];
6637
7527
  var FAILURE_KINDS = [
6638
7528
  "approval_guard",
@@ -6644,7 +7534,7 @@ var FAILURE_KINDS = [
6644
7534
  function readBodyArg(args) {
6645
7535
  const bodyFile = args.bodyFile ? String(args.bodyFile) : void 0;
6646
7536
  if (bodyFile) {
6647
- return { body: readFileSync9(bodyFile, "utf8"), bodyPathHint: bodyFile };
7537
+ return { body: readFileSync10(bodyFile, "utf8"), bodyPathHint: bodyFile };
6648
7538
  }
6649
7539
  const inline = args.body ? String(args.body) : void 0;
6650
7540
  if (inline) return { body: inline };
@@ -6733,8 +7623,62 @@ function runCleanupCli(args) {
6733
7623
  }
6734
7624
  }
6735
7625
 
7626
+ // src/harness-notice/harness-notice.parse.ts
7627
+ var MAX_DIAGNOSTIC_CHARS = 2400;
7628
+ function diagnosticJson(value, maxChars = MAX_DIAGNOSTIC_CHARS) {
7629
+ if (value === void 0 || value === null) return void 0;
7630
+ const raw = typeof value === "string" ? value : JSON.stringify(value, null, 2);
7631
+ const trimmed = raw.trim();
7632
+ if (!trimmed) return void 0;
7633
+ if (trimmed.length <= maxChars) return trimmed;
7634
+ return `${trimmed.slice(0, maxChars - 1).trimEnd()}\u2026`;
7635
+ }
7636
+
7637
+ // src/harness-notice/harness-notice.monitor-tick.ts
7638
+ function formatMonitorTickNotice(tick) {
7639
+ const lines = [];
7640
+ const monitorId = typeof tick.monitorId === "string" ? tick.monitorId : void 0;
7641
+ lines.push(
7642
+ monitorId ? `Harness monitor tick \xB7 ${tick.runId} (${monitorId})` : `Harness monitor tick \xB7 ${tick.runId}`
7643
+ );
7644
+ if (!tick.workers.length) {
7645
+ lines.push("No workers in scope for this poll.");
7646
+ return { primary: lines.join("\n"), diagnostic: diagnosticJson(tick) };
7647
+ }
7648
+ for (const view of tick.workers) {
7649
+ const auto = view.autoComplete.eligible ? "eligible for auto-complete" : "not auto-completing";
7650
+ const blockers = view.autoComplete.blockers.length > 0 ? ` (${view.autoComplete.blockers.slice(0, 2).join("; ")})` : "";
7651
+ lines.push(
7652
+ `\u2022 ${view.worker}: ${view.workerStatus}, ${view.health}${view.healthReason ? ` \u2014 ${view.healthReason}` : ""}; ${auto}${blockers}`
7653
+ );
7654
+ if (view.taskStatus) {
7655
+ lines.push(` Board task: ${view.taskStatus}${view.leaseOwner ? ` (lease: ${view.leaseOwner})` : ""}`);
7656
+ }
7657
+ }
7658
+ const completed = tick.autoCompleted?.filter((a) => a.outcome === "completed" && a.ok) ?? [];
7659
+ const blocked = tick.autoCompleted?.filter((a) => !a.ok && a.outcome !== "skipped") ?? [];
7660
+ if (completed.length) {
7661
+ lines.push(
7662
+ `Auto-completed: ${completed.map((c) => c.worker).join(", ")} \u2014 AgentOS completion should be posted.`
7663
+ );
7664
+ }
7665
+ if (blocked.length) {
7666
+ lines.push(
7667
+ `Auto-complete blocked: ${blocked.map((c) => `${c.worker}${c.reason ? ` (${c.reason})` : ""}`).join("; ")}`
7668
+ );
7669
+ }
7670
+ if (tick.leaseRenewal?.failed?.length) {
7671
+ lines.push(`Lease renew failed for: ${tick.leaseRenewal.failed.map((f) => f.worker).join(", ")}`);
7672
+ }
7673
+ const allDone = tick.workers.length > 0 && tick.workers.every((w) => w.autoComplete.terminalVerified) && (tick.autoCompleted?.every((a) => a.ok || a.outcome === "skipped") ?? true);
7674
+ lines.push(
7675
+ allDone ? "Next: monitor loop should stop \u2014 all workers terminal and handled." : "Next: monitor will poll again until workers are terminal-verified or max time elapses."
7676
+ );
7677
+ return { primary: lines.join("\n"), diagnostic: diagnosticJson(tick) };
7678
+ }
7679
+
6736
7680
  // src/monitor/monitor.service.ts
6737
- import path39 from "node:path";
7681
+ import path45 from "node:path";
6738
7682
 
6739
7683
  // src/monitor/monitor.classify.ts
6740
7684
  function expectedLeaseOwner(runId) {
@@ -6790,11 +7734,11 @@ function classifyWorkerHealth(input) {
6790
7734
  }
6791
7735
 
6792
7736
  // src/monitor/monitor.store.ts
6793
- import { existsSync as existsSync22, mkdirSync as mkdirSync6, readdirSync as readdirSync8, unlinkSync as unlinkSync2 } from "node:fs";
6794
- import path38 from "node:path";
7737
+ import { existsSync as existsSync26, mkdirSync as mkdirSync6, readdirSync as readdirSync9, unlinkSync as unlinkSync2 } from "node:fs";
7738
+ import path44 from "node:path";
6795
7739
  function monitorsDir() {
6796
7740
  const { harnessRoot } = getHarnessPaths();
6797
- const dir = path38.join(harnessRoot, "monitors");
7741
+ const dir = path44.join(harnessRoot, "monitors");
6798
7742
  mkdirSync6(dir, { recursive: true });
6799
7743
  return dir;
6800
7744
  }
@@ -6802,7 +7746,7 @@ function monitorIdFor(runId, workerName) {
6802
7746
  return workerName ? `${safeSlug(runId)}--${safeSlug(workerName)}` : safeSlug(runId);
6803
7747
  }
6804
7748
  function monitorPath(monitorId) {
6805
- return path38.join(monitorsDir(), `${monitorId}.json`);
7749
+ return path44.join(monitorsDir(), `${monitorId}.json`);
6806
7750
  }
6807
7751
  function loadMonitorSession(monitorId) {
6808
7752
  return readJson(monitorPath(monitorId), void 0);
@@ -6812,18 +7756,18 @@ function saveMonitorSession(session) {
6812
7756
  }
6813
7757
  function deleteMonitorSession(monitorId) {
6814
7758
  const file = monitorPath(monitorId);
6815
- if (!existsSync22(file)) return false;
7759
+ if (!existsSync26(file)) return false;
6816
7760
  unlinkSync2(file);
6817
7761
  return true;
6818
7762
  }
6819
7763
  function listMonitorSessions() {
6820
7764
  const dir = monitorsDir();
6821
- if (!existsSync22(dir)) return [];
7765
+ if (!existsSync26(dir)) return [];
6822
7766
  const entries = [];
6823
- for (const name of readdirSync8(dir)) {
7767
+ for (const name of readdirSync9(dir)) {
6824
7768
  if (!name.endsWith(".json")) continue;
6825
7769
  const session = readJson(
6826
- path38.join(dir, name),
7770
+ path44.join(dir, name),
6827
7771
  void 0
6828
7772
  );
6829
7773
  if (!session?.monitorId) continue;
@@ -6914,7 +7858,7 @@ async function fetchTaskLeasesForWorkers(input) {
6914
7858
  // src/monitor/monitor.service.ts
6915
7859
  function workerRecord2(runId, name) {
6916
7860
  return readJson(
6917
- path39.join(runDirectory(runId), "workers", safeSlug(name), "worker.json"),
7861
+ path45.join(runDirectory(runId), "workers", safeSlug(name), "worker.json"),
6918
7862
  void 0
6919
7863
  );
6920
7864
  }
@@ -7099,7 +8043,11 @@ async function runMonitorLoop(args) {
7099
8043
  autoComplete: args.autoComplete ?? true,
7100
8044
  renewLeases: args.renewLeases ?? true
7101
8045
  });
7102
- console.log(JSON.stringify({ monitorId, phase: "tick", ...tick }));
8046
+ const notice = formatMonitorTickNotice({ monitorId, phase: "tick", ...tick });
8047
+ console.log(notice.primary);
8048
+ if (notice.diagnostic) {
8049
+ console.error(`[monitor diagnostic] ${notice.diagnostic}`);
8050
+ }
7103
8051
  const allTerminal = tick.workers.length > 0 && tick.workers.every(
7104
8052
  (w) => w.autoComplete.terminalVerified && (w.autoComplete.eligible || w.autoComplete.blockers.some((b) => b.includes("already acknowledged")))
7105
8053
  );
@@ -7116,18 +8064,18 @@ async function runMonitorLoop(args) {
7116
8064
 
7117
8065
  // src/monitor/monitor-spawn.ts
7118
8066
  import { spawn as spawn4 } from "node:child_process";
7119
- import { closeSync as closeSync4, existsSync as existsSync23, openSync as openSync4 } from "node:fs";
7120
- import path40 from "node:path";
8067
+ import { closeSync as closeSync4, existsSync as existsSync27, openSync as openSync4 } from "node:fs";
8068
+ import path46 from "node:path";
7121
8069
  import { fileURLToPath as fileURLToPath3 } from "node:url";
7122
8070
  function resolveDefaultCliPath2() {
7123
- return path40.join(fileURLToPath3(new URL(".", import.meta.url)), "cli.js");
8071
+ return path46.join(fileURLToPath3(new URL(".", import.meta.url)), "cli.js");
7124
8072
  }
7125
8073
  function spawnMonitorSidecar(opts) {
7126
8074
  const cliPath = opts.cliPath ?? resolveDefaultCliPath2();
7127
- if (!existsSync23(cliPath)) return void 0;
8075
+ if (!existsSync27(cliPath)) return void 0;
7128
8076
  const monitorId = monitorIdFor(opts.runId, opts.workerName);
7129
8077
  const { harnessRoot } = getHarnessPaths();
7130
- const logPath = path40.join(harnessRoot, "monitors", `${monitorId}.log`);
8078
+ const logPath = path46.join(harnessRoot, "monitors", `${monitorId}.log`);
7131
8079
  let logFd;
7132
8080
  try {
7133
8081
  logFd = openSync4(logPath, "a");
@@ -7247,13 +8195,13 @@ async function monitorTickCli(args) {
7247
8195
  }
7248
8196
 
7249
8197
  // src/package-version.ts
7250
- import { existsSync as existsSync24, readFileSync as readFileSync10 } from "node:fs";
8198
+ import { existsSync as existsSync28, readFileSync as readFileSync11 } from "node:fs";
7251
8199
  import { dirname, join } from "node:path";
7252
8200
  import { fileURLToPath as fileURLToPath4 } from "node:url";
7253
8201
  function resolvePackageRoot(moduleUrl) {
7254
8202
  let dir = dirname(fileURLToPath4(moduleUrl));
7255
8203
  for (let depth = 0; depth < 6; depth += 1) {
7256
- if (existsSync24(join(dir, "package.json"))) return dir;
8204
+ if (existsSync28(join(dir, "package.json"))) return dir;
7257
8205
  const parent = dirname(dir);
7258
8206
  if (parent === dir) break;
7259
8207
  dir = parent;
@@ -7262,7 +8210,7 @@ function resolvePackageRoot(moduleUrl) {
7262
8210
  }
7263
8211
  function readOwnPackageVersion(moduleUrl = import.meta.url) {
7264
8212
  const pkgPath = join(resolvePackageRoot(moduleUrl), "package.json");
7265
- const pkg = JSON.parse(readFileSync10(pkgPath, "utf8"));
8213
+ const pkg = JSON.parse(readFileSync11(pkgPath, "utf8"));
7266
8214
  if (typeof pkg.version !== "string" || !pkg.version.trim()) {
7267
8215
  throw new Error(`Missing package.json version at ${pkgPath}`);
7268
8216
  }
@@ -7283,12 +8231,12 @@ function handleCliVersionFlag(argv, moduleUrl = import.meta.url, binName) {
7283
8231
  }
7284
8232
 
7285
8233
  // src/doctor/runtime-takeover.ts
7286
- import path42 from "node:path";
8234
+ import path48 from "node:path";
7287
8235
 
7288
8236
  // src/doctor/runtime-takeover.probes.ts
7289
- import { accessSync, constants, existsSync as existsSync25, readFileSync as readFileSync11 } from "node:fs";
7290
- import { homedir as homedir7 } from "node:os";
7291
- import path41 from "node:path";
8237
+ import { accessSync, constants, existsSync as existsSync29, readFileSync as readFileSync12 } from "node:fs";
8238
+ import { homedir as homedir8 } from "node:os";
8239
+ import path47 from "node:path";
7292
8240
  import { spawnSync as spawnSync6 } from "node:child_process";
7293
8241
  function captureCommand(bin, args) {
7294
8242
  try {
@@ -7317,7 +8265,7 @@ function tokenPrefix(token) {
7317
8265
  return trimmed.length <= 12 ? `${trimmed}\u2026` : `${trimmed.slice(0, 12)}\u2026`;
7318
8266
  }
7319
8267
  function isWritable(target) {
7320
- if (!existsSync25(target)) return false;
8268
+ if (!existsSync29(target)) return false;
7321
8269
  try {
7322
8270
  accessSync(target, constants.W_OK);
7323
8271
  return true;
@@ -7330,15 +8278,15 @@ var defaultRuntimeTakeoverProbes = {
7330
8278
  commandOnPath: (bin) => captureCommand(process.platform === "win32" ? "where" : "which", [bin]),
7331
8279
  kynverVersion: (bin) => captureCommand(bin, ["--version"]),
7332
8280
  loadConfig: () => loadUserConfig(),
7333
- configFilePath: () => path41.join(homedir7(), ".kynver", "config.json"),
7334
- credentialsFilePath: () => path41.join(homedir7(), ".kynver", "credentials"),
8281
+ configFilePath: () => path47.join(homedir8(), ".kynver", "config.json"),
8282
+ credentialsFilePath: () => path47.join(homedir8(), ".kynver", "credentials"),
7335
8283
  readCredentials: () => {
7336
- const credPath = path41.join(homedir7(), ".kynver", "credentials");
7337
- if (!existsSync25(credPath)) {
8284
+ const credPath = path47.join(homedir8(), ".kynver", "credentials");
8285
+ if (!existsSync29(credPath)) {
7338
8286
  return { hasApiKey: false };
7339
8287
  }
7340
8288
  try {
7341
- const parsed = JSON.parse(readFileSync11(credPath, "utf8"));
8289
+ const parsed = JSON.parse(readFileSync12(credPath, "utf8"));
7342
8290
  return {
7343
8291
  hasApiKey: Boolean(parsed.apiKey?.trim()),
7344
8292
  runnerTokenPrefix: tokenPrefix(parsed.runnerToken),
@@ -7365,8 +8313,8 @@ var defaultRuntimeTakeoverProbes = {
7365
8313
  })()
7366
8314
  }),
7367
8315
  harnessRoot: () => resolveHarnessRoot(),
7368
- legacyOpenclawHarnessRoot: () => path41.join(homedir7(), ".openclaw", "harness"),
7369
- pathExists: (target) => existsSync25(target),
8316
+ legacyOpenclawHarnessRoot: () => path47.join(homedir8(), ".openclaw", "harness"),
8317
+ pathExists: (target) => existsSync29(target),
7370
8318
  pathWritable: (target) => isWritable(target),
7371
8319
  vercelVersion: () => captureCommand("vercel", ["--version"]),
7372
8320
  vercelWhoami: () => captureCommand("vercel", ["whoami"])
@@ -7414,7 +8362,7 @@ function assessRuntimeTakeoverScheduler(env, ctx) {
7414
8362
  label: "Scheduler provider (runtime daemon vs OpenClaw cron)",
7415
8363
  status: "warn",
7416
8364
  summary: `OpenClaw local cron still active (${parts.join("; ")})`,
7417
- remediation: "On the Kynver deployment: set KYNVER_SCHEDULER_PROVIDER=qstash with QSTASH_TOKEN configured. On user runners: unset KYNVER_SCHEDULER_PROVIDER and OPENCLAW_CRON_STORE_PATH; set deploymentSchedulerProvider to qstash in ~/.kynver/config.json after Vercel env is updated.",
8365
+ remediation: "On the Kynver deployment: set KYNVER_SCHEDULER_PROVIDER=qstash with QSTASH_TOKEN configured. On user runners: unset KYNVER_SCHEDULER_PROVIDER and OPENCLAW_CRON_STORE_PATH; after Vercel env is updated run `kynver scheduler attest-cutover`.",
7418
8366
  details: schedulerDetails
7419
8367
  });
7420
8368
  }
@@ -7694,8 +8642,8 @@ function assessVercelCli(probes) {
7694
8642
  }
7695
8643
  function assessHarnessDirs(probes) {
7696
8644
  const harnessRoot = probes.harnessRoot();
7697
- const runsDir = path42.join(harnessRoot, "runs");
7698
- const worktreesDir = path42.join(harnessRoot, "worktrees");
8645
+ const runsDir = path48.join(harnessRoot, "runs");
8646
+ const worktreesDir = path48.join(harnessRoot, "worktrees");
7699
8647
  const displayHarnessRoot = redactHomePath(harnessRoot);
7700
8648
  const displayRunsDir = redactHomePath(runsDir);
7701
8649
  const displayWorktreesDir = redactHomePath(worktreesDir);
@@ -7883,6 +8831,131 @@ async function runCommandCenterContractCli(args) {
7883
8831
  console.log(JSON.stringify(res.response, null, 2));
7884
8832
  }
7885
8833
 
8834
+ // src/scheduler-cutover.ts
8835
+ var DEPLOYMENT_SCHEDULER_CUTOVER_STEPS = [
8836
+ "Vercel/hosted: set KYNVER_SCHEDULER_PROVIDER=qstash",
8837
+ "Vercel/hosted: ensure QSTASH_TOKEN (and QStash signing keys) are configured",
8838
+ "Vercel/hosted: unset OPENCLAW_CRON_STORE_PATH if present"
8839
+ ];
8840
+ var RUNNER_SCHEDULER_CUTOVER_STEPS = [
8841
+ "User runner: unset KYNVER_SCHEDULER_PROVIDER (scheduling is deployment-owned)",
8842
+ "User runner: unset OPENCLAW_CRON_STORE_PATH, OPENCLAW_CRON_SECRET, OPENCLAW_CRON_FIRE_BASE_URL",
8843
+ 'User runner: after deployment cutover, run `kynver scheduler attest-cutover` (or set deploymentSchedulerProvider to "qstash" in ~/.kynver/config.json)',
8844
+ "Verify: kynver doctor runtime-takeover \u2014 hotspot_openclaw_scheduler should pass"
8845
+ ];
8846
+ function readSchedulerCutoverEnv(env = process.env) {
8847
+ return {
8848
+ kynverSchedulerProvider: env.KYNVER_SCHEDULER_PROVIDER?.trim() || null,
8849
+ openclawCronStorePath: env.OPENCLAW_CRON_STORE_PATH?.trim() || null,
8850
+ openclawCronSecret: Boolean(env.OPENCLAW_CRON_SECRET?.trim()),
8851
+ openclawCronFireBaseUrl: env.OPENCLAW_CRON_FIRE_BASE_URL?.trim() || null
8852
+ };
8853
+ }
8854
+ function assessSchedulerCutover(config, env = readSchedulerCutoverEnv()) {
8855
+ const blockers = [];
8856
+ if (env.kynverSchedulerProvider === "openclaw-cron") {
8857
+ blockers.push("Runner still has KYNVER_SCHEDULER_PROVIDER=openclaw-cron");
8858
+ }
8859
+ if (env.openclawCronStorePath) {
8860
+ blockers.push("Runner still has OPENCLAW_CRON_STORE_PATH");
8861
+ }
8862
+ if (config.deploymentSchedulerProvider === "openclaw-cron") {
8863
+ blockers.push("~/.kynver/config.json deploymentSchedulerProvider is still openclaw-cron");
8864
+ }
8865
+ return {
8866
+ ok: blockers.length === 0,
8867
+ blockers,
8868
+ runnerEnv: env,
8869
+ deploymentSchedulerProvider: config.deploymentSchedulerProvider ?? null,
8870
+ deploymentSteps: [...DEPLOYMENT_SCHEDULER_CUTOVER_STEPS],
8871
+ runnerSteps: [...RUNNER_SCHEDULER_CUTOVER_STEPS]
8872
+ };
8873
+ }
8874
+ function applySchedulerCutoverAttestation(config) {
8875
+ return {
8876
+ ...config,
8877
+ deploymentSchedulerProvider: "qstash"
8878
+ };
8879
+ }
8880
+
8881
+ // src/scheduler-cutover-cli.ts
8882
+ import path49 from "node:path";
8883
+ import { homedir as homedir9 } from "node:os";
8884
+ var CONFIG_FILE2 = path49.join(homedir9(), ".kynver", "config.json");
8885
+ function runSchedulerCutoverCheckCli(json = false) {
8886
+ const config = loadUserConfig();
8887
+ const report = assessSchedulerCutover(config);
8888
+ const payload = {
8889
+ ...report,
8890
+ configPath: displayUserPath(CONFIG_FILE2),
8891
+ configAttestationExample: { deploymentSchedulerProvider: "qstash" }
8892
+ };
8893
+ if (json) {
8894
+ console.log(JSON.stringify(payload, null, 2));
8895
+ if (!report.ok) process.exitCode = 1;
8896
+ return;
8897
+ }
8898
+ console.log("AgentOS scheduler provider cutover checklist\n");
8899
+ console.log("Deployment (Vercel):");
8900
+ for (const step of DEPLOYMENT_SCHEDULER_CUTOVER_STEPS) console.log(` - ${step}`);
8901
+ console.log("\nUser runner:");
8902
+ for (const step of RUNNER_SCHEDULER_CUTOVER_STEPS) console.log(` - ${step}`);
8903
+ console.log("\nThis host:");
8904
+ console.log(` config: ${payload.configPath}`);
8905
+ console.log(
8906
+ ` deploymentSchedulerProvider: ${report.deploymentSchedulerProvider ?? "(unset)"}`
8907
+ );
8908
+ console.log(
8909
+ ` KYNVER_SCHEDULER_PROVIDER: ${report.runnerEnv.kynverSchedulerProvider ?? "(unset)"}`
8910
+ );
8911
+ console.log(
8912
+ ` OPENCLAW_CRON_STORE_PATH: ${report.runnerEnv.openclawCronStorePath ?? "(unset)"}`
8913
+ );
8914
+ if (report.blockers.length) {
8915
+ console.log("\nBlockers:");
8916
+ for (const b of report.blockers) console.log(` ! ${b}`);
8917
+ process.exitCode = 1;
8918
+ return;
8919
+ }
8920
+ console.log("\nNo local blockers detected on this runner.");
8921
+ }
8922
+ function runSchedulerAttestCutoverCli(json = false) {
8923
+ const existing = loadUserConfig();
8924
+ const report = assessSchedulerCutover(existing);
8925
+ if (!report.ok) {
8926
+ const payload2 = {
8927
+ ok: false,
8928
+ attested: false,
8929
+ blockers: report.blockers,
8930
+ remediation: "Clear local OpenClaw scheduler blockers before attesting qstash cutover."
8931
+ };
8932
+ if (json) {
8933
+ console.log(JSON.stringify(payload2, null, 2));
8934
+ } else {
8935
+ console.error("Cannot attest scheduler cutover \u2014 local blockers remain:");
8936
+ for (const b of report.blockers) console.error(` ! ${b}`);
8937
+ }
8938
+ process.exitCode = 1;
8939
+ return;
8940
+ }
8941
+ const next = applySchedulerCutoverAttestation(existing);
8942
+ saveUserConfig(next);
8943
+ const payload = {
8944
+ ok: true,
8945
+ attested: true,
8946
+ configPath: displayUserPath(CONFIG_FILE2),
8947
+ deploymentSchedulerProvider: "qstash",
8948
+ config: presentUserConfig(next),
8949
+ note: "Recorded deploymentSchedulerProvider=qstash in ~/.kynver/config.json. Confirm Vercel has KYNVER_SCHEDULER_PROVIDER=qstash and QSTASH_TOKEN before relying on hosted schedules."
8950
+ };
8951
+ if (json) {
8952
+ console.log(JSON.stringify(payload, null, 2));
8953
+ return;
8954
+ }
8955
+ console.log(payload.note);
8956
+ console.log(` config: ${payload.configPath}`);
8957
+ }
8958
+
7886
8959
  // src/cli.ts
7887
8960
  function isHelpFlag(arg) {
7888
8961
  return arg === "help" || arg === "--help" || arg === "-h";
@@ -7930,6 +9003,8 @@ function usage(code = 0) {
7930
9003
  " kynver monitor auto-complete --run RUN_ID --name worker [--agent-os-id AOS_ID] [--base-url URL] [--secret SECRET]",
7931
9004
  " kynver monitor run-loop --run RUN_ID --monitor-id ID [--name worker] [--agent-os-id AOS_ID] [--poll-ms MS] [--auto-complete] [--renew-leases]",
7932
9005
  " kynver doctor runtime-takeover",
9006
+ " kynver scheduler cutover-check [--json]",
9007
+ " kynver scheduler attest-cutover [--json]",
7933
9008
  " kynver board contract [--agent-os-id ID] [--base-url URL] [--since ISO] [--limit N]"
7934
9009
  ].join("\n")
7935
9010
  );
@@ -7941,7 +9016,7 @@ async function main(argv = process.argv.slice(2)) {
7941
9016
  const scope = argv.shift();
7942
9017
  let action;
7943
9018
  let rest;
7944
- if (scope === "run" || scope === "worker" || scope === "plan" || scope === "runner" || scope === "harness" || scope === "monitor" || scope === "doctor" || scope === "board") {
9019
+ if (scope === "run" || scope === "worker" || scope === "plan" || scope === "runner" || scope === "harness" || scope === "monitor" || scope === "doctor" || scope === "scheduler" || scope === "board") {
7945
9020
  action = argv.shift();
7946
9021
  rest = argv;
7947
9022
  } else {
@@ -7968,6 +9043,12 @@ async function main(argv = process.argv.slice(2)) {
7968
9043
  }
7969
9044
  if (scope === "cleanup") return runCleanupCli(args);
7970
9045
  if (scope === "doctor" && action === "runtime-takeover") return runRuntimeTakeoverDoctorCli();
9046
+ if (scope === "scheduler" && action === "cutover-check") {
9047
+ return runSchedulerCutoverCheckCli(args.json === true);
9048
+ }
9049
+ if (scope === "scheduler" && action === "attest-cutover") {
9050
+ return runSchedulerAttestCutoverCli(args.json === true);
9051
+ }
7971
9052
  if (scope === "board" && action === "contract") {
7972
9053
  return void await runCommandCenterContractCli(args);
7973
9054
  }