@kynver-app/runtime 0.1.62 → 0.1.69

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 +4 -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/index.js CHANGED
@@ -120,10 +120,6 @@ function tailFile(file, lines) {
120
120
  function readMaybeFile(file) {
121
121
  return file ? readFileSync2(path.resolve(file), "utf8") : "";
122
122
  }
123
- function listRunIds(runsDir) {
124
- if (!existsSync2(runsDir)) return [];
125
- return readdirSync(runsDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
126
- }
127
123
  function sleepMs(ms) {
128
124
  Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
129
125
  }
@@ -661,7 +657,7 @@ async function runSetup(args) {
661
657
  ...existing,
662
658
  ...inferSetupFields(existing, args),
663
659
  ...maxWorkersRaw ? { maxConcurrentWorkers: Math.max(1, Math.floor(Number(maxWorkersRaw))) } : {},
664
- workerProvider: typeof args.provider === "string" ? args.provider : existing.workerProvider || "claude"
660
+ workerProvider: typeof args.provider === "string" ? args.provider : existing.workerProvider || "cursor"
665
661
  });
666
662
  saveUserConfig(config);
667
663
  let runnerCredentialNote;
@@ -780,23 +776,23 @@ function isWslHost() {
780
776
  function observeWslHostDisk(options = {}) {
781
777
  const wsl = options.forceWsl === void 0 ? isWslHost() : options.forceWsl;
782
778
  if (!wsl) return null;
783
- const path44 = options.wslHostMount?.trim() || process.env.KYNVER_WSL_HOST_MOUNT?.trim() || DEFAULT_WSL_HOST_MOUNT;
779
+ const path51 = options.wslHostMount?.trim() || process.env.KYNVER_WSL_HOST_MOUNT?.trim() || DEFAULT_WSL_HOST_MOUNT;
784
780
  const warnBelowBytes = options.wslHostFreeWarnBytes ?? DEFAULT_WSL_HOST_WARN_FREE_BYTES;
785
781
  const criticalBelowBytes = options.wslHostFreeCriticalBytes ?? DEFAULT_WSL_HOST_CRITICAL_FREE_BYTES;
786
782
  const statfs = options.statfs ?? statfsSync;
787
783
  let stats;
788
784
  try {
789
- stats = statfs(path44);
785
+ stats = statfs(path51);
790
786
  } catch (error) {
791
787
  return {
792
788
  ok: false,
793
- path: path44,
789
+ path: path51,
794
790
  freeBytes: 0,
795
791
  totalBytes: 0,
796
792
  usedPercent: 100,
797
793
  warnBelowBytes,
798
794
  criticalBelowBytes,
799
- reason: `Windows host disk probe failed at ${path44}: ${error.message}`,
795
+ reason: `Windows host disk probe failed at ${path51}: ${error.message}`,
800
796
  probeError: error.message
801
797
  };
802
798
  }
@@ -810,11 +806,11 @@ function observeWslHostDisk(options = {}) {
810
806
  let reason = null;
811
807
  if (!ok) {
812
808
  const tag = criticalFree ? "critical" : "warning";
813
- reason = `Windows host disk ${path44} at ${tag}: ${freeGiB} GiB free (<${(criticalFree ? criticalBelowBytes : warnBelowBytes) / 1024 / 1024 / 1024} GiB); WSL VHDX cannot grow safely. ${summarizeWslRecoverySteps()}`;
809
+ reason = `Windows host disk ${path51} at ${tag}: ${freeGiB} GiB free (<${(criticalFree ? criticalBelowBytes : warnBelowBytes) / 1024 / 1024 / 1024} GiB); WSL VHDX cannot grow safely. ${summarizeWslRecoverySteps()}`;
814
810
  }
815
811
  return {
816
812
  ok,
817
- path: path44,
813
+ path: path51,
818
814
  freeBytes,
819
815
  totalBytes,
820
816
  usedPercent,
@@ -834,12 +830,12 @@ var DEFAULT_CRITICAL_FREE_BYTES = 15 * 1024 * 1024 * 1024;
834
830
  var DEFAULT_MAX_USED_PERCENT = 80;
835
831
  var DEFAULT_HARD_MAX_USED_PERCENT = 90;
836
832
  function observeRunnerDiskGate(input = {}) {
837
- const path44 = input.diskPath?.trim() || "/";
833
+ const path51 = input.diskPath?.trim() || "/";
838
834
  const warnBelowBytes = input.diskFreeWarnBytes ?? DEFAULT_WARN_FREE_BYTES;
839
835
  const criticalBelowBytes = input.diskFreeCriticalBytes ?? DEFAULT_CRITICAL_FREE_BYTES;
840
836
  const maxUsedPercent = input.diskMaxUsedPercent ?? DEFAULT_MAX_USED_PERCENT;
841
837
  const hardMaxUsedPercent = input.diskHardMaxUsedPercent ?? DEFAULT_HARD_MAX_USED_PERCENT;
842
- const stats = statfsSync2(path44);
838
+ const stats = statfsSync2(path51);
843
839
  const freeBytes = Number(stats.bavail) * Number(stats.bsize);
844
840
  const totalBytes = Number(stats.blocks) * Number(stats.bsize);
845
841
  const usedPercent = totalBytes > 0 ? (totalBytes - freeBytes) / totalBytes * 100 : 100;
@@ -862,7 +858,7 @@ function observeRunnerDiskGate(input = {}) {
862
858
  }
863
859
  return {
864
860
  ok,
865
- path: path44,
861
+ path: path51,
866
862
  freeBytes,
867
863
  totalBytes,
868
864
  usedPercent,
@@ -942,6 +938,12 @@ function loadRun(id) {
942
938
  }
943
939
  function listRunRecords() {
944
940
  const { runsDir } = getPaths();
941
+ return listRunRecordsAt(runsDir);
942
+ }
943
+ function listRunRecordsForHarnessRoot(harnessRoot) {
944
+ return listRunRecordsAt(path6.join(harnessRoot, "runs"));
945
+ }
946
+ function listRunRecordsAt(runsDir) {
945
947
  if (!existsSync7(runsDir)) return [];
946
948
  const runs = [];
947
949
  for (const entry of readdirSync2(runsDir, { withFileTypes: true })) {
@@ -970,7 +972,10 @@ function saveWorker(runId, worker) {
970
972
  }
971
973
  function runDirectory(id) {
972
974
  const { runsDir } = getPaths();
973
- return runDir(runsDir, safeSlug(id));
975
+ return runDirectoryAt(runsDir, id);
976
+ }
977
+ function runDirectoryAt(harnessRoot, id) {
978
+ return runDir(path6.join(harnessRoot, "runs"), safeSlug(id));
974
979
  }
975
980
 
976
981
  // src/heartbeat.ts
@@ -991,7 +996,9 @@ function parseHeartbeat(file) {
991
996
  lastHeartbeatPhase: null,
992
997
  lastHeartbeatSummary: null,
993
998
  heartbeatBlocker: null,
994
- timestampAnomalies: []
999
+ timestampAnomalies: [],
1000
+ lastBoxResourceSnapshot: null,
1001
+ lastPrEvidence: []
995
1002
  };
996
1003
  if (!existsSync8(file)) return result;
997
1004
  const maxFutureMs = Date.now() + HEARTBEAT_FUTURE_SKEW_MS;
@@ -1018,6 +1025,14 @@ function parseHeartbeat(file) {
1018
1025
  if (row.phase !== void 0 && row.phase !== null) result.lastHeartbeatPhase = String(row.phase);
1019
1026
  if (row.summary !== void 0 && row.summary !== null) result.lastHeartbeatSummary = String(row.summary);
1020
1027
  result.heartbeatBlocker = row.blocker ? String(row.blocker) : null;
1028
+ if (row.boxResourceSnapshot && typeof row.boxResourceSnapshot === "object" && !Array.isArray(row.boxResourceSnapshot)) {
1029
+ result.lastBoxResourceSnapshot = row.boxResourceSnapshot;
1030
+ }
1031
+ if (Array.isArray(row.prEvidence)) {
1032
+ result.lastPrEvidence = row.prEvidence.filter(
1033
+ (entry2) => !!entry2 && typeof entry2 === "object" && typeof entry2.prUrl === "string"
1034
+ );
1035
+ }
1021
1036
  }
1022
1037
  return result;
1023
1038
  }
@@ -1506,8 +1521,8 @@ function normalizePrUrl(url) {
1506
1521
  }
1507
1522
  function parseReconciliation(finalResult) {
1508
1523
  if (!finalResult || typeof finalResult !== "object" || Array.isArray(finalResult)) return [];
1509
- const record = finalResult;
1510
- const raw = record.targetPrReconciliation ?? record.target_pr_reconciliation;
1524
+ const record3 = finalResult;
1525
+ const raw = record3.targetPrReconciliation ?? record3.target_pr_reconciliation;
1511
1526
  if (!Array.isArray(raw)) return [];
1512
1527
  const out = [];
1513
1528
  for (const item of raw) {
@@ -1538,10 +1553,35 @@ function workerPrUrls(snapshot, finalResult) {
1538
1553
  function assessWorkerLandingContract(input) {
1539
1554
  const { contract, snapshot } = input;
1540
1555
  const finalResult = input.finalResult ?? snapshot.finalResult;
1541
- if (!contract.landingOnly && contract.targetPrUrls.length === 0) {
1556
+ if (!contract.landingOnly && contract.targetPrUrls.length === 0 && !contract.repairEnforceOriginalPr) {
1542
1557
  return { blocked: false };
1543
1558
  }
1544
1559
  if (!hasFinalResult3(finalResult)) return { blocked: false };
1560
+ const repairTarget = contract.repairEnforceOriginalPr ? normalizePrUrl(trimOrNull3(contract.targetPrUrl) ?? "") ?? (contract.targetPrUrls.length === 1 ? normalizePrUrl(contract.targetPrUrls[0]) : null) : null;
1561
+ if (repairTarget) {
1562
+ const workerPrs2 = workerPrUrls(snapshot, finalResult);
1563
+ const supersedes = finalResult && typeof finalResult === "object" && !Array.isArray(finalResult) && finalResult.supersedesOriginalTargetPr === true;
1564
+ if (!supersedes) {
1565
+ for (const pr of workerPrs2) {
1566
+ if (pr !== repairTarget) {
1567
+ return {
1568
+ blocked: true,
1569
+ reason: "duplicate_repair_pr",
1570
+ detail: `Repair worker opened or attached PR ${pr} instead of canonical target ${repairTarget}`
1571
+ };
1572
+ }
1573
+ }
1574
+ }
1575
+ const reconciliation2 = parseReconciliation(finalResult);
1576
+ const entry = reconciliation2.find((r) => r.prUrl === repairTarget);
1577
+ if (!entry || entry.outcome !== "merged" && !(entry.reason?.trim() && (entry.outcome === "skipped" || entry.outcome === "blocked"))) {
1578
+ return {
1579
+ blocked: true,
1580
+ reason: "missing_repair_target_reconciliation",
1581
+ detail: `Repair worker must reconcile target PR ${repairTarget}`
1582
+ };
1583
+ }
1584
+ }
1545
1585
  const reconciliation = parseReconciliation(finalResult);
1546
1586
  const byUrl = new Map(reconciliation.map((r) => [r.prUrl, r]));
1547
1587
  const targetSet = new Set(
@@ -1696,6 +1736,12 @@ function computeWorkerStatus(worker, options = {}) {
1696
1736
  ]);
1697
1737
  const error = parsed.error || (!alive && !finalResult ? tailFile(worker.stderrPath, 10).trim() || void 0 : void 0);
1698
1738
  const completionBlocker = typeof worker.completionBlocker === "string" && worker.completionBlocker.trim() ? worker.completionBlocker.trim() : null;
1739
+ const landingContract = worker.repairTargetPrUrl ? {
1740
+ landingOnly: false,
1741
+ targetPrUrls: [worker.repairTargetPrUrl],
1742
+ targetPrUrl: worker.repairTargetPrUrl,
1743
+ repairEnforceOriginalPr: true
1744
+ } : null;
1699
1745
  const attention = computeAttention({
1700
1746
  alive,
1701
1747
  finalResult,
@@ -1708,7 +1754,9 @@ function computeWorkerStatus(worker, options = {}) {
1708
1754
  error,
1709
1755
  changedFiles,
1710
1756
  gitAncestry,
1711
- completionBlocker
1757
+ completionBlocker,
1758
+ landingContract,
1759
+ prUrl: worker.repairTargetPrUrl ?? worker.taskPrUrl ?? null
1712
1760
  });
1713
1761
  const workerStatusLabel = completionBlocker || attention.state === "blocked" ? "blocked" : completionAcknowledged || attention.state === "done" ? "done" : finalResult ? "exited" : alive ? "running" : "exited";
1714
1762
  return {
@@ -1830,9 +1878,16 @@ function observeRunnerResourceGate(input) {
1830
1878
  const slotsByCapacity = Math.max(0, maxConcurrentWorkers - activeWorkers);
1831
1879
  const slotsByFreeMem = capacityFromFree;
1832
1880
  let slotsAvailable = Math.min(slotsByCapacity, slotsByFreeMem);
1881
+ const skipDisk = input.skipDiskGate || process.env.KYNVER_RESOURCE_GATE_SKIP_DISK === "1";
1882
+ const diskGate = skipDisk ? void 0 : observeRunnerDiskGate({
1883
+ diskPath: input.diskPath?.trim() || process.env.KYNVER_DISK_GUARD_PATH?.trim() || "/"
1884
+ });
1885
+ if (diskGate && !diskGate.ok) slotsAvailable = 0;
1833
1886
  let reason = null;
1834
1887
  if (slotsAvailable <= 0) {
1835
- if (activeWorkers >= maxConcurrentWorkers) {
1888
+ if (diskGate && !diskGate.ok) {
1889
+ reason = diskGate.reason ?? "disk gate blocked worker admission";
1890
+ } else if (activeWorkers >= maxConcurrentWorkers) {
1836
1891
  reason = `at worker limit (${activeWorkers}/${maxConcurrentWorkers} running)`;
1837
1892
  } else if (capacityFromFree <= 0) {
1838
1893
  reason = "insufficient free memory \u2014 waiting for workers to finish";
@@ -1852,7 +1907,8 @@ function observeRunnerResourceGate(input) {
1852
1907
  maxConcurrentWorkers,
1853
1908
  activeWorkers,
1854
1909
  slotsAvailable,
1855
- reason
1910
+ reason,
1911
+ ...diskGate ? { diskGate } : {}
1856
1912
  };
1857
1913
  }
1858
1914
 
@@ -2040,10 +2096,86 @@ var claudeProvider = {
2040
2096
  }
2041
2097
  };
2042
2098
 
2099
+ // src/worker-provider-policy.ts
2100
+ var DEFAULT_WORKER_PROVIDER = "cursor";
2101
+ var CLAUDE_FAMILY = /* @__PURE__ */ new Set(["claude", "opus", "anthropic"]);
2102
+ var TASK_OVERRIDE_MARKERS = [
2103
+ /\[worker-provider:\s*claude\]/i,
2104
+ /\[use-claude-worker\]/i,
2105
+ /\[operator-worker-provider:\s*claude\]/i
2106
+ ];
2107
+ function taskString2(task, key) {
2108
+ const v = task[key];
2109
+ return typeof v === "string" ? v.trim() : "";
2110
+ }
2111
+ function isClaudeFamilyProvider(provider) {
2112
+ if (!provider?.trim()) return false;
2113
+ const normalized = provider.trim().toLowerCase();
2114
+ if (CLAUDE_FAMILY.has(normalized)) return true;
2115
+ return normalized.includes("claude") || normalized.includes("opus");
2116
+ }
2117
+ function taskAllowsClaudeWorker(task) {
2118
+ if (!task) return false;
2119
+ const override = task.workerProviderOverride;
2120
+ if (typeof override === "string" && isClaudeFamilyProvider(override)) return true;
2121
+ const ref = taskString2(task, "executorRef").toLowerCase();
2122
+ if (ref === "provider:claude" || ref.startsWith("provider:claude:")) return true;
2123
+ if (ref.includes("claude-worker-override") || ref.includes("operator-claude")) return true;
2124
+ const description = taskString2(task, "description");
2125
+ if (TASK_OVERRIDE_MARKERS.some((re) => re.test(description))) return true;
2126
+ const title = taskString2(task, "title");
2127
+ if (/\[use-claude-worker\]/i.test(title)) return true;
2128
+ return false;
2129
+ }
2130
+ function coerceCursorModel(model, ruleSuffix) {
2131
+ const coerced = {
2132
+ provider: DEFAULT_WORKER_PROVIDER,
2133
+ model: CURSOR_DEFAULT_MODEL,
2134
+ rule: `policy:cursor_default${ruleSuffix}`,
2135
+ requestedModel: model
2136
+ };
2137
+ return coerced;
2138
+ }
2139
+ function enforceCursorWorkerProvider(input) {
2140
+ const { routing, task } = input;
2141
+ const explicit = input.explicitProvider?.trim().toLowerCase();
2142
+ if (input.explicitProviderIsOperatorOverride && isClaudeFamilyProvider(explicit)) {
2143
+ return {
2144
+ ...routing,
2145
+ provider: "claude",
2146
+ rule: routing.rule.startsWith("explicit:") ? routing.rule : "explicit:operator_provider"
2147
+ };
2148
+ }
2149
+ if (taskAllowsClaudeWorker(task)) {
2150
+ return routing;
2151
+ }
2152
+ if (!isClaudeFamilyProvider(routing.provider)) {
2153
+ return routing;
2154
+ }
2155
+ const suffix = routing.rule && routing.rule !== "default:global" ? `:${routing.rule.replace(/:/g, "_")}` : "";
2156
+ return coerceCursorModel(routing.model, suffix);
2157
+ }
2158
+ function resolveConfiguredWorkerProvider(configured, fallback = DEFAULT_WORKER_PROVIDER) {
2159
+ const trimmed = configured?.trim();
2160
+ if (!trimmed) return fallback;
2161
+ if (isClaudeFamilyProvider(trimmed)) return DEFAULT_WORKER_PROVIDER;
2162
+ return trimmed;
2163
+ }
2164
+ function preferCursorExecutor(executors) {
2165
+ const unique2 = [...new Set(executors.map((e) => e.trim().toLowerCase()).filter(Boolean))];
2166
+ if (unique2.includes(DEFAULT_WORKER_PROVIDER)) {
2167
+ return [...new Set(unique2.map((e) => isClaudeFamilyProvider(e) ? DEFAULT_WORKER_PROVIDER : e))];
2168
+ }
2169
+ if (unique2.every((e) => isClaudeFamilyProvider(e))) {
2170
+ return [DEFAULT_WORKER_PROVIDER];
2171
+ }
2172
+ return unique2;
2173
+ }
2174
+
2043
2175
  // src/model-routing.ts
2044
2176
  var GLOBAL_DEFAULT_MODEL = "claude-sonnet-4-6";
2045
2177
  var CURSOR_DEFAULT_MODEL = "composer-2.5";
2046
- function taskString2(task, key) {
2178
+ function taskString3(task, key) {
2047
2179
  const v = task[key];
2048
2180
  return typeof v === "string" ? v.trim() : "";
2049
2181
  }
@@ -2059,11 +2191,14 @@ function resolveGlobalDefaultModel(config = loadUserConfig()) {
2059
2191
  }
2060
2192
  function inferProviderFromModel(model) {
2061
2193
  const m = (model ?? "").toLowerCase();
2062
- if (!m) return "claude";
2194
+ if (!m) return "cursor";
2063
2195
  if (m.includes("composer") || m.includes("cursor") || m.includes("codex") || m.startsWith("gpt-") || m.startsWith("gpt5")) {
2064
2196
  return "cursor";
2065
2197
  }
2066
- return "claude";
2198
+ if (/^claude[-_]/i.test(m) || /^(?:opus|sonnet|haiku)\b/i.test(m)) {
2199
+ return "claude";
2200
+ }
2201
+ return "cursor";
2067
2202
  }
2068
2203
  function normalizeProviderAliasModel(model, explicitProvider) {
2069
2204
  const alias = model.trim().toLowerCase();
@@ -2097,41 +2232,33 @@ function isOpusLane(ref, title) {
2097
2232
  return false;
2098
2233
  }
2099
2234
  function inferModelRoutingFromTask(task) {
2100
- const ref = normalizeRef(taskString2(task, "executorRef"));
2101
- const title = taskString2(task, "title").toLowerCase();
2102
- const priority = taskString2(task, "priority") || "normal";
2103
- const roleLane = normalizeRef(taskString2(task, "roleLane"));
2235
+ const ref = normalizeRef(taskString3(task, "executorRef"));
2236
+ const title = taskString3(task, "title").toLowerCase();
2237
+ const priority = taskString3(task, "priority") || "normal";
2238
+ const roleLane = normalizeRef(taskString3(task, "roleLane"));
2104
2239
  if (ref.includes("cursor") || ref.includes("codex") || ref.includes("composer") || ref.includes("copilot") || roleLane === "implementer" || roleLane === "repair_implementer") {
2105
2240
  return { provider: "cursor", rule: "lane:implementation" };
2106
2241
  }
2107
2242
  if (ref.includes("landing") || title.startsWith("land:") || title.includes(" merge")) {
2108
- return {
2109
- model: "claude-haiku-4-5-20251001",
2110
- provider: "claude",
2111
- rule: "lane:landing"
2112
- };
2243
+ return { provider: "cursor", rule: "lane:landing" };
2113
2244
  }
2114
2245
  if (ref.includes("review") || /^review[\s:]/.test(title) || roleLane.includes("review")) {
2115
2246
  if (isOpusLane(ref, title) || roleLane === "deep_reviewer") {
2116
- return { model: "claude-opus-4-7", provider: "claude", rule: "lane:deep_review" };
2247
+ return { provider: "cursor", rule: "lane:deep_review" };
2117
2248
  }
2118
- return { model: "claude-sonnet-4-6", provider: "claude", rule: "lane:review" };
2249
+ return { provider: "cursor", rule: "lane:review" };
2119
2250
  }
2120
2251
  if (isOpusLane(ref, title) || roleLane === "plan_author") {
2121
- return { model: "claude-opus-4-7", provider: "claude", rule: "lane:planning" };
2252
+ return { provider: "cursor", rule: "lane:planning" };
2122
2253
  }
2123
2254
  if (priority === "critical") {
2124
- return { model: "claude-opus-4-7", provider: "claude", rule: "priority:critical" };
2255
+ return { provider: "cursor", rule: "priority:critical" };
2125
2256
  }
2126
2257
  if (priority === "high") {
2127
- return { model: "claude-sonnet-4-6", provider: "claude", rule: "priority:high" };
2258
+ return { provider: "cursor", rule: "priority:high" };
2128
2259
  }
2129
2260
  if (priority === "low") {
2130
- return {
2131
- model: "claude-haiku-4-5-20251001",
2132
- provider: "claude",
2133
- rule: "priority:low"
2134
- };
2261
+ return { provider: "cursor", rule: "priority:low" };
2135
2262
  }
2136
2263
  const model = resolveGlobalDefaultModel();
2137
2264
  return {
@@ -2141,31 +2268,41 @@ function inferModelRoutingFromTask(task) {
2141
2268
  };
2142
2269
  }
2143
2270
  function resolveWorkerLaunch(input) {
2271
+ let decision;
2144
2272
  if (input.explicitModel?.trim()) {
2145
- const model2 = input.explicitModel.trim();
2146
- const providerAlias = normalizeProviderAliasModel(model2, input.explicitProvider);
2147
- if (providerAlias) return providerAlias;
2148
- return {
2149
- model: model2,
2150
- provider: input.explicitProvider?.trim() || inferProviderFromModel(model2),
2151
- rule: "explicit:cli",
2152
- requestedModel: model2
2153
- };
2154
- }
2155
- if (input.task && Object.keys(input.task).length > 0) {
2273
+ const model = input.explicitModel.trim();
2274
+ const providerAlias = normalizeProviderAliasModel(model, input.explicitProvider);
2275
+ if (providerAlias) {
2276
+ decision = providerAlias;
2277
+ } else {
2278
+ decision = {
2279
+ model,
2280
+ provider: input.explicitProvider?.trim() || inferProviderFromModel(model),
2281
+ rule: "explicit:cli",
2282
+ requestedModel: model
2283
+ };
2284
+ }
2285
+ } else if (input.task && Object.keys(input.task).length > 0) {
2156
2286
  const inferred = inferModelRoutingFromTask(input.task);
2157
- return {
2287
+ decision = {
2158
2288
  ...inferred,
2159
2289
  requestedModel: inferred.model
2160
2290
  };
2291
+ } else {
2292
+ const model = resolveGlobalDefaultModel();
2293
+ decision = {
2294
+ model,
2295
+ provider: input.explicitProvider?.trim() || inferProviderFromModel(model),
2296
+ rule: "default:global",
2297
+ requestedModel: model
2298
+ };
2161
2299
  }
2162
- const model = resolveGlobalDefaultModel();
2163
- return {
2164
- model,
2165
- provider: input.explicitProvider?.trim() || inferProviderFromModel(model),
2166
- rule: "default:global",
2167
- requestedModel: model
2168
- };
2300
+ return enforceCursorWorkerProvider({
2301
+ routing: decision,
2302
+ task: input.task,
2303
+ explicitProvider: input.explicitProvider,
2304
+ explicitProviderIsOperatorOverride: input.explicitProviderIsOperatorOverride
2305
+ });
2169
2306
  }
2170
2307
  function resolveModelFallback(startedModel, launchModel, providerDefault) {
2171
2308
  return startedModel || launchModel || providerDefault || resolveGlobalDefaultModel() || CLAUDE_DEFAULT_MODEL;
@@ -2257,6 +2394,76 @@ function hasLiveWorkerForTask(runId, taskId) {
2257
2394
  import { existsSync as existsSync13, mkdirSync as mkdirSync3 } from "node:fs";
2258
2395
  import path14 from "node:path";
2259
2396
 
2397
+ // src/harness-repair-target.ts
2398
+ var HARNESS_CONTRACT_RE = /<!--\s*harness-contract:\s*(\{[\s\S]*?\})\s*-->/i;
2399
+ var FIX_EXECUTOR_REF_PREFIX = "next-action-fix:";
2400
+ function trimOrNull4(value) {
2401
+ if (typeof value !== "string") return null;
2402
+ const t = value.trim();
2403
+ return t.length ? t : null;
2404
+ }
2405
+ function normalizePrUrl2(url) {
2406
+ const m = url.trim().match(/github\.com\/([^/]+\/[^/]+)\/(?:pull|pulls)\/(\d+)/i);
2407
+ if (!m) return trimOrNull4(url);
2408
+ return `https://github.com/${m[1]}/pull/${m[2]}`;
2409
+ }
2410
+ function isHarnessRepairTask(task) {
2411
+ const title = (task.title ?? "").trim().toLowerCase();
2412
+ if (title.startsWith("fix:") || title.startsWith("repair:")) return true;
2413
+ const ref = (task.executorRef ?? "").toLowerCase();
2414
+ if (ref.startsWith(FIX_EXECUTOR_REF_PREFIX)) return true;
2415
+ if (ref.includes("repair") || ref.includes("unblock")) return true;
2416
+ return false;
2417
+ }
2418
+ function parseRepairTargetContractFromDescription(description) {
2419
+ const empty = {
2420
+ repairEnforceOriginalPr: false,
2421
+ targetPrUrl: null,
2422
+ targetPrBranch: null
2423
+ };
2424
+ if (!description) return empty;
2425
+ const m = description.match(HARNESS_CONTRACT_RE);
2426
+ if (!m?.[1]) return empty;
2427
+ try {
2428
+ const parsed = JSON.parse(m[1]);
2429
+ const url = trimOrNull4(
2430
+ String(parsed.targetPrUrl ?? parsed.target_pr_url ?? "")
2431
+ );
2432
+ const branch = trimOrNull4(
2433
+ String(parsed.targetPrBranch ?? parsed.target_pr_branch ?? "")
2434
+ );
2435
+ const enforce = parsed.repairEnforceOriginalPr === true || parsed.repair_enforce_original_pr === true;
2436
+ return {
2437
+ repairEnforceOriginalPr: enforce || Boolean(url),
2438
+ targetPrUrl: url ? normalizePrUrl2(url) : null,
2439
+ targetPrBranch: branch
2440
+ };
2441
+ } catch {
2442
+ return empty;
2443
+ }
2444
+ }
2445
+ function resolveHarnessRepairTargetFromTask(task) {
2446
+ if (!isHarnessRepairTask(task)) return null;
2447
+ const block = parseRepairTargetContractFromDescription(task.description);
2448
+ const taskPr = task.prUrl ? normalizePrUrl2(task.prUrl) : null;
2449
+ const targetPrUrl = block.targetPrUrl ?? taskPr;
2450
+ if (!targetPrUrl) return null;
2451
+ return {
2452
+ targetPrUrl,
2453
+ targetPrBranch: block.targetPrBranch ?? trimOrNull4(task.branch)
2454
+ };
2455
+ }
2456
+ function repairTargetPromptLines(target) {
2457
+ return [
2458
+ "Repair target PR policy:",
2459
+ `- Work on the existing target PR branch \u2014 do not open a duplicate repair PR by default.`,
2460
+ `- Canonical target PR: ${target.targetPrUrl}`,
2461
+ ...target.targetPrBranch ? [`- Canonical target branch: \`${target.targetPrBranch}\` (checkout is already on this branch).`] : [],
2462
+ `- Reconcile ${target.targetPrUrl} in structured finalResult.targetPrReconciliation.`,
2463
+ `- Only supersede the original when the branch is inaccessible: set supersedesOriginalTargetPr: true with reason and close/comment on the original PR.`
2464
+ ];
2465
+ }
2466
+
2260
2467
  // src/prompt.ts
2261
2468
  function buildPrompt(input) {
2262
2469
  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.";
@@ -2301,7 +2508,7 @@ function buildPrompt(input) {
2301
2508
  `Progress heartbeat file: ${input.heartbeatPath}`,
2302
2509
  "After each major step, append one JSON line to the heartbeat file with fields: ts, phase, summary, changedFiles, blocker.",
2303
2510
  "Final response must include files changed, verification commands, and unresolved risks.",
2304
- "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.",
2511
+ "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.",
2305
2512
  "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.",
2306
2513
  "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.",
2307
2514
  "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.",
@@ -2318,6 +2525,13 @@ function buildPrompt(input) {
2318
2525
  "",
2319
2526
  ...input.personaMarkdown?.trim() ? [input.personaMarkdown.trim(), ""] : [],
2320
2527
  ...input.instructionPolicyMarkdown?.trim() ? ["Operating rules (Lane A \u2014 from AgentOS memory policy):", input.instructionPolicyMarkdown.trim(), ""] : [],
2528
+ ...input.repairTargetPrUrl ? [
2529
+ ...repairTargetPromptLines({
2530
+ targetPrUrl: input.repairTargetPrUrl,
2531
+ targetPrBranch: input.repairTargetBranch ?? null
2532
+ }),
2533
+ ""
2534
+ ] : [],
2321
2535
  "Task:",
2322
2536
  input.task
2323
2537
  ].join("\n");
@@ -2483,7 +2697,15 @@ var BUILTIN = {
2483
2697
  var overrideProvider = null;
2484
2698
  function resolveWorkerProvider(name) {
2485
2699
  if (overrideProvider) return overrideProvider;
2486
- const configured = (name || loadUserConfig().workerProvider || "claude").trim();
2700
+ const explicit = name?.trim();
2701
+ if (explicit) {
2702
+ const provider2 = BUILTIN[explicit];
2703
+ if (!provider2) {
2704
+ throw new Error(`unknown worker provider "${explicit}" \u2014 supported: ${Object.keys(BUILTIN).join(", ")}`);
2705
+ }
2706
+ return provider2;
2707
+ }
2708
+ const configured = resolveConfiguredWorkerProvider(loadUserConfig().workerProvider);
2487
2709
  const provider = BUILTIN[configured];
2488
2710
  if (!provider) {
2489
2711
  throw new Error(`unknown worker provider "${configured}" \u2014 supported: ${Object.keys(BUILTIN).join(", ")}`);
@@ -2527,13 +2749,13 @@ var ADVANCED_OUTCOMES = /* @__PURE__ */ new Set([
2527
2749
  "review_already_scheduled"
2528
2750
  ]);
2529
2751
  function summarizeHarnessCompletionResponse(parsed) {
2530
- const record = asRecord(parsed);
2531
- if (!record) {
2752
+ const record3 = asRecord(parsed);
2753
+ if (!record3) {
2532
2754
  return { routeOutcome: null, taskAdvanced: false, detail: null };
2533
2755
  }
2534
- const outcome = asString(record.outcome);
2535
- const detail = asString(record.detail) ?? asString(record.error);
2536
- const task = asRecord(record.task);
2756
+ const outcome = asString(record3.outcome);
2757
+ const detail = asString(record3.detail) ?? asString(record3.error);
2758
+ const task = asRecord(record3.task);
2537
2759
  const taskStatus = task ? asString(task.status) : null;
2538
2760
  const taskAdvanced = outcome !== null && ADVANCED_OUTCOMES.has(outcome) || taskStatus === "awaiting_review" || taskStatus === "done";
2539
2761
  return {
@@ -2579,7 +2801,7 @@ var NO_PR_COMMITS_BETWEEN_RE = /no commits between/i;
2579
2801
  function isGhNoCommitsBetweenError(detail) {
2580
2802
  return Boolean(detail && NO_PR_COMMITS_BETWEEN_RE.test(detail));
2581
2803
  }
2582
- function trimOrNull4(value) {
2804
+ function trimOrNull5(value) {
2583
2805
  if (typeof value !== "string") return null;
2584
2806
  const trimmed = value.trim();
2585
2807
  return trimmed.length ? trimmed : null;
@@ -2587,7 +2809,7 @@ function trimOrNull4(value) {
2587
2809
  function committedHead(ancestry) {
2588
2810
  if (!ancestry?.checked) return null;
2589
2811
  if (ancestry.headIsAncestorOfBase !== false) return null;
2590
- return trimOrNull4(ancestry.head);
2812
+ return trimOrNull5(ancestry.head);
2591
2813
  }
2592
2814
  function extractPrUrlFromText(value) {
2593
2815
  if (value === void 0 || value === null) return null;
@@ -2595,7 +2817,7 @@ function extractPrUrlFromText(value) {
2595
2817
  const m = text.match(
2596
2818
  /https?:\/\/[^\s)>"]+\/(?:pull|pulls|merge_requests|pull-requests)\/\d+/i
2597
2819
  );
2598
- return m ? trimOrNull4(m[0]) : null;
2820
+ return m ? trimOrNull5(m[0]) : null;
2599
2821
  }
2600
2822
  function countCommitsAheadOfBase(worktreePath, baseRef, exec) {
2601
2823
  const base = baseRef.trim();
@@ -2607,21 +2829,21 @@ function countCommitsAheadOfBase(worktreePath, baseRef, exec) {
2607
2829
  }
2608
2830
  function isReviewArtifactWorker(worker, snapshot) {
2609
2831
  if (snapshot.changedFiles.length > 0) return false;
2610
- const persona = trimOrNull4(worker.personaSlug)?.toLowerCase();
2832
+ const persona = trimOrNull5(worker.personaSlug)?.toLowerCase();
2611
2833
  if (persona && REVIEW_PERSONA_SLUGS.has(persona)) return true;
2612
- const rule = trimOrNull4(worker.routingRule) ?? "";
2834
+ const rule = trimOrNull5(worker.routingRule) ?? "";
2613
2835
  if (rule && REVIEW_LANE_RULE.test(rule)) return true;
2614
2836
  return false;
2615
2837
  }
2616
2838
  function hasWorkProduct(snapshot, options) {
2617
2839
  if (snapshot.changedFiles.length > 0) return true;
2618
- const baseRef = trimOrNull4(options?.baseRef);
2840
+ const baseRef = trimOrNull5(options?.baseRef);
2619
2841
  if (baseRef && options?.exec && options.worktreePath) {
2620
2842
  const ahead = countCommitsAheadOfBase(options.worktreePath, baseRef, options.exec);
2621
2843
  if (ahead === 0) return false;
2622
2844
  if (ahead !== null && ahead > 0) return true;
2623
2845
  }
2624
- if (trimOrNull4(snapshot.headCommit)) return true;
2846
+ if (trimOrNull5(snapshot.headCommit)) return true;
2625
2847
  if (committedHead(snapshot.gitAncestry)) return true;
2626
2848
  return false;
2627
2849
  }
@@ -2637,7 +2859,7 @@ function assessPrHandoffRequirement(input) {
2637
2859
  })) {
2638
2860
  return { required: false, reason: "expert_review_task" };
2639
2861
  }
2640
- const rule = trimOrNull4(input.routingRule) ?? "";
2862
+ const rule = trimOrNull5(input.routingRule) ?? "";
2641
2863
  if (rule && REVIEW_LANE_RULE.test(rule)) {
2642
2864
  return { required: false, reason: "review_lane" };
2643
2865
  }
@@ -2648,10 +2870,14 @@ function assessPrHandoffRequirement(input) {
2648
2870
  if (isReviewArtifactWorker(workerCtx, input.snapshot)) {
2649
2871
  return { required: false, reason: "review_artifact" };
2650
2872
  }
2651
- if (trimOrNull4(input.patchPath) || trimOrNull4(input.artifactBundlePath)) {
2873
+ if (trimOrNull5(input.patchPath) || trimOrNull5(input.artifactBundlePath)) {
2652
2874
  return { required: false, reason: "patch_or_bundle" };
2653
2875
  }
2654
- const prUrl = trimOrNull4(input.prUrl) ?? trimOrNull4(input.taskPrUrl) ?? trimOrNull4(input.snapshot.prUrl);
2876
+ const repairTarget = trimOrNull5(input.repairTargetPrUrl);
2877
+ if (repairTarget) {
2878
+ return { required: false, reason: "repair_target_pr" };
2879
+ }
2880
+ const prUrl = trimOrNull5(input.prUrl) ?? trimOrNull5(input.taskPrUrl) ?? trimOrNull5(input.snapshot.prUrl);
2655
2881
  if (prUrl) {
2656
2882
  return { required: false, reason: "already_has_pr" };
2657
2883
  }
@@ -2672,8 +2898,8 @@ function buildPrHandoffSnapshotFromStatus(status, extras) {
2672
2898
  worktreePath: status.worktreePath,
2673
2899
  gitAncestry: status.gitAncestry,
2674
2900
  finalResult: status.finalResult,
2675
- headCommit: trimOrNull4(extras?.headCommit) ?? committedHead(status.gitAncestry),
2676
- prUrl: trimOrNull4(extras?.prUrl) ?? null
2901
+ headCommit: trimOrNull5(extras?.headCommit) ?? committedHead(status.gitAncestry),
2902
+ prUrl: trimOrNull5(extras?.prUrl) ?? null
2677
2903
  };
2678
2904
  }
2679
2905
 
@@ -2879,6 +3105,7 @@ function ensurePrReadyHandoff(input, exec = defaultPrHandoffExec) {
2879
3105
  executorRef: input.worker.executorRef,
2880
3106
  parentTaskId: input.worker.parentTaskId,
2881
3107
  taskPrUrl: input.worker.taskPrUrl,
3108
+ repairTargetPrUrl: input.worker.repairTargetPrUrl,
2882
3109
  baseRef,
2883
3110
  exec,
2884
3111
  worker: input.worker,
@@ -2907,6 +3134,48 @@ function ensurePrReadyHandoff(input, exec = defaultPrHandoffExec) {
2907
3134
  nextAction: "Ensure `origin` points at GitHub, push the branch, open a PR, and rerun `kynver worker complete`."
2908
3135
  };
2909
3136
  }
3137
+ const repairTarget = input.worker.repairTargetPrUrl?.trim();
3138
+ if (repairTarget) {
3139
+ let committed2 = false;
3140
+ let pushed2 = false;
3141
+ let headCommit2 = snapshot.headCommit ?? resolveHeadCommit(snapshot.worktreePath, exec) ?? void 0;
3142
+ if (snapshot.changedFiles.length > 0) {
3143
+ const pushResult2 = commitAndPushBranch({
3144
+ worktreePath: snapshot.worktreePath,
3145
+ branch: snapshot.branch,
3146
+ commitMessage: `fix(harness): repair target PR ${repairTarget}`,
3147
+ hasDirtyFiles: true,
3148
+ exec
3149
+ });
3150
+ if (!pushResult2.ok) {
3151
+ return {
3152
+ ok: false,
3153
+ reason: `PR-ready handoff blocked: ${pushResult2.detail ?? "git commit/push failed"}`,
3154
+ nextAction: "Commit and push to the target PR branch, then rerun `kynver worker complete`."
3155
+ };
3156
+ }
3157
+ committed2 = pushResult2.committed;
3158
+ pushed2 = pushResult2.pushed;
3159
+ headCommit2 = pushResult2.headCommit ?? headCommit2;
3160
+ } else {
3161
+ const pushOnly = exec.git(snapshot.worktreePath, ["push", "-u", "origin", snapshot.branch]);
3162
+ if (pushOnly.status !== 0 && !/already up to date/i.test(pushOnly.stderr || pushOnly.stdout)) {
3163
+ return {
3164
+ ok: false,
3165
+ reason: `PR-ready handoff blocked: ${pushOnly.stderr || pushOnly.stdout || "git push failed"}`,
3166
+ nextAction: "Push the target branch to origin, then rerun `kynver worker complete`."
3167
+ };
3168
+ }
3169
+ pushed2 = pushOnly.status === 0;
3170
+ }
3171
+ return {
3172
+ ok: true,
3173
+ prUrl: repairTarget,
3174
+ headCommit: headCommit2,
3175
+ committed: committed2,
3176
+ pushed: pushed2
3177
+ };
3178
+ }
2910
3179
  const existing = findOpenPrUrl(snapshot.worktreePath, repo, snapshot.branch, exec);
2911
3180
  if (existing) {
2912
3181
  return {
@@ -3018,12 +3287,12 @@ function asRecord2(value) {
3018
3287
  return value && typeof value === "object" && !Array.isArray(value) ? value : null;
3019
3288
  }
3020
3289
  function extractDisposableArtifactsRemoved(finalResult) {
3021
- const record = asRecord2(finalResult);
3022
- if (!record) return [];
3023
- const nested = asRecord2(record.worktreeHandoff);
3290
+ const record3 = asRecord2(finalResult);
3291
+ if (!record3) return [];
3292
+ const nested = asRecord2(record3.worktreeHandoff);
3024
3293
  const fromNested = stringList(nested?.disposableArtifactsRemoved);
3025
3294
  if (fromNested.length) return fromNested;
3026
- return stringList(record.disposableArtifactsRemoved);
3295
+ return stringList(record3.disposableArtifactsRemoved);
3027
3296
  }
3028
3297
  function normalizeRelativePath(value) {
3029
3298
  return value.replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/+$/, "");
@@ -3034,13 +3303,13 @@ function dirtyPathsCoveredByDisposableRemoval(changedFiles, removed) {
3034
3303
  if (removed.length === 0) return false;
3035
3304
  const removedSet = new Set(removed.map((p) => normalizeRelativePath(p)));
3036
3305
  return material.every((line) => {
3037
- const path44 = normalizeRelativePath(pathFromGitStatusLine(line));
3038
- return removedSet.has(path44);
3306
+ const path51 = normalizeRelativePath(pathFromGitStatusLine(line));
3307
+ return removedSet.has(path51);
3039
3308
  });
3040
3309
  }
3041
3310
 
3042
3311
  // src/worktree-completion-handoff.ts
3043
- function trimOrNull5(value) {
3312
+ function trimOrNull6(value) {
3044
3313
  if (typeof value !== "string") return null;
3045
3314
  const t = value.trim();
3046
3315
  return t.length ? t : null;
@@ -3067,7 +3336,7 @@ function assessWorktreeCompletionHandoff(input) {
3067
3336
  const materialDirty = materialWorktreeChanges(rawDirty);
3068
3337
  const removed = mergedDisposableRemoved(input);
3069
3338
  const effectivelyClean = materialDirty.length === 0 || dirtyPathsCoveredByDisposableRemoval(rawDirty, removed);
3070
- if (trimOrNull5(input.prUrl)) {
3339
+ if (trimOrNull6(input.prUrl)) {
3071
3340
  if (!effectivelyClean) {
3072
3341
  return {
3073
3342
  allowed: false,
@@ -3078,7 +3347,7 @@ function assessWorktreeCompletionHandoff(input) {
3078
3347
  }
3079
3348
  return { allowed: true, state: "pr_handoff", materialDirtyCount: 0 };
3080
3349
  }
3081
- if (trimOrNull5(input.headCommit)) {
3350
+ if (trimOrNull6(input.headCommit)) {
3082
3351
  if (!effectivelyClean) {
3083
3352
  return {
3084
3353
  allowed: false,
@@ -3089,7 +3358,7 @@ function assessWorktreeCompletionHandoff(input) {
3089
3358
  }
3090
3359
  return { allowed: true, state: "commit_handoff", materialDirtyCount: 0 };
3091
3360
  }
3092
- if (trimOrNull5(input.artifactBundlePath) || trimOrNull5(input.patchPath)) {
3361
+ if (trimOrNull6(input.artifactBundlePath) || trimOrNull6(input.patchPath)) {
3093
3362
  if (!effectivelyClean) {
3094
3363
  return {
3095
3364
  allowed: false,
@@ -3819,6 +4088,19 @@ function spawnCompletionSidecar(opts) {
3819
4088
  }
3820
4089
  }
3821
4090
 
4091
+ // src/repair-target-worktree.ts
4092
+ function addWorktreeForRepairBranch(repo, worktreePath, branch) {
4093
+ git(repo, ["fetch", "origin", branch, "--prune"], { allowFailure: true });
4094
+ const remoteRef = `origin/${branch}`;
4095
+ const added = gitCapture(repo, ["worktree", "add", "-B", branch, worktreePath, remoteRef]);
4096
+ if (added.status === 0) return;
4097
+ const fallback = gitCapture(repo, ["worktree", "add", "-b", branch, worktreePath, "HEAD"]);
4098
+ if (fallback.status !== 0) {
4099
+ const detail = added.stderr || added.stdout || fallback.stderr || fallback.stdout || "git worktree add failed for repair target branch";
4100
+ throw new Error(detail);
4101
+ }
4102
+ }
4103
+
3822
4104
  // src/supervisor.ts
3823
4105
  function spawnWorkerProcess(run, opts) {
3824
4106
  const rawName = typeof opts.name === "string" ? opts.name.trim() : "";
@@ -3829,13 +4111,14 @@ function spawnWorkerProcess(run, opts) {
3829
4111
  if (run.workers?.[name]) throw new Error(`worker already exists in run ${run.id}: ${name}`);
3830
4112
  if (!opts.task) throw new Error(`missing task text for worker ${name}`);
3831
4113
  const routing = opts.routingRule || opts.requestedModel ? {
3832
- provider: opts.provider || "claude",
4114
+ provider: opts.provider || DEFAULT_WORKER_PROVIDER,
3833
4115
  model: opts.model,
3834
4116
  rule: opts.routingRule || "explicit:spawn",
3835
4117
  requestedModel: opts.requestedModel ?? opts.model
3836
4118
  } : resolveWorkerLaunch({
3837
4119
  explicitModel: opts.model,
3838
- explicitProvider: opts.provider
4120
+ explicitProvider: opts.provider,
4121
+ explicitProviderIsOperatorOverride: Boolean(opts.provider?.trim())
3839
4122
  });
3840
4123
  const provider = resolveWorkerProvider(routing.provider);
3841
4124
  let launchModel = routing.model;
@@ -3855,10 +4138,15 @@ function spawnWorkerProcess(run, opts) {
3855
4138
  const workerDir = path14.join(runDirectory(run.id), "workers", name);
3856
4139
  mkdirSync3(workerDir, { recursive: true });
3857
4140
  const worktreePath = path14.join(worktreesDir, run.id, name);
3858
- const branch = opts.branch || `agent/${run.id}/${name}`;
4141
+ const repairBranch = opts.repairTargetBranch?.trim() || void 0;
4142
+ const branch = repairBranch || opts.branch || `agent/${run.id}/${name}`;
3859
4143
  if (existsSync13(worktreePath)) throw new Error(`worktree path already exists: ${worktreePath}`);
3860
4144
  git(run.repo, ["fetch", "origin", "--prune"], { allowFailure: true });
3861
- git(run.repo, ["worktree", "add", "-b", branch, worktreePath, run.baseCommit], { throwError: true });
4145
+ if (repairBranch) {
4146
+ addWorktreeForRepairBranch(run.repo, worktreePath, repairBranch);
4147
+ } else {
4148
+ git(run.repo, ["worktree", "add", "-b", branch, worktreePath, run.baseCommit], { throwError: true });
4149
+ }
3862
4150
  const stdoutPath = path14.join(workerDir, "stdout.jsonl");
3863
4151
  const stderrPath = path14.join(workerDir, "stderr.log");
3864
4152
  const heartbeatPath = path14.join(workerDir, "heartbeat.jsonl");
@@ -3871,7 +4159,9 @@ function spawnWorkerProcess(run, opts) {
3871
4159
  taskId: opts.taskId,
3872
4160
  instructionPolicyMarkdown: opts.instructionPolicyMarkdown,
3873
4161
  personaMarkdown: opts.personaMarkdown,
3874
- model: launchModel
4162
+ model: launchModel,
4163
+ repairTargetPrUrl: opts.repairTargetPrUrl,
4164
+ repairTargetBranch: opts.repairTargetBranch ?? (repairBranch || void 0)
3875
4165
  });
3876
4166
  let started;
3877
4167
  try {
@@ -3924,6 +4214,8 @@ function spawnWorkerProcess(run, opts) {
3924
4214
  ...opts.parentTaskId ? { parentTaskId: String(opts.parentTaskId) } : {},
3925
4215
  ...opts.taskTitle ? { taskTitle: String(opts.taskTitle) } : {},
3926
4216
  ...opts.taskPrUrl ? { taskPrUrl: String(opts.taskPrUrl) } : {},
4217
+ ...opts.repairTargetPrUrl ? { repairTargetPrUrl: String(opts.repairTargetPrUrl) } : {},
4218
+ ...opts.repairTargetBranch ? { repairTargetBranch: String(opts.repairTargetBranch) } : {},
3927
4219
  startedAt: (/* @__PURE__ */ new Date()).toISOString()
3928
4220
  };
3929
4221
  saveWorker(run.id, worker);
@@ -4595,6 +4887,24 @@ function extractPlanOutboxFromTask(task) {
4595
4887
  };
4596
4888
  }
4597
4889
 
4890
+ // src/runner-identity.ts
4891
+ import os3 from "node:os";
4892
+ function trimOrNull7(value) {
4893
+ if (!value?.trim()) return null;
4894
+ return value.trim();
4895
+ }
4896
+ function resolveRunnerPresencePayload(input = {}) {
4897
+ const env = input.env ?? process.env;
4898
+ const runnerId = trimOrNull7(env.KYNVER_RUNTIME_ID) ?? trimOrNull7(env.OPENCLAW_RUNTIME_ID) ?? trimOrNull7(env.HOSTNAME) ?? os3.hostname();
4899
+ return {
4900
+ runnerId,
4901
+ hostname: trimOrNull7(env.HOSTNAME) ?? os3.hostname(),
4902
+ profile: trimOrNull7(env.KYNVER_RUNNER_PROFILE) ?? trimOrNull7(env.OPENCLAW_RUNNER_PROFILE),
4903
+ harnessRepo: trimOrNull7(env.KYNVER_HARNESS_REPO) ?? trimOrNull7(env.KYNVER_DEFAULT_REPO),
4904
+ runId: input.runId ?? null
4905
+ };
4906
+ }
4907
+
4598
4908
  // src/dispatch.ts
4599
4909
  var DEFAULT_DISPATCH_LEASE_MS = 60 * 60 * 1e3;
4600
4910
  function readHarnessWorkerContext(decision) {
@@ -4659,7 +4969,8 @@ async function dispatchRun(args) {
4659
4969
  const secret = await resolveCallbackSecretWithMint(args.secret ? String(args.secret) : void 0, agentOsId, { baseUrl: base });
4660
4970
  const execute = args.execute === true || args.execute === "true";
4661
4971
  const dryRun = !execute;
4662
- const leaseOwner = `kynver-harness:${run.id}`;
4972
+ const runnerPresence = resolveRunnerPresencePayload({ runId: run.id });
4973
+ const leaseOwner = `kynver-harness:${run.id}@runner:${runnerPresence.runnerId}`;
4663
4974
  const runnerDiskGate = args.diskPath ? observeRunnerDiskGate({ diskPath: String(args.diskPath) }) : observeRunnerDiskGate({ diskPath: run.repo });
4664
4975
  const runnerResourceGate = observeRunnerResourceGate({ runId: run.id });
4665
4976
  const requestedStarts = Number(args.maxStarts) > 0 ? Math.floor(Number(args.maxStarts)) : 1;
@@ -4671,11 +4982,18 @@ async function dispatchRun(args) {
4671
4982
  void 0
4672
4983
  );
4673
4984
  if (!worker?.taskId || !isPidAlive(worker.pid)) continue;
4985
+ const ownedPaths = Array.isArray(worker.ownedPaths) ? worker.ownedPaths.filter((p) => typeof p === "string") : [];
4986
+ const writeSetPrefixes = Array.isArray(
4987
+ worker.writeSetPrefixes
4988
+ ) ? (worker.writeSetPrefixes ?? []).filter((p) => typeof p === "string") : [];
4674
4989
  activeHarnessWorkers.push({
4675
4990
  runId: run.id,
4676
4991
  workerName: name,
4677
4992
  taskId: worker.taskId,
4678
- pid: worker.pid
4993
+ pid: worker.pid,
4994
+ ...ownedPaths.length ? { ownedPaths } : {},
4995
+ ...writeSetPrefixes.length ? { writeSetPrefixes } : {},
4996
+ ...worker.allowConcurrentHotspot ? { allowConcurrentHotspot: true } : {}
4679
4997
  });
4680
4998
  }
4681
4999
  const dispatchUrl = `${base}/api/agent-os/by-id/${encodeURIComponent(agentOsId)}/tasks/dispatch-next`;
@@ -4688,6 +5006,7 @@ async function dispatchRun(args) {
4688
5006
  runnerDiskGate,
4689
5007
  runnerResourceGate,
4690
5008
  activeHarnessWorkers,
5009
+ runnerPresence,
4691
5010
  harnessBoardSnapshot: buildRunBoard(run.id),
4692
5011
  ...args.lane ? { lane: String(args.lane) } : {},
4693
5012
  executor: args.executor ? String(args.executor) : "harness",
@@ -4780,10 +5099,19 @@ async function dispatchRun(args) {
4780
5099
  const name = safeSlug(`t-${task.id}-a${task.attempt}`);
4781
5100
  const routing = resolveWorkerLaunch({
4782
5101
  explicitModel: args.model ? String(args.model) : void 0,
5102
+ explicitProvider: args.provider ? String(args.provider) : void 0,
5103
+ explicitProviderIsOperatorOverride: Boolean(args.provider),
4783
5104
  task: enrichTaskForModelRouting(task)
4784
5105
  });
4785
5106
  try {
4786
5107
  const planId = task.planId ? String(task.planId) : void 0;
5108
+ const repairTarget = resolveHarnessRepairTargetFromTask({
5109
+ title: task.title ? String(task.title) : void 0,
5110
+ description: task.description ? String(task.description) : null,
5111
+ executorRef: task.executorRef ? String(task.executorRef) : null,
5112
+ prUrl: task.prUrl ? String(task.prUrl) : null,
5113
+ branch: task.branch ? String(task.branch) : null
5114
+ });
4787
5115
  const worker = spawnWorkerProcess(run, {
4788
5116
  name,
4789
5117
  task: buildDispatchTaskText(task, agentOsId),
@@ -4795,10 +5123,13 @@ async function dispatchRun(args) {
4795
5123
  agentOsId,
4796
5124
  taskId: String(task.id),
4797
5125
  planId,
5126
+ branch: repairTarget?.targetPrBranch ?? void 0,
4798
5127
  executorRef: task.executorRef ? String(task.executorRef) : void 0,
4799
5128
  parentTaskId: task.parentTaskId ? String(task.parentTaskId) : void 0,
4800
5129
  taskTitle: task.title ? String(task.title) : void 0,
4801
- taskPrUrl: task.prUrl ? String(task.prUrl) : void 0,
5130
+ taskPrUrl: repairTarget?.targetPrUrl ?? (task.prUrl ? String(task.prUrl) : void 0),
5131
+ repairTargetPrUrl: repairTarget?.targetPrUrl,
5132
+ repairTargetBranch: repairTarget?.targetPrBranch ?? void 0,
4802
5133
  instructionPolicyMarkdown: harnessContext?.instructionPolicyMarkdown ?? null,
4803
5134
  instructionPolicyFingerprint: harnessContext?.instructionPolicyFingerprint ?? null,
4804
5135
  instructionPolicyEvidence: harnessContext?.instructionPolicyEvidence ?? null,
@@ -4876,6 +5207,58 @@ async function dispatchRun(args) {
4876
5207
  }
4877
5208
  }
4878
5209
 
5210
+ // src/box-resource-snapshot.ts
5211
+ import os5 from "node:os";
5212
+
5213
+ // src/box-resource-snapshot-shared.ts
5214
+ import os4 from "node:os";
5215
+ function resolveBoxKindFromEnv(env = process.env) {
5216
+ const kind = (env.KYNVER_BOX_KIND ?? env.KYNVER_AGENT_OS_SLUG ?? "forge").trim().toLowerCase();
5217
+ if (kind === "ghost" || kind === "forge") return kind;
5218
+ return kind || "forge";
5219
+ }
5220
+ function defaultBoxId(boxKind, hostLabel) {
5221
+ const host = (hostLabel ?? os4.hostname()).trim().toLowerCase().replace(/\s+/g, "-") || "unknown-host";
5222
+ return `${boxKind}:${host}`;
5223
+ }
5224
+
5225
+ // src/box-resource-snapshot.ts
5226
+ function buildBoxResourceSnapshotFromGate(gate, input = {}) {
5227
+ const boxKind = (input.boxKind ?? resolveBoxKindFromEnv()).trim().toLowerCase() || "forge";
5228
+ const hostLabel = input.hostLabel ?? os5.hostname();
5229
+ const boxId = input.boxId ?? defaultBoxId(boxKind, hostLabel);
5230
+ return {
5231
+ boxId,
5232
+ boxKind,
5233
+ displayName: input.displayName ?? null,
5234
+ hostLabel,
5235
+ observedAt: input.observedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
5236
+ totalMemBytes: gate.totalMemBytes,
5237
+ freeMemBytes: gate.freeMemBytes,
5238
+ activeWorkers: gate.activeWorkers,
5239
+ maxConcurrentWorkers: gate.maxConcurrentWorkers,
5240
+ autoCap: gate.autoCap,
5241
+ slotsAvailable: gate.slotsAvailable,
5242
+ harnessRunId: input.harnessRunId ?? null,
5243
+ queuedTasks: input.queuedTasks ?? null,
5244
+ reason: gate.reason,
5245
+ ...input.prEvidence?.length ? { prEvidence: input.prEvidence } : {}
5246
+ };
5247
+ }
5248
+ function formatHeartbeatLine(input) {
5249
+ const row = {
5250
+ ts: input.ts ?? (/* @__PURE__ */ new Date()).toISOString(),
5251
+ phase: input.phase,
5252
+ summary: input.summary,
5253
+ changedFiles: input.changedFiles ?? [],
5254
+ blocker: input.blocker ?? null
5255
+ };
5256
+ if (input.boxResourceSnapshot) row.boxResourceSnapshot = input.boxResourceSnapshot;
5257
+ if (input.prEvidence?.length) row.prEvidence = input.prEvidence;
5258
+ return `${JSON.stringify(row)}
5259
+ `;
5260
+ }
5261
+
4879
5262
  // src/fortress-engagement-gate.ts
4880
5263
  function isEngagementRequiredSkip(skip) {
4881
5264
  if (skip.skipReason === "engagement_required") return true;
@@ -5094,79 +5477,365 @@ function validateTailLines(lines) {
5094
5477
  }
5095
5478
 
5096
5479
  // src/worktree.ts
5097
- import { existsSync as existsSync17, mkdirSync as mkdirSync5 } from "node:fs";
5480
+ import { existsSync as existsSync18, mkdirSync as mkdirSync5 } from "node:fs";
5481
+ import path25 from "node:path";
5482
+
5483
+ // src/run-list.ts
5484
+ import { existsSync as existsSync17, readFileSync as readFileSync11 } from "node:fs";
5485
+ import path23 from "node:path";
5486
+
5487
+ // src/stale-reconcile.ts
5098
5488
  import path22 from "node:path";
5099
5489
 
5100
- // src/default-repo.ts
5490
+ // src/finalize.ts
5101
5491
  import path21 from "node:path";
5102
- function expandConfiguredRepo(value) {
5103
- return path21.resolve(resolveUserPath(value.trim()));
5104
- }
5105
- function fromConfigured(value, source, persistedInConfig) {
5106
- const trimmed = value?.trim();
5107
- if (!trimmed) return null;
5108
- return {
5109
- repo: expandConfiguredRepo(trimmed),
5110
- source,
5111
- persistedInConfig
5112
- };
5113
- }
5114
- function resolveDefaultRepo(opts = {}) {
5115
- const env = opts.env ?? process.env;
5116
- const config = opts.config ?? loadUserConfig();
5117
- const fromConfig = fromConfigured(config.defaultRepo, "config", true);
5118
- if (fromConfig) return fromConfig;
5119
- const fromDefaultEnv = fromConfigured(env.KYNVER_DEFAULT_REPO, "env_default_repo", false);
5120
- if (fromDefaultEnv) return fromDefaultEnv;
5121
- const fromHarnessEnv = fromConfigured(env.KYNVER_HARNESS_REPO, "env_harness_repo", false);
5122
- if (fromHarnessEnv) return fromHarnessEnv;
5123
- const discovered = discoverDefaultRepo({
5124
- cwd: opts.cwd,
5125
- runtimeModuleUrl: opts.runtimeModuleUrl
5126
- });
5127
- if (!discovered) return null;
5128
- return {
5129
- repo: discovered.repo,
5130
- source: discovered.source,
5131
- persistedInConfig: false
5132
- };
5133
- }
5134
- function persistDefaultRepo(repo, existing) {
5135
- const config = {
5136
- ...existing ?? loadUserConfig(),
5137
- defaultRepo: redactHomePath(path21.resolve(repo))
5138
- };
5139
- saveUserConfig(config);
5140
- return config;
5141
- }
5142
- function remediateDefaultRepo(opts) {
5143
- const existing = opts?.config ?? loadUserConfig();
5144
- const resolved = resolveDefaultRepo({ ...opts, config: existing });
5145
- if (!resolved) {
5146
- return {
5147
- ok: false,
5148
- reason: "No Kynver git checkout found. Clone the repo, cd into it, then run `kynver setup --repo /path/to/Kynver` (or export KYNVER_DEFAULT_REPO)."
5149
- };
5150
- }
5151
- if (resolved.persistedInConfig) {
5152
- return { ok: true, resolved, config: existing };
5492
+ var ACTIVE_RUN_STATUSES = /* @__PURE__ */ new Set([
5493
+ "running",
5494
+ "dispatching",
5495
+ "pending",
5496
+ "queued",
5497
+ "needs_attention"
5498
+ ]);
5499
+ var TERMINAL_RUN_STATUSES = /* @__PURE__ */ new Set(["completed", "failed", "cancelled", "done"]);
5500
+ function deriveTerminalRunStatus(run) {
5501
+ const names = Object.keys(run.workers || {});
5502
+ if (names.length === 0) return "failed";
5503
+ let anyAlive = false;
5504
+ let anyResult = false;
5505
+ let anyCompletionBlocked = false;
5506
+ let anyLandingBlocked = false;
5507
+ for (const name of names) {
5508
+ const worker = readJson(
5509
+ path21.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
5510
+ void 0
5511
+ );
5512
+ if (!worker) continue;
5513
+ const status = computeWorkerStatus(worker, {
5514
+ base: run.base,
5515
+ baseCommit: run.baseCommit
5516
+ });
5517
+ if (status.alive && !status.finalResult) {
5518
+ anyAlive = true;
5519
+ break;
5520
+ }
5521
+ if (typeof worker.completionBlocker === "string" && worker.completionBlocker) {
5522
+ anyCompletionBlocked = true;
5523
+ }
5524
+ if (isLandingBlockedWorkerStatus(status)) {
5525
+ anyLandingBlocked = true;
5526
+ }
5527
+ if (status.finalResult && status.attention.state === "done") anyResult = true;
5153
5528
  }
5154
- const config = persistDefaultRepo(resolved.repo, existing);
5155
- return {
5156
- ok: true,
5157
- resolved: { ...resolved, persistedInConfig: true, source: "config" },
5158
- config
5159
- };
5529
+ if (anyAlive) return null;
5530
+ if (anyCompletionBlocked) return null;
5531
+ if (anyLandingBlocked) return null;
5532
+ return anyResult ? "completed" : "failed";
5160
5533
  }
5161
- function formatResolvedDefaultRepo(resolved) {
5162
- return {
5163
- defaultRepo: displayUserPath(resolved.repo),
5164
- source: resolved.source,
5165
- persistedInConfig: resolved.persistedInConfig
5166
- };
5534
+ function finalizeStaleRuns() {
5535
+ const finalized = [];
5536
+ for (const run of listRunRecords()) {
5537
+ if (!ACTIVE_RUN_STATUSES.has(run.status)) continue;
5538
+ const next = deriveTerminalRunStatus(run);
5539
+ if (!next || next === run.status) continue;
5540
+ const from = run.status;
5541
+ run.status = next;
5542
+ saveRun(run);
5543
+ finalized.push({ runId: run.id, from, to: next });
5544
+ }
5545
+ return finalized;
5167
5546
  }
5168
5547
 
5169
- // src/worktree.ts
5548
+ // src/stale-reconcile.ts
5549
+ var STALE_RECONCILE_HEARTBEAT_MS = 15 * 60 * 1e3;
5550
+ function staleReconcileDisabled() {
5551
+ return process.env.KYNVER_NO_STALE_CLEANUP === "1";
5552
+ }
5553
+ function reconcileStaleWorkers() {
5554
+ if (staleReconcileDisabled()) {
5555
+ return { workers: [], finalizedRuns: finalizeStaleRuns() };
5556
+ }
5557
+ const outcomes = [];
5558
+ const now = Date.now();
5559
+ for (const run of listRunRecords()) {
5560
+ for (const name of Object.keys(run.workers || {})) {
5561
+ const workerPath = path22.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json");
5562
+ const worker = readJson(workerPath, void 0);
5563
+ if (!worker || worker.status !== "running") {
5564
+ outcomes.push({
5565
+ runId: run.id,
5566
+ worker: name,
5567
+ action: "skipped",
5568
+ reason: worker ? `worker status is ${worker.status}` : "worker.json missing"
5569
+ });
5570
+ continue;
5571
+ }
5572
+ const status = computeWorkerStatus(worker, { base: run.base, baseCommit: run.baseCommit });
5573
+ if (status.finalResult) {
5574
+ if (worker.status === "running") {
5575
+ const nextStatus = status.attention.state === "blocked" ? "blocked" : status.attention.state === "done" || status.status === "done" ? "done" : "exited";
5576
+ worker.status = nextStatus;
5577
+ worker.reconciledAt = (/* @__PURE__ */ new Date()).toISOString();
5578
+ worker.reconcileReason = "synced finished worker record after terminal stdout/heartbeat";
5579
+ saveWorker(run.id, worker);
5580
+ outcomes.push({
5581
+ runId: run.id,
5582
+ worker: name,
5583
+ action: "marked_exited",
5584
+ reason: worker.reconcileReason
5585
+ });
5586
+ } else {
5587
+ outcomes.push({ runId: run.id, worker: name, action: "skipped", reason: "final result present" });
5588
+ }
5589
+ continue;
5590
+ }
5591
+ if (!status.alive) {
5592
+ const nextStatus = status.attention.state === "blocked" ? "blocked" : status.status === "done" ? "done" : "exited";
5593
+ worker.status = nextStatus;
5594
+ worker.reconciledAt = (/* @__PURE__ */ new Date()).toISOString();
5595
+ worker.reconcileReason = status.attention.reason;
5596
+ saveWorker(run.id, worker);
5597
+ outcomes.push({
5598
+ runId: run.id,
5599
+ worker: name,
5600
+ action: "marked_exited",
5601
+ reason: status.attention.reason
5602
+ });
5603
+ continue;
5604
+ }
5605
+ if (status.attention.state === "stale" && worker.pid && isPidAlive(worker.pid)) {
5606
+ const hbMs = status.lastHeartbeatAt ? Date.parse(status.lastHeartbeatAt) : NaN;
5607
+ const actMs = status.lastActivityAt ? Date.parse(status.lastActivityAt) : NaN;
5608
+ const hbStale = !Number.isFinite(hbMs) || now - hbMs > STALE_RECONCILE_HEARTBEAT_MS;
5609
+ const actStale = Number.isFinite(actMs) && now - actMs > STALE_MS;
5610
+ if (hbStale && actStale) {
5611
+ killWorkerProcess(worker.pid, "SIGTERM");
5612
+ worker.status = "exited";
5613
+ worker.reconciledAt = (/* @__PURE__ */ new Date()).toISOString();
5614
+ worker.reconcileReason = `reconciled stale worker: ${status.attention.reason}`;
5615
+ saveWorker(run.id, worker);
5616
+ outcomes.push({
5617
+ runId: run.id,
5618
+ worker: name,
5619
+ action: "killed_stale",
5620
+ reason: status.attention.reason
5621
+ });
5622
+ continue;
5623
+ }
5624
+ }
5625
+ outcomes.push({
5626
+ runId: run.id,
5627
+ worker: name,
5628
+ action: "skipped",
5629
+ reason: status.attention.reason
5630
+ });
5631
+ }
5632
+ }
5633
+ return { workers: outcomes, finalizedRuns: finalizeStaleRuns() };
5634
+ }
5635
+ function reconcileRunsCli() {
5636
+ const result = reconcileStaleWorkers();
5637
+ const markedExited = result.workers.filter((w) => w.action === "marked_exited").length;
5638
+ const killedStale = result.workers.filter((w) => w.action === "killed_stale").length;
5639
+ const skipped = result.workers.filter((w) => w.action === "skipped").length;
5640
+ console.log(
5641
+ JSON.stringify(
5642
+ {
5643
+ ok: true,
5644
+ workers: { markedExited, killedStale, skipped, total: result.workers.length },
5645
+ finalizedRuns: result.finalizedRuns.length,
5646
+ details: { workers: result.workers, finalizedRuns: result.finalizedRuns }
5647
+ },
5648
+ null,
5649
+ 2
5650
+ )
5651
+ );
5652
+ }
5653
+
5654
+ // src/run-list.ts
5655
+ function heartbeatByteLength(heartbeatPath) {
5656
+ if (!heartbeatPath || !existsSync17(heartbeatPath)) return 0;
5657
+ try {
5658
+ return readFileSync11(heartbeatPath, "utf8").trim().length;
5659
+ } catch {
5660
+ return 0;
5661
+ }
5662
+ }
5663
+ function workerEvidence(run, workerName) {
5664
+ const workerPath = path23.join(runDirectory(run.id), "workers", safeSlug(workerName), "worker.json");
5665
+ const worker = readJson(workerPath, void 0);
5666
+ if (!worker) {
5667
+ return {
5668
+ worker: workerName,
5669
+ workerStatus: "missing",
5670
+ attention: "needs_attention",
5671
+ attentionReason: "worker.json missing",
5672
+ missingHeartbeat: true,
5673
+ missingFinalResult: true,
5674
+ landingBlocked: false,
5675
+ completionBlocked: false
5676
+ };
5677
+ }
5678
+ const status = computeWorkerStatus(worker, { base: run.base, baseCommit: run.baseCommit });
5679
+ const missingHeartbeat = heartbeatByteLength(worker.heartbeatPath) === 0;
5680
+ const missingFinalResult = !status.finalResult && !status.alive;
5681
+ const completionBlocked = typeof worker.completionBlocker === "string" && worker.completionBlocker.length > 0;
5682
+ return {
5683
+ worker: workerName,
5684
+ workerStatus: worker.status,
5685
+ attention: status.attention.state,
5686
+ attentionReason: status.attention.reason,
5687
+ missingHeartbeat,
5688
+ missingFinalResult,
5689
+ landingBlocked: isLandingBlockedWorkerStatus(status),
5690
+ completionBlocked
5691
+ };
5692
+ }
5693
+ function deriveFinalizeBlockedReason(input) {
5694
+ if (input.openWorkerCount > 0) return "active_workers";
5695
+ if (input.workers.some((w) => w.completionBlocked)) return "completion_blocked";
5696
+ if (input.workers.some((w) => w.landingBlocked)) return "landing_blocked";
5697
+ return null;
5698
+ }
5699
+ function aggregateRunAttention(workers) {
5700
+ const rank = {
5701
+ blocked: 5,
5702
+ needs_attention: 4,
5703
+ stale: 3,
5704
+ done: 2,
5705
+ ok: 1
5706
+ };
5707
+ let best = "ok";
5708
+ let reason;
5709
+ for (const w of workers) {
5710
+ const state = w.attention;
5711
+ if ((rank[state] ?? 0) >= (rank[best] ?? 0)) {
5712
+ best = state;
5713
+ reason = w.attentionReason;
5714
+ }
5715
+ }
5716
+ return { attention: best, attentionReason: reason };
5717
+ }
5718
+ function countOpenWorkers(run) {
5719
+ let open = 0;
5720
+ for (const name of Object.keys(run.workers || {})) {
5721
+ const workerPath = path23.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json");
5722
+ const worker = readJson(workerPath, void 0);
5723
+ if (!worker) continue;
5724
+ const status = computeWorkerStatus(worker, { base: run.base, baseCommit: run.baseCommit });
5725
+ if (status.alive && !status.finalResult) open += 1;
5726
+ }
5727
+ return open;
5728
+ }
5729
+ function buildRunListRows() {
5730
+ reconcileStaleWorkers();
5731
+ return listRunRecords().map((run) => {
5732
+ const workerNames = Object.keys(run.workers || {});
5733
+ const workers = workerNames.map((name) => workerEvidence(run, name));
5734
+ const missingHeartbeatWorkers = workers.filter((w) => w.missingHeartbeat).map((w) => w.worker);
5735
+ const missingFinalResultWorkers = workers.filter((w) => w.missingFinalResult).map((w) => w.worker);
5736
+ const landingBlockedWorkers = workers.filter((w) => w.landingBlocked).map((w) => w.worker);
5737
+ const completionBlockedWorkers = workers.filter((w) => w.completionBlocked).map((w) => w.worker);
5738
+ const { attention, attentionReason } = aggregateRunAttention(workers);
5739
+ const openWorkerCount = countOpenWorkers(run);
5740
+ const effectiveStatus = deriveRunStatus(
5741
+ run.status,
5742
+ workers.map((w) => ({ attention: w.attention, status: w.workerStatus }))
5743
+ );
5744
+ return {
5745
+ id: run.id,
5746
+ name: run.name,
5747
+ status: run.status,
5748
+ effectiveStatus,
5749
+ repo: run.repo,
5750
+ createdAt: run.createdAt,
5751
+ openWorkerCount,
5752
+ attention,
5753
+ attentionReason,
5754
+ finalizeBlockedReason: deriveFinalizeBlockedReason({ run, workers, openWorkerCount }),
5755
+ evidence: {
5756
+ missingHeartbeatWorkers,
5757
+ missingFinalResultWorkers,
5758
+ landingBlockedWorkers,
5759
+ completionBlockedWorkers,
5760
+ workers
5761
+ }
5762
+ };
5763
+ });
5764
+ }
5765
+ function listRunsCli() {
5766
+ console.log(JSON.stringify(buildRunListRows(), null, 2));
5767
+ }
5768
+
5769
+ // src/default-repo.ts
5770
+ import path24 from "node:path";
5771
+ function expandConfiguredRepo(value) {
5772
+ return path24.resolve(resolveUserPath(value.trim()));
5773
+ }
5774
+ function fromConfigured(value, source, persistedInConfig) {
5775
+ const trimmed = value?.trim();
5776
+ if (!trimmed) return null;
5777
+ return {
5778
+ repo: expandConfiguredRepo(trimmed),
5779
+ source,
5780
+ persistedInConfig
5781
+ };
5782
+ }
5783
+ function resolveDefaultRepo(opts = {}) {
5784
+ const env = opts.env ?? process.env;
5785
+ const config = opts.config ?? loadUserConfig();
5786
+ const fromConfig = fromConfigured(config.defaultRepo, "config", true);
5787
+ if (fromConfig) return fromConfig;
5788
+ const fromDefaultEnv = fromConfigured(env.KYNVER_DEFAULT_REPO, "env_default_repo", false);
5789
+ if (fromDefaultEnv) return fromDefaultEnv;
5790
+ const fromHarnessEnv = fromConfigured(env.KYNVER_HARNESS_REPO, "env_harness_repo", false);
5791
+ if (fromHarnessEnv) return fromHarnessEnv;
5792
+ const discovered = discoverDefaultRepo({
5793
+ cwd: opts.cwd,
5794
+ runtimeModuleUrl: opts.runtimeModuleUrl
5795
+ });
5796
+ if (!discovered) return null;
5797
+ return {
5798
+ repo: discovered.repo,
5799
+ source: discovered.source,
5800
+ persistedInConfig: false
5801
+ };
5802
+ }
5803
+ function persistDefaultRepo(repo, existing) {
5804
+ const config = {
5805
+ ...existing ?? loadUserConfig(),
5806
+ defaultRepo: redactHomePath(path24.resolve(repo))
5807
+ };
5808
+ saveUserConfig(config);
5809
+ return config;
5810
+ }
5811
+ function remediateDefaultRepo(opts) {
5812
+ const existing = opts?.config ?? loadUserConfig();
5813
+ const resolved = resolveDefaultRepo({ ...opts, config: existing });
5814
+ if (!resolved) {
5815
+ return {
5816
+ ok: false,
5817
+ reason: "No Kynver git checkout found. Clone the repo, cd into it, then run `kynver setup --repo /path/to/Kynver` (or export KYNVER_DEFAULT_REPO)."
5818
+ };
5819
+ }
5820
+ if (resolved.persistedInConfig) {
5821
+ return { ok: true, resolved, config: existing };
5822
+ }
5823
+ const config = persistDefaultRepo(resolved.repo, existing);
5824
+ return {
5825
+ ok: true,
5826
+ resolved: { ...resolved, persistedInConfig: true, source: "config" },
5827
+ config
5828
+ };
5829
+ }
5830
+ function formatResolvedDefaultRepo(resolved) {
5831
+ return {
5832
+ defaultRepo: displayUserPath(resolved.repo),
5833
+ source: resolved.source,
5834
+ persistedInConfig: resolved.persistedInConfig
5835
+ };
5836
+ }
5837
+
5838
+ // src/worktree.ts
5170
5839
  function resolveCreateRunRepo(args) {
5171
5840
  const explicit = typeof args.repo === "string" ? args.repo.trim() : "";
5172
5841
  if (explicit) return explicit;
@@ -5180,7 +5849,7 @@ function createRun(args) {
5180
5849
  ensureGitRepo(repo);
5181
5850
  const id = args.id ? validateRunId(String(args.id)) : timestampSlug(String(args.name || "run"));
5182
5851
  const dir = runDirectory(id);
5183
- if (existsSync17(dir)) failExists(`run already exists: ${id}`);
5852
+ if (existsSync18(dir)) failExists(`run already exists: ${id}`);
5184
5853
  mkdirSync5(dir, { recursive: true });
5185
5854
  const base = String(args.base || "origin/main");
5186
5855
  const baseCommit = git(repo, ["rev-parse", base]).trim();
@@ -5194,19 +5863,11 @@ function createRun(args) {
5194
5863
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
5195
5864
  workers: {}
5196
5865
  };
5197
- writeJson(path22.join(dir, "run.json"), run);
5866
+ writeJson(path25.join(dir, "run.json"), run);
5198
5867
  console.log(JSON.stringify({ runId: id, runDir: dir, repo, base, baseCommit }, null, 2));
5199
5868
  }
5200
5869
  function listRuns() {
5201
- const { runsDir } = getPaths();
5202
- const rows = listRunIds(runsDir).map((id) => readJson(path22.join(runDirectory(id), "run.json"), void 0)).filter(Boolean).map((run) => ({
5203
- id: run.id,
5204
- name: run.name,
5205
- status: run.status,
5206
- repo: run.repo,
5207
- createdAt: run.createdAt
5208
- }));
5209
- console.log(JSON.stringify(rows, null, 2));
5870
+ listRunsCli();
5210
5871
  }
5211
5872
  function failExists(message) {
5212
5873
  console.error(message);
@@ -5214,7 +5875,7 @@ function failExists(message) {
5214
5875
  }
5215
5876
 
5216
5877
  // src/sweep.ts
5217
- import path23 from "node:path";
5878
+ import path26 from "node:path";
5218
5879
  async function sweepRun(args) {
5219
5880
  const pipeline = args.pipeline === true || args.pipeline === "true";
5220
5881
  try {
@@ -5227,7 +5888,7 @@ async function sweepRun(args) {
5227
5888
  const releasedLocalOrphans = [];
5228
5889
  for (const name of Object.keys(run.workers || {})) {
5229
5890
  const worker = readJson(
5230
- path23.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
5891
+ path26.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
5231
5892
  void 0
5232
5893
  );
5233
5894
  if (!worker || !worker.dispatched || !worker.taskId) continue;
@@ -5270,14 +5931,14 @@ async function sweepRun(args) {
5270
5931
  }
5271
5932
 
5272
5933
  // src/harness-storage-snapshot.ts
5273
- import { existsSync as existsSync19, readdirSync as readdirSync6, statSync as statSync3 } from "node:fs";
5274
- import path25 from "node:path";
5934
+ import { existsSync as existsSync20, readdirSync as readdirSync6, statSync as statSync3 } from "node:fs";
5935
+ import path28 from "node:path";
5275
5936
 
5276
5937
  // src/cleanup-dir-size.ts
5277
- import { existsSync as existsSync18, readdirSync as readdirSync5, statSync as statSync2 } from "node:fs";
5278
- import path24 from "node:path";
5938
+ import { existsSync as existsSync19, readdirSync as readdirSync5, statSync as statSync2 } from "node:fs";
5939
+ import path27 from "node:path";
5279
5940
  function directorySizeBytes(root, maxEntries = 5e4) {
5280
- if (!existsSync18(root)) return 0;
5941
+ if (!existsSync19(root)) return 0;
5281
5942
  let total = 0;
5282
5943
  let seen = 0;
5283
5944
  const stack = [root];
@@ -5291,7 +5952,7 @@ function directorySizeBytes(root, maxEntries = 5e4) {
5291
5952
  }
5292
5953
  for (const name of entries) {
5293
5954
  if (seen++ > maxEntries) return null;
5294
- const full = path24.join(current, name);
5955
+ const full = path27.join(current, name);
5295
5956
  let st;
5296
5957
  try {
5297
5958
  st = statSync2(full);
@@ -5308,10 +5969,10 @@ function directorySizeBytes(root, maxEntries = 5e4) {
5308
5969
  // src/harness-storage-snapshot.ts
5309
5970
  function harnessStorageSnapshot(opts = {}) {
5310
5971
  const harnessRoot = opts.harnessRoot ?? resolveHarnessRoot();
5311
- const worktreesDir = path25.join(harnessRoot, "worktrees");
5972
+ const worktreesDir = path28.join(harnessRoot, "worktrees");
5312
5973
  const now = opts.now ?? Date.now();
5313
5974
  const scannedAt = new Date(now).toISOString();
5314
- if (!existsSync19(worktreesDir)) {
5975
+ if (!existsSync20(worktreesDir)) {
5315
5976
  return {
5316
5977
  harnessRoot,
5317
5978
  worktreesDir,
@@ -5343,7 +6004,7 @@ function harnessStorageSnapshot(opts = {}) {
5343
6004
  for (const runEntry of entries) {
5344
6005
  if (!runEntry.isDirectory()) continue;
5345
6006
  runCount += 1;
5346
- const runPath = path25.join(worktreesDir, runEntry.name);
6007
+ const runPath = path28.join(worktreesDir, runEntry.name);
5347
6008
  try {
5348
6009
  const st = statSync3(runPath);
5349
6010
  oldestMs = oldestMs === null ? st.mtimeMs : Math.min(oldestMs, st.mtimeMs);
@@ -5377,149 +6038,58 @@ function harnessStorageSnapshot(opts = {}) {
5377
6038
  };
5378
6039
  }
5379
6040
 
5380
- // src/cleanup-orphan-safety.ts
5381
- import { existsSync as existsSync20, statSync as statSync4 } from "node:fs";
5382
- import path26 from "node:path";
6041
+ // src/cleanup.ts
6042
+ import path38 from "node:path";
6043
+
6044
+ // src/cleanup-guards.ts
6045
+ import path29 from "node:path";
6046
+
6047
+ // src/cleanup-run-liveness.ts
6048
+ function isWorkerProcessLive(indexed) {
6049
+ if (indexed.status.alive) return true;
6050
+ if (indexed.worker.status === "running") return true;
6051
+ return false;
6052
+ }
6053
+ function isRunStaleActive(indexed) {
6054
+ if (TERMINAL_RUN_STATUSES.has(indexed.run.status)) return false;
6055
+ return deriveTerminalRunStatus(indexed.run) !== null;
6056
+ }
6057
+ function runBlocksWorktreeRemoval(indexed) {
6058
+ if (isWorkerProcessLive(indexed)) return true;
6059
+ if (indexed.worker.completionBlocker) return true;
6060
+ if (TERMINAL_RUN_STATUSES.has(indexed.run.status)) return false;
6061
+ if (isRunStaleActive(indexed)) return false;
6062
+ if (!isFinishedWorkerStatus(indexed.status)) return true;
6063
+ return deriveTerminalRunStatus(indexed.run) === null;
6064
+ }
6065
+
6066
+ // src/cleanup-build-cache-paths.ts
6067
+ var HARNESS_BUILD_CACHE_RELATIVE_PATHS = [
6068
+ ".next",
6069
+ ".turbo",
6070
+ "dist",
6071
+ "build",
6072
+ ".cache",
6073
+ "node_modules/.cache"
6074
+ ];
6075
+ function isGeneratedHarnessPath(pathPart) {
6076
+ const normalized = pathPart.replace(/\\/g, "/").replace(/\/+$/, "");
6077
+ if (normalized === "node_modules" || normalized.startsWith("node_modules/")) return true;
6078
+ for (const rel of HARNESS_BUILD_CACHE_RELATIVE_PATHS) {
6079
+ if (normalized === rel || normalized.startsWith(`${rel}/`)) return true;
6080
+ }
6081
+ return false;
6082
+ }
5383
6083
 
5384
6084
  // src/cleanup-guards-helpers.ts
5385
6085
  function materialWorktreeChanges2(changedFiles) {
5386
6086
  return changedFiles.filter((line) => {
5387
6087
  const trimmed = line.trim();
5388
6088
  const pathPart = trimmed.startsWith("??") ? trimmed.slice(2).trim() : trimmed.length > 3 ? trimmed.slice(3).trim() : trimmed;
5389
- return pathPart !== "node_modules" && !pathPart.startsWith("node_modules/");
6089
+ return !isGeneratedHarnessPath(pathPart);
5390
6090
  });
5391
6091
  }
5392
6092
 
5393
- // src/cleanup-orphan-safety.ts
5394
- var DEFAULT_HEARTBEAT_FRESH_MS = 30 * 60 * 1e3;
5395
- function assessOrphanWorktreeSafety(input) {
5396
- const now = input.now ?? Date.now();
5397
- const heartbeatFreshMs = input.heartbeatFreshMs ?? DEFAULT_HEARTBEAT_FRESH_MS;
5398
- if (!existsSync20(input.worktreePath)) return null;
5399
- if (input.runId && input.workerName) {
5400
- const heartbeatPath = path26.join(
5401
- input.harnessRoot,
5402
- "runs",
5403
- input.runId,
5404
- "workers",
5405
- input.workerName,
5406
- "heartbeat.jsonl"
5407
- );
5408
- try {
5409
- const mtime = statSync4(heartbeatPath).mtimeMs;
5410
- if (now - mtime < heartbeatFreshMs) return "active_worker";
5411
- } catch {
5412
- }
5413
- }
5414
- const gitDir = path26.join(input.worktreePath, ".git");
5415
- if (!existsSync20(gitDir)) return null;
5416
- const porcelain = gitCapture(input.worktreePath, ["status", "--porcelain"]);
5417
- if (porcelain.status !== 0) return "pr_or_unmerged_commits";
5418
- const dirtyLines = porcelain.stdout.split("\n").map((line) => line.trim()).filter((line) => line.length > 0);
5419
- if (materialWorktreeChanges2(dirtyLines).length > 0) return "dirty_worktree";
5420
- const upstreamAhead = gitCapture(input.worktreePath, [
5421
- "rev-list",
5422
- "--count",
5423
- "@{u}..HEAD"
5424
- ]);
5425
- if (upstreamAhead.status === 0) {
5426
- const count = Number(upstreamAhead.stdout.trim());
5427
- if (Number.isFinite(count) && count > 0) return "pr_or_unmerged_commits";
5428
- }
5429
- const mainAhead = gitCapture(input.worktreePath, [
5430
- "rev-list",
5431
- "--count",
5432
- "origin/main..HEAD"
5433
- ]);
5434
- if (mainAhead.status !== 0) {
5435
- if (upstreamAhead.status !== 0) return "pr_or_unmerged_commits";
5436
- return null;
5437
- }
5438
- const mainCount = Number(mainAhead.stdout.trim());
5439
- if (Number.isFinite(mainCount) && mainCount > 0) return "pr_or_unmerged_commits";
5440
- return null;
5441
- }
5442
-
5443
- // src/cleanup.ts
5444
- import path31 from "node:path";
5445
-
5446
- // src/finalize.ts
5447
- import path27 from "node:path";
5448
- var ACTIVE_RUN_STATUSES = /* @__PURE__ */ new Set([
5449
- "running",
5450
- "dispatching",
5451
- "pending",
5452
- "queued",
5453
- "needs_attention"
5454
- ]);
5455
- var TERMINAL_RUN_STATUSES = /* @__PURE__ */ new Set(["completed", "failed", "cancelled", "done"]);
5456
- function deriveTerminalRunStatus(run) {
5457
- const names = Object.keys(run.workers || {});
5458
- if (names.length === 0) return "failed";
5459
- let anyAlive = false;
5460
- let anyResult = false;
5461
- let anyCompletionBlocked = false;
5462
- let anyLandingBlocked = false;
5463
- for (const name of names) {
5464
- const worker = readJson(
5465
- path27.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
5466
- void 0
5467
- );
5468
- if (!worker) continue;
5469
- const status = computeWorkerStatus(worker, {
5470
- base: run.base,
5471
- baseCommit: run.baseCommit
5472
- });
5473
- if (status.alive && !status.finalResult) {
5474
- anyAlive = true;
5475
- break;
5476
- }
5477
- if (typeof worker.completionBlocker === "string" && worker.completionBlocker) {
5478
- anyCompletionBlocked = true;
5479
- }
5480
- if (isLandingBlockedWorkerStatus(status)) {
5481
- anyLandingBlocked = true;
5482
- }
5483
- if (status.finalResult && status.attention.state === "done") anyResult = true;
5484
- }
5485
- if (anyAlive) return null;
5486
- if (anyCompletionBlocked) return null;
5487
- if (anyLandingBlocked) return null;
5488
- return anyResult ? "completed" : "failed";
5489
- }
5490
- function finalizeStaleRuns() {
5491
- const finalized = [];
5492
- for (const run of listRunRecords()) {
5493
- if (!ACTIVE_RUN_STATUSES.has(run.status)) continue;
5494
- const next = deriveTerminalRunStatus(run);
5495
- if (!next || next === run.status) continue;
5496
- const from = run.status;
5497
- run.status = next;
5498
- saveRun(run);
5499
- finalized.push({ runId: run.id, from, to: next });
5500
- }
5501
- return finalized;
5502
- }
5503
-
5504
- // src/cleanup-run-liveness.ts
5505
- function isWorkerProcessLive(indexed) {
5506
- if (indexed.status.alive) return true;
5507
- if (indexed.worker.status === "running") return true;
5508
- return false;
5509
- }
5510
- function isRunStaleActive(indexed) {
5511
- if (TERMINAL_RUN_STATUSES.has(indexed.run.status)) return false;
5512
- return deriveTerminalRunStatus(indexed.run) !== null;
5513
- }
5514
- function runBlocksWorktreeRemoval(indexed) {
5515
- if (isWorkerProcessLive(indexed)) return true;
5516
- if (indexed.worker.completionBlocker) return true;
5517
- if (TERMINAL_RUN_STATUSES.has(indexed.run.status)) return false;
5518
- if (isRunStaleActive(indexed)) return false;
5519
- if (!isFinishedWorkerStatus(indexed.status)) return true;
5520
- return deriveTerminalRunStatus(indexed.run) === null;
5521
- }
5522
-
5523
6093
  // src/cleanup-guards.ts
5524
6094
  function prUrlFromFinalResult(finalResult) {
5525
6095
  if (typeof finalResult === "string") {
@@ -5581,30 +6151,22 @@ function skipWorktreeRemoval(input) {
5581
6151
  }
5582
6152
  return null;
5583
6153
  }
5584
- function skipNodeModulesRemoval(input) {
5585
- const { indexed, includeOrphans, nodeModulesAgeMs, ageMs } = input;
5586
- if (ageMs < nodeModulesAgeMs) return "below_age_threshold";
5587
- if (!indexed) return includeOrphans ? null : "orphan_without_flag";
5588
- if (isWorkerProcessLive(indexed)) return "active_worker";
5589
- if (indexed.worker.completionBlocker) return "completion_blocked";
5590
- if (!isFinishedWorkerStatus(indexed.status)) return "run_still_active";
5591
- if (hasUnrestorableWorktreeChanges(indexed.status)) return "dirty_worktree";
5592
- const landing = assessWorkerLanding({
5593
- finalResult: indexed.status.finalResult,
5594
- changedFiles: indexed.status.changedFiles,
5595
- gitAncestry: indexed.status.gitAncestry,
5596
- prUrl: prUrlFromFinalResult(indexed.status.finalResult)
5597
- });
5598
- if (landing.blocked && materialWorktreeChanges2(indexed.status.changedFiles).length > 0) {
5599
- return "landing_blocked";
5600
- }
6154
+ function skipDependencyCacheRemoval(input) {
6155
+ const { indexed, nodeModulesAgeMs, ageMs, worktreePath, activeWorktreePaths, diskPressure } = input;
6156
+ if (!diskPressure && ageMs < nodeModulesAgeMs) return "below_age_threshold";
6157
+ if (activeWorktreePaths.has(path29.resolve(worktreePath))) return "active_worker";
6158
+ if (indexed && isWorkerProcessLive(indexed)) return "active_worker";
6159
+ if (indexed && hasUnrestorableWorktreeChanges(indexed.status)) return "dirty_worktree";
5601
6160
  return null;
5602
6161
  }
6162
+ function skipBuildCacheRemoval(input) {
6163
+ return skipDependencyCacheRemoval(input);
6164
+ }
5603
6165
 
5604
6166
  // src/cleanup-execute.ts
5605
6167
  import { existsSync as existsSync21, rmSync } from "node:fs";
5606
- import path28 from "node:path";
5607
- function removeNodeModules(candidate, execute) {
6168
+ import path30 from "node:path";
6169
+ function removeDependencyCache(candidate, execute) {
5608
6170
  if (!existsSync21(candidate.path)) {
5609
6171
  return {
5610
6172
  ...candidate,
@@ -5635,6 +6197,15 @@ function removeNodeModules(candidate, execute) {
5635
6197
  };
5636
6198
  }
5637
6199
  }
6200
+ function removeNodeModules(candidate, execute) {
6201
+ return removeDependencyCache(candidate, execute);
6202
+ }
6203
+ function removeNextCache(candidate, execute) {
6204
+ return removeDependencyCache(candidate, execute);
6205
+ }
6206
+ function removeBuildCache(candidate, execute) {
6207
+ return removeDependencyCache(candidate, execute);
6208
+ }
5638
6209
  function removeWorktree(candidate, execute) {
5639
6210
  if (!existsSync21(candidate.path)) {
5640
6211
  return {
@@ -5672,74 +6243,97 @@ function removeWorktree(candidate, execute) {
5672
6243
  };
5673
6244
  }
5674
6245
  }
6246
+ function isHarnessDependencyCachePath(targetPath, harnessRoot, worktreesDir, cacheDirName) {
6247
+ const resolved = path30.resolve(targetPath);
6248
+ const suffix = `${path30.sep}${cacheDirName}`;
6249
+ const cachePath = resolved.endsWith(suffix) ? resolved : null;
6250
+ if (!cachePath) return "path_outside_harness";
6251
+ const rel = path30.relative(worktreesDir, cachePath);
6252
+ if (rel.startsWith("..") || path30.isAbsolute(rel)) return "path_outside_harness";
6253
+ const parts = rel.split(path30.sep);
6254
+ if (parts.length < 3 || parts[parts.length - 1] !== cacheDirName) return "path_outside_harness";
6255
+ if (!resolved.startsWith(path30.resolve(harnessRoot))) return "path_outside_harness";
6256
+ return null;
6257
+ }
5675
6258
  function isHarnessNodeModulesPath(targetPath, harnessRoot, worktreesDir) {
5676
- const resolved = path28.resolve(targetPath);
5677
- const nm = resolved.endsWith(`${path28.sep}node_modules`) ? resolved : null;
5678
- if (!nm) return "path_outside_harness";
5679
- const rel = path28.relative(worktreesDir, nm);
5680
- if (rel.startsWith("..") || path28.isAbsolute(rel)) return "path_outside_harness";
5681
- const parts = rel.split(path28.sep);
5682
- if (parts.length < 3 || parts[parts.length - 1] !== "node_modules") return "path_outside_harness";
5683
- if (!resolved.startsWith(path28.resolve(harnessRoot))) return "path_outside_harness";
6259
+ return isHarnessDependencyCachePath(targetPath, harnessRoot, worktreesDir, "node_modules");
6260
+ }
6261
+ function isHarnessNextCachePath(targetPath, harnessRoot, worktreesDir) {
6262
+ return isHarnessDependencyCachePath(targetPath, harnessRoot, worktreesDir, ".next");
6263
+ }
6264
+ function isHarnessBuildCachePath(targetPath, harnessRoot, worktreesDir) {
6265
+ const resolved = path30.resolve(targetPath);
6266
+ const relToWt = path30.relative(worktreesDir, resolved);
6267
+ if (relToWt.startsWith("..") || path30.isAbsolute(relToWt)) return "path_outside_harness";
6268
+ const parts = relToWt.split(path30.sep);
6269
+ if (parts.length < 3) return "path_outside_harness";
6270
+ if (!resolved.startsWith(path30.resolve(harnessRoot))) return "path_outside_harness";
5684
6271
  return null;
5685
6272
  }
5686
6273
 
5687
6274
  // src/cleanup-scan.ts
5688
- import { existsSync as existsSync22, readdirSync as readdirSync7, statSync as statSync5 } from "node:fs";
5689
- import path29 from "node:path";
6275
+ import { existsSync as existsSync22, readdirSync as readdirSync7, statSync as statSync4 } from "node:fs";
6276
+ import path31 from "node:path";
5690
6277
  function pathAgeMs(target, now) {
5691
6278
  try {
5692
- const mtime = statSync5(target).mtimeMs;
6279
+ const mtime = statSync4(target).mtimeMs;
5693
6280
  return Math.max(0, now - mtime);
5694
6281
  } catch {
5695
6282
  return 0;
5696
6283
  }
5697
6284
  }
5698
6285
  function isPathInside(child, parent) {
5699
- const rel = path29.relative(parent, child);
5700
- return rel === "" || !rel.startsWith("..") && !path29.isAbsolute(rel);
6286
+ const rel = path31.relative(parent, child);
6287
+ return rel === "" || !rel.startsWith("..") && !path31.isAbsolute(rel);
5701
6288
  }
5702
- function scanNodeModulesCandidates(opts) {
5703
- const candidates = [];
5704
- const seen = /* @__PURE__ */ new Set();
5705
- for (const entry of opts.index.values()) {
5706
- if (opts.runIdFilter && entry.runId !== opts.runIdFilter) continue;
5707
- const nm = path29.join(entry.worktreePath, "node_modules");
5708
- if (!existsSync22(nm)) continue;
5709
- const resolved = path29.resolve(nm);
6289
+ function collectBuildCacheForWorktree(worktreePath, opts, seen, meta) {
6290
+ const out = [];
6291
+ for (const rel of HARNESS_BUILD_CACHE_RELATIVE_PATHS) {
6292
+ if (rel === ".next") continue;
6293
+ const target = path31.join(worktreePath, rel);
6294
+ if (!existsSync22(target)) continue;
6295
+ const resolved = path31.resolve(target);
5710
6296
  if (seen.has(resolved)) continue;
6297
+ if (!isPathInside(resolved, opts.harnessRoot)) continue;
5711
6298
  seen.add(resolved);
5712
- candidates.push({
5713
- kind: "remove_node_modules",
6299
+ out.push({
6300
+ kind: "remove_build_cache",
5714
6301
  path: resolved,
5715
6302
  bytes: null,
5716
- runId: entry.runId,
5717
- worker: entry.workerName,
5718
- repo: entry.run.repo,
6303
+ runId: meta.runId,
6304
+ worker: meta.worker,
6305
+ repo: meta.repo,
5719
6306
  ageMs: pathAgeMs(resolved, opts.now)
5720
6307
  });
5721
6308
  }
6309
+ return out;
6310
+ }
6311
+ function scanBuildCacheCandidates(opts) {
6312
+ const candidates = [];
6313
+ const seen = /* @__PURE__ */ new Set();
6314
+ for (const entry of opts.index.values()) {
6315
+ if (opts.runIdFilter && entry.runId !== opts.runIdFilter) continue;
6316
+ candidates.push(
6317
+ ...collectBuildCacheForWorktree(entry.worktreePath, opts, seen, {
6318
+ runId: entry.runId,
6319
+ worker: entry.workerName,
6320
+ repo: entry.run.repo
6321
+ })
6322
+ );
6323
+ }
5722
6324
  if (!opts.includeOrphans || !existsSync22(opts.worktreesDir)) return candidates;
5723
6325
  for (const runEntry of readdirSync7(opts.worktreesDir, { withFileTypes: true })) {
5724
6326
  if (!runEntry.isDirectory()) continue;
5725
- const runPath = path29.join(opts.worktreesDir, runEntry.name);
6327
+ const runPath = path31.join(opts.worktreesDir, runEntry.name);
5726
6328
  for (const workerEntry of readdirSync7(runPath, { withFileTypes: true })) {
5727
6329
  if (!workerEntry.isDirectory()) continue;
5728
- const worktreePath = path29.join(runPath, workerEntry.name);
5729
- const nm = path29.join(worktreePath, "node_modules");
5730
- if (!existsSync22(nm)) continue;
5731
- const resolved = path29.resolve(nm);
5732
- if (seen.has(resolved)) continue;
5733
- if (!isPathInside(resolved, opts.harnessRoot)) continue;
5734
- seen.add(resolved);
5735
- candidates.push({
5736
- kind: "remove_node_modules",
5737
- path: resolved,
5738
- bytes: null,
5739
- runId: runEntry.name,
5740
- worker: workerEntry.name,
5741
- ageMs: pathAgeMs(resolved, opts.now)
5742
- });
6330
+ const worktreePath = path31.join(runPath, workerEntry.name);
6331
+ candidates.push(
6332
+ ...collectBuildCacheForWorktree(worktreePath, opts, seen, {
6333
+ runId: runEntry.name,
6334
+ worker: workerEntry.name
6335
+ })
6336
+ );
5743
6337
  }
5744
6338
  }
5745
6339
  return candidates;
@@ -5771,12 +6365,12 @@ function scanWorktreeCandidates(opts) {
5771
6365
  if (!orphanEnabled || !existsSync22(opts.worktreesDir)) return candidates;
5772
6366
  const indexedPaths = /* @__PURE__ */ new Set();
5773
6367
  for (const entry of opts.index.values()) {
5774
- indexedPaths.add(path29.resolve(entry.worktreePath));
6368
+ indexedPaths.add(path31.resolve(entry.worktreePath));
5775
6369
  }
5776
6370
  for (const runEntry of readdirSync7(opts.worktreesDir, { withFileTypes: true })) {
5777
6371
  if (!runEntry.isDirectory()) continue;
5778
6372
  if (opts.runIdFilter && runEntry.name !== opts.runIdFilter) continue;
5779
- const runPath = path29.join(opts.worktreesDir, runEntry.name);
6373
+ const runPath = path31.join(opts.worktreesDir, runEntry.name);
5780
6374
  let workerEntries;
5781
6375
  try {
5782
6376
  workerEntries = readdirSync7(runPath, { withFileTypes: true });
@@ -5785,7 +6379,7 @@ function scanWorktreeCandidates(opts) {
5785
6379
  }
5786
6380
  for (const workerEntry of workerEntries) {
5787
6381
  if (!workerEntry.isDirectory()) continue;
5788
- const worktreePath = path29.resolve(path29.join(runPath, workerEntry.name));
6382
+ const worktreePath = path31.resolve(path31.join(runPath, workerEntry.name));
5789
6383
  if (seen.has(worktreePath)) continue;
5790
6384
  if (indexedPaths.has(worktreePath)) continue;
5791
6385
  if (!isPathInside(worktreePath, opts.harnessRoot)) continue;
@@ -5803,18 +6397,191 @@ function scanWorktreeCandidates(opts) {
5803
6397
  return candidates;
5804
6398
  }
5805
6399
 
6400
+ // src/cleanup-dependency-scan.ts
6401
+ import { existsSync as existsSync23, readdirSync as readdirSync8, statSync as statSync5 } from "node:fs";
6402
+ import path32 from "node:path";
6403
+ var DEPENDENCY_CACHE_DIRS = [
6404
+ { dirName: "node_modules", kind: "remove_node_modules" },
6405
+ { dirName: ".next", kind: "remove_next_cache" }
6406
+ ];
6407
+ function pathAgeMs2(target, now) {
6408
+ try {
6409
+ const mtime = statSync5(target).mtimeMs;
6410
+ return Math.max(0, now - mtime);
6411
+ } catch {
6412
+ return 0;
6413
+ }
6414
+ }
6415
+ function isPathInside2(child, parent) {
6416
+ const rel = path32.relative(parent, child);
6417
+ return rel === "" || !rel.startsWith("..") && !path32.isAbsolute(rel);
6418
+ }
6419
+ function pushCandidate2(candidates, seen, opts, targetPath, kind, meta) {
6420
+ if (!existsSync23(targetPath)) return;
6421
+ const resolved = path32.resolve(targetPath);
6422
+ if (seen.has(resolved)) return;
6423
+ if (!isPathInside2(resolved, opts.harnessRoot)) return;
6424
+ seen.add(resolved);
6425
+ candidates.push({
6426
+ kind,
6427
+ path: resolved,
6428
+ bytes: null,
6429
+ harnessRoot: opts.harnessRoot,
6430
+ runId: meta.runId,
6431
+ worker: meta.worker,
6432
+ repo: meta.repo,
6433
+ ageMs: pathAgeMs2(resolved, opts.now)
6434
+ });
6435
+ }
6436
+ function scanWorktreeDependencyCaches(candidates, seen, opts, worktreePath, meta) {
6437
+ for (const entry of DEPENDENCY_CACHE_DIRS) {
6438
+ pushCandidate2(candidates, seen, opts, path32.join(worktreePath, entry.dirName), entry.kind, meta);
6439
+ }
6440
+ }
6441
+ function scanDependencyCacheCandidates(opts) {
6442
+ const candidates = [];
6443
+ const seen = /* @__PURE__ */ new Set();
6444
+ for (const entry of opts.index.values()) {
6445
+ if (opts.runIdFilter && entry.runId !== opts.runIdFilter) continue;
6446
+ scanWorktreeDependencyCaches(candidates, seen, opts, entry.worktreePath, {
6447
+ runId: entry.runId,
6448
+ worker: entry.workerName,
6449
+ repo: entry.run.repo
6450
+ });
6451
+ }
6452
+ if (!existsSync23(opts.worktreesDir)) return candidates;
6453
+ for (const runEntry of readdirSync8(opts.worktreesDir, { withFileTypes: true })) {
6454
+ if (!runEntry.isDirectory()) continue;
6455
+ if (opts.runIdFilter && runEntry.name !== opts.runIdFilter) continue;
6456
+ const runPath = path32.join(opts.worktreesDir, runEntry.name);
6457
+ let workerEntries;
6458
+ try {
6459
+ workerEntries = readdirSync8(runPath, { withFileTypes: true });
6460
+ } catch {
6461
+ continue;
6462
+ }
6463
+ for (const workerEntry of workerEntries) {
6464
+ if (!workerEntry.isDirectory()) continue;
6465
+ const worktreePath = path32.join(runPath, workerEntry.name);
6466
+ scanWorktreeDependencyCaches(candidates, seen, opts, worktreePath, {
6467
+ runId: runEntry.name,
6468
+ worker: workerEntry.name
6469
+ });
6470
+ }
6471
+ }
6472
+ return candidates;
6473
+ }
6474
+
6475
+ // src/cleanup-duplicate-worktrees.ts
6476
+ import { existsSync as existsSync24, statSync as statSync6 } from "node:fs";
6477
+ import path33 from "node:path";
6478
+ function pathAgeMs3(target, now) {
6479
+ try {
6480
+ const mtime = statSync6(target).mtimeMs;
6481
+ return Math.max(0, now - mtime);
6482
+ } catch {
6483
+ return 0;
6484
+ }
6485
+ }
6486
+ function parseWorktreePorcelain(output) {
6487
+ const records = [];
6488
+ let current = null;
6489
+ for (const line of output.split("\n")) {
6490
+ if (!line.trim()) continue;
6491
+ const [key, ...rest] = line.split(" ");
6492
+ const value = rest.join(" ");
6493
+ if (key === "worktree") {
6494
+ if (current) records.push(current);
6495
+ current = { path: value };
6496
+ continue;
6497
+ }
6498
+ if (!current) continue;
6499
+ if (key === "branch") current.branch = value;
6500
+ if (key === "HEAD") current.head = value;
6501
+ if (key === "bare") current.bare = true;
6502
+ }
6503
+ if (current) records.push(current);
6504
+ return records;
6505
+ }
6506
+ function isUnderWorktreesDir(worktreePath, worktreesDir) {
6507
+ const rel = path33.relative(path33.resolve(worktreesDir), path33.resolve(worktreePath));
6508
+ return rel !== "" && !rel.startsWith("..") && !path33.isAbsolute(rel);
6509
+ }
6510
+ function isCleanWorktree(worktreePath, repoRoot) {
6511
+ try {
6512
+ const porcelain = git(repoRoot, ["-C", worktreePath, "status", "--porcelain"], {
6513
+ allowFailure: true
6514
+ });
6515
+ return !String(porcelain || "").trim();
6516
+ } catch {
6517
+ return false;
6518
+ }
6519
+ }
6520
+ function scanDuplicateWorktreeCandidates(opts) {
6521
+ if (!opts.includeOrphans || !existsSync24(opts.worktreesDir)) return [];
6522
+ const repos = /* @__PURE__ */ new Set();
6523
+ for (const entry of opts.index.values()) {
6524
+ if (entry.run.repo) repos.add(path33.resolve(entry.run.repo));
6525
+ }
6526
+ const indexedPaths = /* @__PURE__ */ new Set();
6527
+ for (const entry of opts.index.values()) {
6528
+ indexedPaths.add(path33.resolve(entry.worktreePath));
6529
+ }
6530
+ const candidates = [];
6531
+ const seen = /* @__PURE__ */ new Set();
6532
+ for (const repoRoot of repos) {
6533
+ let porcelain;
6534
+ try {
6535
+ porcelain = git(repoRoot, ["worktree", "list", "--porcelain"], { allowFailure: true });
6536
+ } catch {
6537
+ continue;
6538
+ }
6539
+ const worktrees = parseWorktreePorcelain(porcelain);
6540
+ for (const wt of worktrees) {
6541
+ const resolved = path33.resolve(wt.path);
6542
+ if (resolved === path33.resolve(repoRoot)) continue;
6543
+ if (!isUnderWorktreesDir(resolved, opts.worktreesDir)) continue;
6544
+ if (indexedPaths.has(resolved)) continue;
6545
+ if (seen.has(resolved)) continue;
6546
+ if (!existsSync24(resolved)) continue;
6547
+ if (!isCleanWorktree(resolved, repoRoot)) continue;
6548
+ const rel = path33.relative(opts.worktreesDir, resolved);
6549
+ const parts = rel.split(path33.sep);
6550
+ const runId = parts[0];
6551
+ const worker = parts[1] ?? "unknown";
6552
+ seen.add(resolved);
6553
+ candidates.push({
6554
+ kind: "remove_worktree",
6555
+ path: resolved,
6556
+ bytes: null,
6557
+ runId,
6558
+ worker,
6559
+ repo: repoRoot,
6560
+ ageMs: pathAgeMs3(resolved, opts.now)
6561
+ });
6562
+ }
6563
+ }
6564
+ return candidates;
6565
+ }
6566
+
5806
6567
  // src/cleanup-worktree-index.ts
5807
- import path30 from "node:path";
5808
- function buildWorktreeIndex() {
6568
+ import path34 from "node:path";
6569
+ function buildWorktreeIndexAt(harnessRoot) {
5809
6570
  const index = /* @__PURE__ */ new Map();
5810
- for (const run of listRunRecords()) {
6571
+ for (const run of listRunRecordsForHarnessRoot(harnessRoot)) {
5811
6572
  for (const name of Object.keys(run.workers || {})) {
5812
- const workerPath = path30.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json");
6573
+ const workerPath = path34.join(
6574
+ runDirectoryAt(harnessRoot, run.id),
6575
+ "workers",
6576
+ safeSlug(name),
6577
+ "worker.json"
6578
+ );
5813
6579
  const worker = readJson(workerPath, void 0);
5814
6580
  if (!worker?.worktreePath) continue;
5815
6581
  const status = computeWorkerStatus(worker, { base: run.base, baseCommit: run.baseCommit });
5816
- index.set(path30.resolve(worker.worktreePath), {
5817
- worktreePath: path30.resolve(worker.worktreePath),
6582
+ index.set(path34.resolve(worker.worktreePath), {
6583
+ harnessRoot,
6584
+ worktreePath: path34.resolve(worker.worktreePath),
5818
6585
  runId: run.id,
5819
6586
  workerName: name,
5820
6587
  run,
@@ -5872,12 +6639,162 @@ function resolvePipelineHarnessRetention(runId) {
5872
6639
  });
5873
6640
  }
5874
6641
 
5875
- // src/cleanup.ts
5876
- function resolvePaths(options = {}) {
5877
- const harnessRoot = options.harnessRoot ? resolveUserPath(options.harnessRoot) : resolveHarnessRoot();
5878
- const { worktreesDir } = options.harnessRoot ? { worktreesDir: path31.join(harnessRoot, "worktrees") } : getHarnessPaths();
5879
- const now = options.now ?? Date.now();
5880
- return { harnessRoot, worktreesDir, now };
6642
+ // src/cleanup-orphan-safety.ts
6643
+ import { existsSync as existsSync25, statSync as statSync7 } from "node:fs";
6644
+ import path35 from "node:path";
6645
+ var DEFAULT_HEARTBEAT_FRESH_MS = 30 * 60 * 1e3;
6646
+ function assessOrphanWorktreeSafety(input) {
6647
+ const now = input.now ?? Date.now();
6648
+ const heartbeatFreshMs = input.heartbeatFreshMs ?? DEFAULT_HEARTBEAT_FRESH_MS;
6649
+ if (!existsSync25(input.worktreePath)) return null;
6650
+ if (input.runId && input.workerName) {
6651
+ const heartbeatPath = path35.join(
6652
+ input.harnessRoot,
6653
+ "runs",
6654
+ input.runId,
6655
+ "workers",
6656
+ input.workerName,
6657
+ "heartbeat.jsonl"
6658
+ );
6659
+ try {
6660
+ const mtime = statSync7(heartbeatPath).mtimeMs;
6661
+ if (now - mtime < heartbeatFreshMs) return "active_worker";
6662
+ } catch {
6663
+ }
6664
+ }
6665
+ const gitDir = path35.join(input.worktreePath, ".git");
6666
+ if (!existsSync25(gitDir)) return null;
6667
+ const porcelain = gitCapture(input.worktreePath, ["status", "--porcelain"]);
6668
+ if (porcelain.status !== 0) return "pr_or_unmerged_commits";
6669
+ const dirtyLines = porcelain.stdout.split("\n").map((line) => line.trim()).filter((line) => line.length > 0);
6670
+ if (materialWorktreeChanges2(dirtyLines).length > 0) return "dirty_worktree";
6671
+ const upstreamAhead = gitCapture(input.worktreePath, [
6672
+ "rev-list",
6673
+ "--count",
6674
+ "@{u}..HEAD"
6675
+ ]);
6676
+ if (upstreamAhead.status === 0) {
6677
+ const count = Number(upstreamAhead.stdout.trim());
6678
+ if (Number.isFinite(count) && count > 0) return "pr_or_unmerged_commits";
6679
+ }
6680
+ const mainAhead = gitCapture(input.worktreePath, [
6681
+ "rev-list",
6682
+ "--count",
6683
+ "origin/main..HEAD"
6684
+ ]);
6685
+ if (mainAhead.status !== 0) {
6686
+ if (upstreamAhead.status !== 0) return "pr_or_unmerged_commits";
6687
+ return null;
6688
+ }
6689
+ const mainCount = Number(mainAhead.stdout.trim());
6690
+ if (Number.isFinite(mainCount) && mainCount > 0) return "pr_or_unmerged_commits";
6691
+ return null;
6692
+ }
6693
+
6694
+ // src/cleanup-harness-roots.ts
6695
+ import { existsSync as existsSync26 } from "node:fs";
6696
+ import { homedir as homedir6 } from "node:os";
6697
+ import path36 from "node:path";
6698
+ var WELL_KNOWN_HARNESS_SCAN_ROOTS = [
6699
+ "/var/tmp/kynver-harness",
6700
+ path36.join(homedir6(), ".openclaw", "harness")
6701
+ ];
6702
+ function addRoot(seen, roots, candidate) {
6703
+ if (!candidate?.trim()) return;
6704
+ const resolved = path36.resolve(resolveUserPath(candidate.trim()));
6705
+ if (seen.has(resolved)) return;
6706
+ seen.add(resolved);
6707
+ roots.push(resolved);
6708
+ }
6709
+ function shouldScanWellKnownRoots(options) {
6710
+ if (options.scanWellKnown != null) return options.scanWellKnown;
6711
+ if (process.env.VITEST === "true") return false;
6712
+ return process.env.KYNVER_CLEANUP_SCAN_WELL_KNOWN !== "0" && !["0", "false", "no"].includes((process.env.KYNVER_CLEANUP_SCAN_WELL_KNOWN ?? "").toLowerCase());
6713
+ }
6714
+ function resolveHarnessScanRoots(options = {}) {
6715
+ const seen = /* @__PURE__ */ new Set();
6716
+ const roots = [];
6717
+ addRoot(seen, roots, options.harnessRoot ?? resolveHarnessRoot());
6718
+ const extra = process.env.KYNVER_CLEANUP_EXTRA_ROOTS?.split(",").map((part) => part.trim()).filter(Boolean);
6719
+ for (const candidate of extra ?? []) addRoot(seen, roots, candidate);
6720
+ if (shouldScanWellKnownRoots(options)) {
6721
+ for (const candidate of WELL_KNOWN_HARNESS_SCAN_ROOTS) {
6722
+ const resolved = path36.resolve(candidate);
6723
+ if (!seen.has(resolved) && existsSync26(resolved)) addRoot(seen, roots, resolved);
6724
+ }
6725
+ }
6726
+ return roots;
6727
+ }
6728
+
6729
+ // src/cleanup-active-worktrees.ts
6730
+ import path37 from "node:path";
6731
+ function isActiveHarnessWorker2(worker, runBase, runBaseCommit) {
6732
+ const status = computeWorkerStatus(worker, { base: runBase, baseCommit: runBaseCommit });
6733
+ return status.alive && !status.finalResult && status.attention.state !== "done";
6734
+ }
6735
+ function collectActiveWorktreeGuards(harnessRoots) {
6736
+ const activeWorktreePaths = /* @__PURE__ */ new Set();
6737
+ const liveRunKeys = /* @__PURE__ */ new Set();
6738
+ for (const harnessRoot of harnessRoots) {
6739
+ for (const run of listRunRecordsForHarnessRoot(harnessRoot)) {
6740
+ let runHasLive = false;
6741
+ for (const name of Object.keys(run.workers || {})) {
6742
+ const worker = readJson(
6743
+ path37.join(runDirectoryAt(harnessRoot, run.id), "workers", safeSlug(name), "worker.json"),
6744
+ void 0
6745
+ );
6746
+ if (!worker?.worktreePath) continue;
6747
+ const worktreePath = path37.resolve(worker.worktreePath);
6748
+ if (!isActiveHarnessWorker2(worker, run.base, run.baseCommit)) continue;
6749
+ runHasLive = true;
6750
+ activeWorktreePaths.add(worktreePath);
6751
+ }
6752
+ if (runHasLive) liveRunKeys.add(`${harnessRoot}\0${run.id}`);
6753
+ }
6754
+ }
6755
+ return { activeWorktreePaths, liveRunKeys };
6756
+ }
6757
+
6758
+ // src/cleanup-disk-pressure.ts
6759
+ function envFlag2(name) {
6760
+ const v = process.env[name];
6761
+ return v === "1" || v === "true" || v === "yes";
6762
+ }
6763
+ function envNumber(name, fallback) {
6764
+ const raw = process.env[name];
6765
+ if (!raw) return fallback;
6766
+ const n = Number(raw);
6767
+ return Number.isFinite(n) ? n : fallback;
6768
+ }
6769
+ function observeCleanupDiskPressure(input = {}) {
6770
+ const diskPath = input.diskPath?.trim() || process.env.KYNVER_DISK_GUARD_PATH?.trim() || "/";
6771
+ const maxUsedPercent = envNumber("KYNVER_DISK_GUARD_MAX_USED_PERCENT", 75);
6772
+ const diskGate = observeRunnerDiskGate({
6773
+ ...input,
6774
+ diskPath,
6775
+ diskMaxUsedPercent: input.diskMaxUsedPercent ?? maxUsedPercent
6776
+ });
6777
+ const pressured = !diskGate.ok || diskGate.usedPercent >= maxUsedPercent;
6778
+ return { diskGate, pressured, maxUsedPercent };
6779
+ }
6780
+ function applyDiskPressureToRetention(retention, pressure) {
6781
+ if (!pressure.pressured) return retention;
6782
+ const executeOnPressure = retention.execute || envFlag2("KYNVER_CLEANUP_EXECUTE_ON_PRESSURE");
6783
+ return {
6784
+ ...retention,
6785
+ execute: executeOnPressure,
6786
+ nodeModulesAgeMs: 0,
6787
+ diskPressure: true,
6788
+ diskGate: pressure.diskGate
6789
+ };
6790
+ }
6791
+
6792
+ // src/cleanup.ts
6793
+ function resolvePaths(options = {}) {
6794
+ const harnessRoot = options.harnessRoot ? resolveUserPath(options.harnessRoot) : resolveHarnessRoot();
6795
+ const scanRoots = resolveHarnessScanRoots({ harnessRoot });
6796
+ const now = options.now ?? Date.now();
6797
+ return { harnessRoot, scanRoots, now };
5881
6798
  }
5882
6799
  function normalizeGuardSkip(skip) {
5883
6800
  if (typeof skip === "string") return { reason: skip };
@@ -5902,72 +6819,145 @@ function tallySkipReasons(actions, skips) {
5902
6819
  }
5903
6820
  return counts;
5904
6821
  }
6822
+ function removeDependencyCacheAction(candidate, execute) {
6823
+ if (candidate.kind === "remove_next_cache") return removeNextCache(candidate, execute);
6824
+ return removeNodeModules(candidate, execute);
6825
+ }
6826
+ function pathGuardForDependencyCache(candidate, harnessRoot, worktreesDir) {
6827
+ if (candidate.kind === "remove_next_cache") {
6828
+ return isHarnessNextCachePath(candidate.path, harnessRoot, worktreesDir);
6829
+ }
6830
+ return isHarnessNodeModulesPath(candidate.path, harnessRoot, worktreesDir);
6831
+ }
6832
+ function mergeWorktreeIndexes(scanRoots) {
6833
+ const merged = /* @__PURE__ */ new Map();
6834
+ for (const root of scanRoots) {
6835
+ for (const [key, value] of buildWorktreeIndexAt(root)) merged.set(key, value);
6836
+ }
6837
+ return merged;
6838
+ }
6839
+ function worktreePathForCandidate(candidate, worktreesDir) {
6840
+ if (candidate.runId && candidate.worker) {
6841
+ return path38.join(worktreesDir, candidate.runId, candidate.worker);
6842
+ }
6843
+ return path38.resolve(candidate.path, "..");
6844
+ }
5905
6845
  function runHarnessCleanup(options = {}) {
5906
- const retention = resolveHarnessRetention(options);
6846
+ let retention = resolveHarnessRetention(options);
6847
+ const diskPressure = observeCleanupDiskPressure();
6848
+ retention = applyDiskPressureToRetention(retention, diskPressure);
5907
6849
  const paths = resolvePaths(options);
6850
+ const activeGuards = collectActiveWorktreeGuards(paths.scanRoots);
6851
+ const index = mergeWorktreeIndexes(paths.scanRoots);
5908
6852
  const finalizedRuns = retention.finalizeStaleRuns ? finalizeStaleRuns().map((f) => ({ runId: f.runId, from: f.from, to: f.to })) : [];
5909
- const index = buildWorktreeIndex();
5910
- const scanOpts = {
5911
- harnessRoot: paths.harnessRoot,
5912
- worktreesDir: paths.worktreesDir,
5913
- nodeModulesAgeMs: retention.nodeModulesAgeMs,
5914
- worktreesAgeMs: retention.worktreesAgeMs,
5915
- includeOrphans: retention.includeOrphans,
5916
- runIdFilter: retention.runIdFilter,
5917
- index,
5918
- now: paths.now
5919
- };
5920
6853
  const skips = [];
5921
6854
  const actions = [];
5922
- for (const raw of scanNodeModulesCandidates(scanOpts)) {
5923
- const candidate = attachCandidateBytes(raw, retention.accountBytes);
5924
- const pathSkip = isHarnessNodeModulesPath(candidate.path, paths.harnessRoot, paths.worktreesDir);
5925
- if (pathSkip) {
5926
- recordSkip(skips, candidate.path, pathSkip);
5927
- actions.push({ ...candidate, executed: false, skipped: true, skipReason: pathSkip });
5928
- continue;
5929
- }
5930
- const worktreePath = path31.resolve(candidate.path, "..");
5931
- const indexed = index.get(worktreePath) ?? null;
5932
- const guardReason = skipNodeModulesRemoval({
5933
- indexed,
5934
- includeOrphans: retention.includeOrphans,
6855
+ const processedPaths = /* @__PURE__ */ new Set();
6856
+ for (const harnessRoot of paths.scanRoots) {
6857
+ const worktreesDir = path38.join(harnessRoot, "worktrees");
6858
+ const scanOpts = {
6859
+ harnessRoot,
6860
+ worktreesDir,
5935
6861
  nodeModulesAgeMs: retention.nodeModulesAgeMs,
5936
- ageMs: candidate.ageMs
5937
- });
5938
- if (guardReason) {
5939
- recordSkip(skips, candidate.path, guardReason);
5940
- actions.push({ ...candidate, executed: false, skipped: true, skipReason: guardReason });
5941
- continue;
5942
- }
5943
- actions.push(removeNodeModules(candidate, retention.execute));
5944
- }
5945
- for (const raw of scanWorktreeCandidates(scanOpts)) {
5946
- const candidate = attachCandidateBytes(raw, retention.accountBytes);
5947
- const indexed = index.get(path31.resolve(candidate.path)) ?? null;
5948
- const orphanSafety = indexed ? null : assessOrphanWorktreeSafety({
5949
- worktreePath: candidate.path,
5950
- harnessRoot: paths.harnessRoot,
5951
- runId: candidate.runId,
5952
- workerName: candidate.worker,
5953
- now: paths.now
5954
- });
5955
- const guardSkip = skipWorktreeRemoval({
5956
- indexed,
5957
- worktreePath: path31.resolve(candidate.path),
5958
- includeOrphans: retention.includeOrphans,
5959
6862
  worktreesAgeMs: retention.worktreesAgeMs,
5960
- ageMs: candidate.ageMs,
5961
- orphanSafety,
5962
- worktreeRemovalGuard: options.worktreeRemovalGuard
5963
- });
5964
- if (guardSkip) {
5965
- const { reason: guardReason, detail: guardDetail } = normalizeGuardSkip(guardSkip);
5966
- recordSkip(skips, candidate.path, guardReason, guardDetail);
5967
- actions.push({ ...candidate, executed: false, skipped: true, skipReason: guardReason });
5968
- continue;
6863
+ includeOrphans: retention.includeOrphans,
6864
+ runIdFilter: retention.runIdFilter,
6865
+ index,
6866
+ now: paths.now
6867
+ };
6868
+ for (const raw of scanDependencyCacheCandidates(scanOpts)) {
6869
+ const candidate = attachCandidateBytes(raw, retention.accountBytes);
6870
+ const resolved = path38.resolve(candidate.path);
6871
+ if (processedPaths.has(resolved)) continue;
6872
+ processedPaths.add(resolved);
6873
+ const pathSkip = pathGuardForDependencyCache(candidate, harnessRoot, worktreesDir);
6874
+ if (pathSkip) {
6875
+ recordSkip(skips, candidate.path, pathSkip);
6876
+ actions.push({ ...candidate, executed: false, skipped: true, skipReason: pathSkip });
6877
+ continue;
6878
+ }
6879
+ const worktreePath = worktreePathForCandidate(candidate, worktreesDir);
6880
+ const indexed = index.get(path38.resolve(worktreePath)) ?? null;
6881
+ const guardReason = skipDependencyCacheRemoval({
6882
+ indexed,
6883
+ includeOrphans: true,
6884
+ nodeModulesAgeMs: retention.nodeModulesAgeMs,
6885
+ ageMs: candidate.ageMs,
6886
+ worktreePath,
6887
+ activeWorktreePaths: activeGuards.activeWorktreePaths,
6888
+ diskPressure: retention.diskPressure
6889
+ });
6890
+ if (guardReason) {
6891
+ recordSkip(skips, candidate.path, guardReason);
6892
+ actions.push({ ...candidate, executed: false, skipped: true, skipReason: guardReason });
6893
+ continue;
6894
+ }
6895
+ actions.push(removeDependencyCacheAction(candidate, retention.execute));
6896
+ }
6897
+ for (const raw of scanBuildCacheCandidates(scanOpts)) {
6898
+ const candidate = attachCandidateBytes(raw, retention.accountBytes);
6899
+ const resolved = path38.resolve(candidate.path);
6900
+ if (processedPaths.has(resolved)) continue;
6901
+ processedPaths.add(resolved);
6902
+ const pathSkip = isHarnessBuildCachePath(candidate.path, harnessRoot, worktreesDir);
6903
+ if (pathSkip) {
6904
+ recordSkip(skips, candidate.path, pathSkip);
6905
+ actions.push({ ...candidate, executed: false, skipped: true, skipReason: pathSkip });
6906
+ continue;
6907
+ }
6908
+ const worktreePath = worktreePathForCandidate(candidate, worktreesDir);
6909
+ const indexed = index.get(path38.resolve(worktreePath)) ?? null;
6910
+ const guardReason = skipBuildCacheRemoval({
6911
+ indexed,
6912
+ includeOrphans: true,
6913
+ nodeModulesAgeMs: retention.nodeModulesAgeMs,
6914
+ ageMs: candidate.ageMs,
6915
+ worktreePath,
6916
+ activeWorktreePaths: activeGuards.activeWorktreePaths,
6917
+ diskPressure: retention.diskPressure
6918
+ });
6919
+ if (guardReason) {
6920
+ recordSkip(skips, candidate.path, guardReason);
6921
+ actions.push({ ...candidate, executed: false, skipped: true, skipReason: guardReason });
6922
+ continue;
6923
+ }
6924
+ actions.push(removeBuildCache(candidate, retention.execute));
6925
+ }
6926
+ const worktreeCandidates = [
6927
+ ...scanWorktreeCandidates(scanOpts),
6928
+ ...scanDuplicateWorktreeCandidates(scanOpts)
6929
+ ];
6930
+ const worktreeSeen = /* @__PURE__ */ new Set();
6931
+ for (const raw of worktreeCandidates) {
6932
+ const resolved = path38.resolve(raw.path);
6933
+ if (worktreeSeen.has(resolved)) continue;
6934
+ worktreeSeen.add(resolved);
6935
+ const candidate = attachCandidateBytes({ ...raw, path: resolved }, retention.accountBytes);
6936
+ const indexed = index.get(path38.resolve(candidate.path)) ?? null;
6937
+ const orphanSafety = indexed ? null : assessOrphanWorktreeSafety({
6938
+ worktreePath: candidate.path,
6939
+ harnessRoot,
6940
+ runId: candidate.runId,
6941
+ workerName: candidate.worker,
6942
+ now: paths.now
6943
+ });
6944
+ const guardSkip = skipWorktreeRemoval({
6945
+ indexed,
6946
+ worktreePath: path38.resolve(candidate.path),
6947
+ includeOrphans: retention.includeOrphans,
6948
+ worktreesAgeMs: retention.worktreesAgeMs,
6949
+ ageMs: candidate.ageMs,
6950
+ orphanSafety,
6951
+ worktreeRemovalGuard: options.worktreeRemovalGuard
6952
+ });
6953
+ if (guardSkip) {
6954
+ const { reason: guardReason, detail: guardDetail } = normalizeGuardSkip(guardSkip);
6955
+ recordSkip(skips, candidate.path, guardReason, guardDetail);
6956
+ actions.push({ ...candidate, executed: false, skipped: true, skipReason: guardReason });
6957
+ continue;
6958
+ }
6959
+ actions.push(removeWorktree(candidate, retention.execute));
5969
6960
  }
5970
- actions.push(removeWorktree(candidate, retention.execute));
5971
6961
  }
5972
6962
  let candidateBytes = 0;
5973
6963
  let reclaimableBytes = 0;
@@ -5988,11 +6978,20 @@ function runHarnessCleanup(options = {}) {
5988
6978
  const storage = retention.accountBytes ? harnessStorageSnapshot({ harnessRoot: paths.harnessRoot, now: paths.now }) : void 0;
5989
6979
  return {
5990
6980
  harnessRoot: paths.harnessRoot,
6981
+ scanRoots: paths.scanRoots,
5991
6982
  dryRun: !retention.execute,
5992
6983
  execute: retention.execute,
5993
6984
  nodeModulesAgeMs: retention.nodeModulesAgeMs,
5994
6985
  worktreesAgeMs: retention.worktreesAgeMs,
5995
6986
  includeOrphans: retention.includeOrphans,
6987
+ diskPressure: retention.diskPressure,
6988
+ diskGate: retention.diskGate ? {
6989
+ ok: retention.diskGate.ok,
6990
+ path: retention.diskGate.path,
6991
+ freeBytes: retention.diskGate.freeBytes,
6992
+ usedPercent: retention.diskGate.usedPercent,
6993
+ reason: retention.diskGate.reason
6994
+ } : void 0,
5996
6995
  scannedAt: new Date(paths.now).toISOString(),
5997
6996
  finalizedRuns,
5998
6997
  actions,
@@ -6029,8 +7028,8 @@ import { mkdirSync as mkdirSync7, realpathSync } from "node:fs";
6029
7028
  import { fileURLToPath as fileURLToPath5 } from "node:url";
6030
7029
 
6031
7030
  // src/discard-disposable.ts
6032
- import { existsSync as existsSync23, rmSync as rmSync2 } from "node:fs";
6033
- import path32 from "node:path";
7031
+ import { existsSync as existsSync27, rmSync as rmSync2 } from "node:fs";
7032
+ import path39 from "node:path";
6034
7033
  function normalizeRelativePath2(value) {
6035
7034
  const normalized = value.replace(/\\/g, "/").replace(/^\.\//, "").trim();
6036
7035
  if (!normalized || normalized.startsWith("/") || normalized.includes("..")) {
@@ -6051,15 +7050,15 @@ function discardDisposableArtifacts(args) {
6051
7050
  if (paths.length === 0) {
6052
7051
  return { ok: false, removed: [], reason: "requires at least one --path" };
6053
7052
  }
6054
- const worktreeRoot = path32.resolve(worker.worktreePath);
7053
+ const worktreeRoot = path39.resolve(worker.worktreePath);
6055
7054
  const removed = [];
6056
7055
  for (const raw of paths) {
6057
7056
  const rel = normalizeRelativePath2(raw);
6058
- const abs = path32.resolve(worktreeRoot, rel);
6059
- if (!abs.startsWith(worktreeRoot + path32.sep) && abs !== worktreeRoot) {
7057
+ const abs = path39.resolve(worktreeRoot, rel);
7058
+ if (!abs.startsWith(worktreeRoot + path39.sep) && abs !== worktreeRoot) {
6060
7059
  return { ok: false, removed, reason: `path escapes worktree: ${raw}` };
6061
7060
  }
6062
- if (!existsSync23(abs)) {
7061
+ if (!existsSync27(abs)) {
6063
7062
  return { ok: false, removed, reason: `path not found: ${raw}` };
6064
7063
  }
6065
7064
  rmSync2(abs, { recursive: true, force: true });
@@ -6082,195 +7081,88 @@ function discardDisposableCli(args) {
6082
7081
  }
6083
7082
 
6084
7083
  // src/pipeline-tick.ts
6085
- import path36 from "node:path";
7084
+ import path42 from "node:path";
6086
7085
 
6087
7086
  // src/pipeline-dispatch.ts
6088
7087
  var RESERVED_REVIEW_STARTS = 1;
6089
- function countDispatchStarts(result) {
6090
- if (!result || typeof result !== "object") return 0;
6091
- const startedCount = result.startedCount;
6092
- if (typeof startedCount === "number") return startedCount;
6093
- const outcomes = result.outcomes;
6094
- if (!Array.isArray(outcomes)) return 0;
6095
- return outcomes.filter((o) => o.started).length;
6096
- }
6097
- function stripCliMaxStarts(args) {
6098
- const { maxStarts: _maxStarts, ...rest } = args;
6099
- return rest;
6100
- }
6101
- async function runPipelineDispatch(args, slots) {
6102
- if (slots <= 0) {
6103
- return { ok: true, skipped: true, reason: "no slots", maxStarts: 0, startedCount: 0 };
6104
- }
6105
- const base = stripCliMaxStarts(args);
6106
- const reviewBudget = Math.min(slots, RESERVED_REVIEW_STARTS);
6107
- const workBudget = Math.max(0, slots - reviewBudget);
6108
- const review = await dispatchRun({
6109
- ...base,
6110
- execute: true,
6111
- pipeline: true,
6112
- lane: "review",
6113
- maxStarts: String(reviewBudget)
6114
- });
6115
- const reviewStarted = countDispatchStarts(review);
6116
- const workSlots = workBudget + (reviewBudget - reviewStarted);
6117
- if (workSlots <= 0) {
6118
- return {
6119
- ...typeof review === "object" && review !== null ? review : {},
6120
- passes: { review },
6121
- startedCount: reviewStarted
6122
- };
6123
- }
6124
- const work = await dispatchRun({
6125
- ...base,
6126
- execute: true,
6127
- pipeline: true,
6128
- maxStarts: String(workSlots)
6129
- });
6130
- const workStarted = countDispatchStarts(work);
6131
- return {
6132
- passes: { review, work },
6133
- startedCount: reviewStarted + workStarted,
6134
- ok: true
6135
- };
6136
- }
6137
-
6138
- // src/pipeline-max-starts.ts
6139
- function operatorDispatchFromTick(operatorTick) {
6140
- const body = operatorTick;
6141
- const dispatch = body.response?.dispatch;
6142
- return dispatch && typeof dispatch === "object" ? dispatch : null;
6143
- }
6144
- function resolvePipelineMaxStarts(resourceGate, operatorTick) {
6145
- const dispatch = operatorDispatchFromTick(operatorTick);
6146
- const advised = typeof dispatch?.recommendedMaxStarts === "number" ? Math.max(0, dispatch.recommendedMaxStarts) : null;
6147
- let maxStarts = resourceGate.slotsAvailable;
6148
- if (advised !== null) {
6149
- maxStarts = Math.min(maxStarts, advised);
6150
- }
6151
- const underutilized = dispatch?.underutilized === true;
6152
- const boardAdvancedThisTick = typeof dispatch?.boardAdvancedThisTick === "number" ? dispatch.boardAdvancedThisTick : 0;
6153
- if (underutilized && resourceGate.slotsAvailable > 0 && maxStarts === 0) {
6154
- const ready = dispatch?.actionableReady ?? dispatch?.queuedTasks ?? (boardAdvancedThisTick > 0 ? boardAdvancedThisTick : 1);
6155
- maxStarts = Math.min(resourceGate.slotsAvailable, Math.max(1, ready));
6156
- }
6157
- return {
6158
- maxStarts: Math.max(0, maxStarts),
6159
- underutilized,
6160
- advisedStarts: advised,
6161
- boardAdvancedThisTick
6162
- };
6163
- }
6164
-
6165
- // src/stale-reconcile.ts
6166
- import path33 from "node:path";
6167
- var STALE_RECONCILE_HEARTBEAT_MS = 15 * 60 * 1e3;
6168
- function staleReconcileDisabled() {
6169
- return process.env.KYNVER_NO_STALE_CLEANUP === "1";
6170
- }
6171
- function reconcileStaleWorkers() {
6172
- if (staleReconcileDisabled()) {
6173
- return { workers: [], finalizedRuns: finalizeStaleRuns() };
6174
- }
6175
- const outcomes = [];
6176
- const now = Date.now();
6177
- for (const run of listRunRecords()) {
6178
- for (const name of Object.keys(run.workers || {})) {
6179
- const workerPath = path33.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json");
6180
- const worker = readJson(workerPath, void 0);
6181
- if (!worker || worker.status !== "running") {
6182
- outcomes.push({
6183
- runId: run.id,
6184
- worker: name,
6185
- action: "skipped",
6186
- reason: worker ? `worker status is ${worker.status}` : "worker.json missing"
6187
- });
6188
- continue;
6189
- }
6190
- const status = computeWorkerStatus(worker, { base: run.base, baseCommit: run.baseCommit });
6191
- if (status.finalResult) {
6192
- if (worker.status === "running") {
6193
- const nextStatus = status.attention.state === "blocked" ? "blocked" : status.attention.state === "done" || status.status === "done" ? "done" : "exited";
6194
- worker.status = nextStatus;
6195
- worker.reconciledAt = (/* @__PURE__ */ new Date()).toISOString();
6196
- worker.reconcileReason = "synced finished worker record after terminal stdout/heartbeat";
6197
- saveWorker(run.id, worker);
6198
- outcomes.push({
6199
- runId: run.id,
6200
- worker: name,
6201
- action: "marked_exited",
6202
- reason: worker.reconcileReason
6203
- });
6204
- } else {
6205
- outcomes.push({ runId: run.id, worker: name, action: "skipped", reason: "final result present" });
6206
- }
6207
- continue;
6208
- }
6209
- if (!status.alive) {
6210
- const nextStatus = status.attention.state === "blocked" ? "blocked" : status.status === "done" ? "done" : "exited";
6211
- worker.status = nextStatus;
6212
- worker.reconciledAt = (/* @__PURE__ */ new Date()).toISOString();
6213
- worker.reconcileReason = status.attention.reason;
6214
- saveWorker(run.id, worker);
6215
- outcomes.push({
6216
- runId: run.id,
6217
- worker: name,
6218
- action: "marked_exited",
6219
- reason: status.attention.reason
6220
- });
6221
- continue;
6222
- }
6223
- if (status.attention.state === "stale" && worker.pid && isPidAlive(worker.pid)) {
6224
- const hbMs = status.lastHeartbeatAt ? Date.parse(status.lastHeartbeatAt) : NaN;
6225
- const actMs = status.lastActivityAt ? Date.parse(status.lastActivityAt) : NaN;
6226
- const hbStale = !Number.isFinite(hbMs) || now - hbMs > STALE_RECONCILE_HEARTBEAT_MS;
6227
- const actStale = Number.isFinite(actMs) && now - actMs > STALE_MS;
6228
- if (hbStale && actStale) {
6229
- killWorkerProcess(worker.pid, "SIGTERM");
6230
- worker.status = "exited";
6231
- worker.reconciledAt = (/* @__PURE__ */ new Date()).toISOString();
6232
- worker.reconcileReason = `reconciled stale worker: ${status.attention.reason}`;
6233
- saveWorker(run.id, worker);
6234
- outcomes.push({
6235
- runId: run.id,
6236
- worker: name,
6237
- action: "killed_stale",
6238
- reason: status.attention.reason
6239
- });
6240
- continue;
6241
- }
6242
- }
6243
- outcomes.push({
6244
- runId: run.id,
6245
- worker: name,
6246
- action: "skipped",
6247
- reason: status.attention.reason
6248
- });
6249
- }
7088
+ function countDispatchStarts(result) {
7089
+ if (!result || typeof result !== "object") return 0;
7090
+ const startedCount = result.startedCount;
7091
+ if (typeof startedCount === "number") return startedCount;
7092
+ const outcomes = result.outcomes;
7093
+ if (!Array.isArray(outcomes)) return 0;
7094
+ return outcomes.filter((o) => o.started).length;
7095
+ }
7096
+ function stripCliMaxStarts(args) {
7097
+ const { maxStarts: _maxStarts, ...rest } = args;
7098
+ return rest;
7099
+ }
7100
+ async function runPipelineDispatch(args, slots) {
7101
+ if (slots <= 0) {
7102
+ return { ok: true, skipped: true, reason: "no slots", maxStarts: 0, startedCount: 0 };
6250
7103
  }
6251
- return { workers: outcomes, finalizedRuns: finalizeStaleRuns() };
7104
+ const base = stripCliMaxStarts(args);
7105
+ const reviewBudget = Math.min(slots, RESERVED_REVIEW_STARTS);
7106
+ const workBudget = Math.max(0, slots - reviewBudget);
7107
+ const review = await dispatchRun({
7108
+ ...base,
7109
+ execute: true,
7110
+ pipeline: true,
7111
+ lane: "review",
7112
+ maxStarts: String(reviewBudget)
7113
+ });
7114
+ const reviewStarted = countDispatchStarts(review);
7115
+ const workSlots = workBudget + (reviewBudget - reviewStarted);
7116
+ if (workSlots <= 0) {
7117
+ return {
7118
+ ...typeof review === "object" && review !== null ? review : {},
7119
+ passes: { review },
7120
+ startedCount: reviewStarted
7121
+ };
7122
+ }
7123
+ const work = await dispatchRun({
7124
+ ...base,
7125
+ execute: true,
7126
+ pipeline: true,
7127
+ maxStarts: String(workSlots)
7128
+ });
7129
+ const workStarted = countDispatchStarts(work);
7130
+ return {
7131
+ passes: { review, work },
7132
+ startedCount: reviewStarted + workStarted,
7133
+ ok: true
7134
+ };
6252
7135
  }
6253
- function reconcileRunsCli() {
6254
- const result = reconcileStaleWorkers();
6255
- const markedExited = result.workers.filter((w) => w.action === "marked_exited").length;
6256
- const killedStale = result.workers.filter((w) => w.action === "killed_stale").length;
6257
- const skipped = result.workers.filter((w) => w.action === "skipped").length;
6258
- console.log(
6259
- JSON.stringify(
6260
- {
6261
- ok: true,
6262
- workers: { markedExited, killedStale, skipped, total: result.workers.length },
6263
- finalizedRuns: result.finalizedRuns.length,
6264
- details: { workers: result.workers, finalizedRuns: result.finalizedRuns }
6265
- },
6266
- null,
6267
- 2
6268
- )
6269
- );
7136
+
7137
+ // src/pipeline-max-starts.ts
7138
+ function operatorDispatchFromTick(operatorTick) {
7139
+ const body = operatorTick;
7140
+ const dispatch = body.response?.dispatch;
7141
+ return dispatch && typeof dispatch === "object" ? dispatch : null;
7142
+ }
7143
+ function resolvePipelineMaxStarts(resourceGate, operatorTick) {
7144
+ const dispatch = operatorDispatchFromTick(operatorTick);
7145
+ const advised = typeof dispatch?.recommendedMaxStarts === "number" ? Math.max(0, dispatch.recommendedMaxStarts) : null;
7146
+ let maxStarts = resourceGate.slotsAvailable;
7147
+ if (advised !== null) {
7148
+ maxStarts = Math.min(maxStarts, advised);
7149
+ }
7150
+ const underutilized = dispatch?.underutilized === true;
7151
+ const boardAdvancedThisTick = typeof dispatch?.boardAdvancedThisTick === "number" ? dispatch.boardAdvancedThisTick : 0;
7152
+ if (underutilized && resourceGate.slotsAvailable > 0 && maxStarts === 0) {
7153
+ const ready = dispatch?.actionableReady ?? dispatch?.queuedTasks ?? (boardAdvancedThisTick > 0 ? boardAdvancedThisTick : 1);
7154
+ maxStarts = Math.min(resourceGate.slotsAvailable, Math.max(1, ready));
7155
+ }
7156
+ return {
7157
+ maxStarts: Math.max(0, maxStarts),
7158
+ underutilized,
7159
+ advisedStarts: advised,
7160
+ boardAdvancedThisTick
7161
+ };
6270
7162
  }
6271
7163
 
6272
7164
  // src/plan-progress-daemon-sync.ts
6273
- import path34 from "node:path";
7165
+ import path40 from "node:path";
6274
7166
 
6275
7167
  // src/plan-progress-sync.ts
6276
7168
  async function syncPlanProgress(args) {
@@ -6294,7 +7186,7 @@ async function syncActiveWorkerPlanProgress(runId, args) {
6294
7186
  const outcomes = [];
6295
7187
  for (const name of Object.keys(run.workers || {})) {
6296
7188
  const worker = readJson(
6297
- path34.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
7189
+ path40.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
6298
7190
  void 0
6299
7191
  );
6300
7192
  if (!worker?.dispatched || !worker.taskId) continue;
@@ -6344,8 +7236,8 @@ async function fetchWorkspaceRuntimePreferences(agentOsId, args) {
6344
7236
 
6345
7237
  // src/installed-package-versions.ts
6346
7238
  import { readFile } from "node:fs/promises";
6347
- import { homedir as homedir6 } from "node:os";
6348
- import path35 from "node:path";
7239
+ import { homedir as homedir7 } from "node:os";
7240
+ import path41 from "node:path";
6349
7241
  var MANAGED_PACKAGES = [
6350
7242
  "@kynver-app/runtime",
6351
7243
  "@kynver-app/openclaw-agent-os",
@@ -6359,13 +7251,13 @@ function unique(values) {
6359
7251
  return [...new Set(values.filter((value) => Boolean(value)))];
6360
7252
  }
6361
7253
  function moduleRoots() {
6362
- const home = homedir6();
6363
- const openClawPrefix = trim(process.env.KYNVER_OPENCLAW_NPM_ROOT) ?? trim(process.env.OPENCLAW_NPM_ROOT) ?? path35.join(home, ".openclaw", "npm");
6364
- const npmGlobalRoot = trim(process.env.KYNVER_NPM_GLOBAL_ROOT) ?? trim(process.env.KYNVER_NPM_GLOBAL_MODULES_ROOT) ?? (trim(process.env.NPM_CONFIG_PREFIX) ? path35.join(trim(process.env.NPM_CONFIG_PREFIX), "lib", "node_modules") : path35.join(home, ".npm-global", "lib", "node_modules"));
7254
+ const home = homedir7();
7255
+ const openClawPrefix = trim(process.env.KYNVER_OPENCLAW_NPM_ROOT) ?? trim(process.env.OPENCLAW_NPM_ROOT) ?? path41.join(home, ".openclaw", "npm");
7256
+ const npmGlobalRoot = trim(process.env.KYNVER_NPM_GLOBAL_ROOT) ?? trim(process.env.KYNVER_NPM_GLOBAL_MODULES_ROOT) ?? (trim(process.env.NPM_CONFIG_PREFIX) ? path41.join(trim(process.env.NPM_CONFIG_PREFIX), "lib", "node_modules") : path41.join(home, ".npm-global", "lib", "node_modules"));
6365
7257
  return unique([
6366
- path35.join(openClawPrefix, "lib", "node_modules"),
6367
- path35.join(openClawPrefix, "node_modules"),
6368
- npmGlobalRoot.endsWith("node_modules") ? npmGlobalRoot : path35.join(npmGlobalRoot, "lib", "node_modules")
7258
+ path41.join(openClawPrefix, "lib", "node_modules"),
7259
+ path41.join(openClawPrefix, "node_modules"),
7260
+ npmGlobalRoot.endsWith("node_modules") ? npmGlobalRoot : path41.join(npmGlobalRoot, "lib", "node_modules")
6369
7261
  ]);
6370
7262
  }
6371
7263
  async function readVersion(packageJsonPath) {
@@ -6381,7 +7273,7 @@ async function collectInstalledPackageVersions(observedAt = (/* @__PURE__ */ new
6381
7273
  const out = {};
6382
7274
  for (const packageName of MANAGED_PACKAGES) {
6383
7275
  for (const root of roots) {
6384
- const packageJsonPath = path35.join(root, packageName, "package.json");
7276
+ const packageJsonPath = path41.join(root, packageName, "package.json");
6385
7277
  const version = await readVersion(packageJsonPath);
6386
7278
  if (!version) continue;
6387
7279
  out[packageName] = { version, observedAt, path: packageJsonPath };
@@ -6397,7 +7289,7 @@ async function completeFinishedWorkers(runId, args) {
6397
7289
  const outcomes = [];
6398
7290
  for (const name of Object.keys(run.workers || {})) {
6399
7291
  const worker = readJson(
6400
- path36.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
7292
+ path42.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
6401
7293
  void 0
6402
7294
  );
6403
7295
  if (!worker?.taskId || worker.localOnly) continue;
@@ -6424,18 +7316,38 @@ async function completeFinishedWorkers(runId, args) {
6424
7316
  }
6425
7317
  return outcomes;
6426
7318
  }
6427
- async function postOperatorTick(agentOsId, runId, resourceGate, args) {
7319
+ async function postOperatorTick(agentOsId, runId, resourceGate, args, harnessCleanup) {
6428
7320
  const base = resolveBaseUrl(args.baseUrl ? String(args.baseUrl) : void 0);
6429
7321
  const secret = await resolveCallbackSecretWithMint(args.secret ? String(args.secret) : void 0, agentOsId, { baseUrl: base });
6430
7322
  const url = `${base}/api/agent-os/by-id/${encodeURIComponent(agentOsId)}/operator/tick`;
6431
7323
  const packageVersions = await collectInstalledPackageVersions();
7324
+ const activeHarnessWorkers = [];
7325
+ const run = loadRun(runId);
7326
+ for (const name of Object.keys(run.workers || {})) {
7327
+ const worker = readJson(
7328
+ path42.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
7329
+ void 0
7330
+ );
7331
+ if (!worker?.taskId) continue;
7332
+ activeHarnessWorkers.push({
7333
+ runId: run.id,
7334
+ workerName: name,
7335
+ taskId: worker.taskId,
7336
+ pid: worker.pid
7337
+ });
7338
+ }
6432
7339
  const res = await postJson(url, secret, {
6433
7340
  agentOsId,
6434
7341
  runId,
6435
7342
  ingestHarness: true,
6436
7343
  harnessBoardSnapshot: buildRunBoard(runId),
6437
7344
  resourceGate,
6438
- packageVersions
7345
+ boxResourceSnapshot: buildBoxResourceSnapshotFromGate(resourceGate, { harnessRunId: runId }),
7346
+ packageVersions,
7347
+ ...harnessCleanup ? { harnessCleanup } : {},
7348
+ runnerPresence: resolveRunnerPresencePayload({ runId }),
7349
+ activeHarnessWorkers,
7350
+ ...harnessCleanup ? { harnessCleanup } : {}
6439
7351
  });
6440
7352
  return { ok: res.ok, httpStatus: res.status, response: res.response };
6441
7353
  }
@@ -6449,12 +7361,12 @@ async function runPipelineTick(args) {
6449
7361
  runId,
6450
7362
  configuredMaxWorkersOverride: workspacePrefs?.maxConcurrentWorkers
6451
7363
  });
6452
- const operatorTick = await postOperatorTick(agentOsId, runId, resourceGate, args);
7364
+ const harnessCleanup = isPipelineCleanupEnabled() ? runPipelineHarnessCleanup(runId) : void 0;
7365
+ const operatorTick = await postOperatorTick(agentOsId, runId, resourceGate, args, harnessCleanup);
6453
7366
  const completionAckSync = syncCompletionAcknowledgedFromOperatorTick(runId, operatorTick);
6454
7367
  const leaseRenewal = await renewActiveTaskLeases(runId, args);
6455
7368
  const completedWorkers = await completeFinishedWorkers(runId, args);
6456
7369
  const staleReconcile = reconcileStaleWorkers();
6457
- const harnessCleanup = isPipelineCleanupEnabled() ? runPipelineHarnessCleanup(runId) : void 0;
6458
7370
  const planProgressSync = await syncActiveWorkerPlanProgress(runId, args);
6459
7371
  const maxStartsAdvice = resolvePipelineMaxStarts(resourceGate, operatorTick);
6460
7372
  let maxStarts = maxStartsAdvice.maxStarts;
@@ -6535,7 +7447,7 @@ async function runDaemon(args) {
6535
7447
  }
6536
7448
 
6537
7449
  // src/plan-progress.ts
6538
- import path37 from "node:path";
7450
+ import path43 from "node:path";
6539
7451
 
6540
7452
  // src/bounded-build/constants.ts
6541
7453
  var DEFAULT_BUILD_MEM_BUDGET_BYTES = 1536 * 1024 * 1024;
@@ -6785,7 +7697,8 @@ async function emitPlanProgress(args) {
6785
7697
  const secret = await resolveCallbackSecretWithMint(args.secret ? String(args.secret) : void 0, agentOsId, { baseUrl: base });
6786
7698
  const url = `${base}/api/agent-os/by-id/${encodeURIComponent(agentOsId)}/plans/${encodeURIComponent(planId)}/progress-events`;
6787
7699
  const cfg = loadUserConfig();
6788
- const provider = cfg.workerProvider ? `provider:${cfg.workerProvider}` : void 0;
7700
+ const workerProvider = resolveConfiguredWorkerProvider(cfg.workerProvider, DEFAULT_WORKER_PROVIDER);
7701
+ const provider = `provider:${workerProvider}`;
6789
7702
  const explicitProposed = args.proposed === true || args.proposed === "true" ? true : args.proposed === false || args.proposed === "false" ? false : void 0;
6790
7703
  const proposed = explicitProposed ?? (status !== "done" && (roleLane === "implementer" || roleLane === "repair_implementer"));
6791
7704
  const body = {
@@ -6821,7 +7734,7 @@ async function emitPlanProgress(args) {
6821
7734
  }
6822
7735
  function verifyPlanLocal(args) {
6823
7736
  const worktree = required(args.worktree ? String(args.worktree) : void 0, "worktree");
6824
- const cwd = path37.resolve(worktree);
7737
+ const cwd = path43.resolve(worktree);
6825
7738
  const summary = runHarnessVerifyCommands(cwd);
6826
7739
  const emitJson = args.json === true || args.json === "true";
6827
7740
  const payload = { passed: summary.passed, worktree: cwd, steps: summary.steps };
@@ -6870,9 +7783,9 @@ async function verifyPlan(args) {
6870
7783
  }
6871
7784
 
6872
7785
  // src/harness-verify-cli.ts
6873
- import path38 from "node:path";
7786
+ import path44 from "node:path";
6874
7787
  function runHarnessVerifyCli(args) {
6875
- const cwd = path38.resolve(required(args.worktree ? String(args.worktree) : void 0, "worktree"));
7788
+ const cwd = path44.resolve(required(args.worktree ? String(args.worktree) : void 0, "worktree"));
6876
7789
  const emitJson = args.json === true || args.json === "true" || args.emitJson === true || args.emitJson === "true";
6877
7790
  const commands = [];
6878
7791
  const rawCmd = args.command;
@@ -6916,7 +7829,7 @@ function runHarnessVerifyCli(args) {
6916
7829
  }
6917
7830
 
6918
7831
  // src/plan-persist-cli.ts
6919
- import { readFileSync as readFileSync11 } from "node:fs";
7832
+ import { readFileSync as readFileSync12 } from "node:fs";
6920
7833
  var OPERATIONS = ["create", "add_version", "update_metadata"];
6921
7834
  var FAILURE_KINDS = [
6922
7835
  "approval_guard",
@@ -6928,7 +7841,7 @@ var FAILURE_KINDS = [
6928
7841
  function readBodyArg(args) {
6929
7842
  const bodyFile = args.bodyFile ? String(args.bodyFile) : void 0;
6930
7843
  if (bodyFile) {
6931
- return { body: readFileSync11(bodyFile, "utf8"), bodyPathHint: bodyFile };
7844
+ return { body: readFileSync12(bodyFile, "utf8"), bodyPathHint: bodyFile };
6932
7845
  }
6933
7846
  const inline = args.body ? String(args.body) : void 0;
6934
7847
  if (inline) return { body: inline };
@@ -7017,8 +7930,291 @@ function runCleanupCli(args) {
7017
7930
  }
7018
7931
  }
7019
7932
 
7933
+ // src/harness-notice/harness-notice.parse.ts
7934
+ var MAX_DIAGNOSTIC_CHARS = 2400;
7935
+ function tryParseJsonValue(text) {
7936
+ const trimmed = text.trim();
7937
+ if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) return null;
7938
+ try {
7939
+ return JSON.parse(trimmed);
7940
+ } catch {
7941
+ return null;
7942
+ }
7943
+ }
7944
+ function diagnosticJson(value, maxChars = MAX_DIAGNOSTIC_CHARS) {
7945
+ if (value === void 0 || value === null) return void 0;
7946
+ const raw = typeof value === "string" ? value : JSON.stringify(value, null, 2);
7947
+ const trimmed = raw.trim();
7948
+ if (!trimmed) return void 0;
7949
+ if (trimmed.length <= maxChars) return trimmed;
7950
+ return `${trimmed.slice(0, maxChars - 1).trimEnd()}\u2026`;
7951
+ }
7952
+ function firstJsonFromStdout(stdout) {
7953
+ const trimmed = stdout.trim();
7954
+ if (!trimmed) return null;
7955
+ const direct = tryParseJsonValue(trimmed);
7956
+ if (direct !== null) return direct;
7957
+ for (const line of trimmed.split("\n")) {
7958
+ const parsed = tryParseJsonValue(line);
7959
+ if (parsed !== null) return parsed;
7960
+ }
7961
+ return null;
7962
+ }
7963
+
7964
+ // src/harness-notice/harness-notice.auto-complete.ts
7965
+ function formatAutoCompleteOutcomeNotice(outcome) {
7966
+ const lines = [];
7967
+ lines.push(`Background auto-complete \xB7 ${outcome.runId} / ${outcome.worker}`);
7968
+ switch (outcome.outcome) {
7969
+ case "completed":
7970
+ lines.push("Outcome: harness completion posted to AgentOS successfully.");
7971
+ lines.push("AgentOS task should close or advance to review per completion routing.");
7972
+ lines.push("Next: check Command Center \u2014 no manual complete needed unless the board still shows running.");
7973
+ break;
7974
+ case "blocked":
7975
+ lines.push(
7976
+ `Outcome: worker finished but completion was blocked${outcome.httpStatus ? ` (HTTP ${outcome.httpStatus})` : ""}.`
7977
+ );
7978
+ if (outcome.reason) lines.push(`Blocker: ${outcome.reason}`);
7979
+ lines.push("Next: fix the blocker (auth, landing gate, dirty worktree) and replay completion from Command Center.");
7980
+ break;
7981
+ case "timed_out":
7982
+ lines.push(`Outcome: monitor gave up waiting \u2014 ${outcome.reason ?? "worker did not finish in time"}.`);
7983
+ lines.push("Next: inspect the worker process/logs; stop or unblock the worker, then retry auto-complete.");
7984
+ break;
7985
+ case "missing_link":
7986
+ lines.push(`Outcome: cannot complete \u2014 ${outcome.reason ?? "worker missing agentOsId/taskId"}.`);
7987
+ lines.push("Next: re-dispatch with board linkage or run `kynver worker complete` with --agent-os-id.");
7988
+ break;
7989
+ default:
7990
+ lines.push(`Outcome: ${outcome.outcome}`);
7991
+ }
7992
+ return { primary: lines.join("\n"), diagnostic: diagnosticJson(outcome) };
7993
+ }
7994
+
7995
+ // src/harness-notice/harness-notice.monitor-tick.ts
7996
+ function formatMonitorTickNotice(tick) {
7997
+ const lines = [];
7998
+ const monitorId = typeof tick.monitorId === "string" ? tick.monitorId : void 0;
7999
+ lines.push(
8000
+ monitorId ? `Harness monitor tick \xB7 ${tick.runId} (${monitorId})` : `Harness monitor tick \xB7 ${tick.runId}`
8001
+ );
8002
+ if (!tick.workers.length) {
8003
+ lines.push("No workers in scope for this poll.");
8004
+ return { primary: lines.join("\n"), diagnostic: diagnosticJson(tick) };
8005
+ }
8006
+ for (const view of tick.workers) {
8007
+ const auto = view.autoComplete.eligible ? "eligible for auto-complete" : "not auto-completing";
8008
+ const blockers = view.autoComplete.blockers.length > 0 ? ` (${view.autoComplete.blockers.slice(0, 2).join("; ")})` : "";
8009
+ lines.push(
8010
+ `\u2022 ${view.worker}: ${view.workerStatus}, ${view.health}${view.healthReason ? ` \u2014 ${view.healthReason}` : ""}; ${auto}${blockers}`
8011
+ );
8012
+ if (view.taskStatus) {
8013
+ lines.push(` Board task: ${view.taskStatus}${view.leaseOwner ? ` (lease: ${view.leaseOwner})` : ""}`);
8014
+ }
8015
+ }
8016
+ const completed = tick.autoCompleted?.filter((a) => a.outcome === "completed" && a.ok) ?? [];
8017
+ const blocked = tick.autoCompleted?.filter((a) => !a.ok && a.outcome !== "skipped") ?? [];
8018
+ if (completed.length) {
8019
+ lines.push(
8020
+ `Auto-completed: ${completed.map((c) => c.worker).join(", ")} \u2014 AgentOS completion should be posted.`
8021
+ );
8022
+ }
8023
+ if (blocked.length) {
8024
+ lines.push(
8025
+ `Auto-complete blocked: ${blocked.map((c) => `${c.worker}${c.reason ? ` (${c.reason})` : ""}`).join("; ")}`
8026
+ );
8027
+ }
8028
+ if (tick.leaseRenewal?.failed?.length) {
8029
+ lines.push(`Lease renew failed for: ${tick.leaseRenewal.failed.map((f) => f.worker).join(", ")}`);
8030
+ }
8031
+ const allDone = tick.workers.length > 0 && tick.workers.every((w) => w.autoComplete.terminalVerified) && (tick.autoCompleted?.every((a) => a.ok || a.outcome === "skipped") ?? true);
8032
+ lines.push(
8033
+ 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."
8034
+ );
8035
+ return { primary: lines.join("\n"), diagnostic: diagnosticJson(tick) };
8036
+ }
8037
+
8038
+ // src/harness-notice/harness-notice.worker-complete.ts
8039
+ function record(value) {
8040
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
8041
+ }
8042
+ function str(value) {
8043
+ return typeof value === "string" && value.trim() ? value.trim() : null;
8044
+ }
8045
+ function formatWorkerCompleteNotice(data) {
8046
+ const rec = record(data);
8047
+ if (!rec) {
8048
+ return {
8049
+ primary: "Harness worker complete finished with no parseable result.",
8050
+ diagnostic: diagnosticJson(data)
8051
+ };
8052
+ }
8053
+ const worker = str(rec.worker) ?? "worker";
8054
+ const runId = str(rec.runId);
8055
+ const skipped = str(rec.status) === "skipped";
8056
+ const httpStatus = typeof rec.httpStatus === "number" ? rec.httpStatus : null;
8057
+ const response = record(rec.response);
8058
+ const lines = [];
8059
+ lines.push(`Harness worker complete \xB7 ${worker}${runId ? ` (${runId})` : ""}`);
8060
+ if (skipped) {
8061
+ lines.push(`Outcome: skipped \u2014 ${str(rec.reason) ?? "worker not finished yet"}.`);
8062
+ lines.push("Next: wait for the worker to exit or post a finalResult, then retry complete.");
8063
+ return { primary: lines.join("\n"), diagnostic: diagnosticJson(data) };
8064
+ }
8065
+ const routeOutcome = str(response?.outcome);
8066
+ const taskRec = record(response?.task);
8067
+ const taskStatus = str(taskRec?.status);
8068
+ const prUrl = str(taskRec?.prUrl) ?? str(response?.prUrl);
8069
+ if (httpStatus && httpStatus >= 200 && httpStatus < 300) {
8070
+ lines.push("Outcome: completion callback accepted by AgentOS.");
8071
+ if (routeOutcome) lines.push(`Routing: ${routeOutcome.replace(/_/g, " ")}`);
8072
+ if (taskStatus) lines.push(`AgentOS task status: ${taskStatus}`);
8073
+ if (prUrl) lines.push(`PR: ${prUrl}`);
8074
+ lines.push("Next: check Command Center for review scheduling or blockers.");
8075
+ } else {
8076
+ lines.push(
8077
+ `Outcome: completion failed${httpStatus != null ? ` (HTTP ${httpStatus})` : ""}.`
8078
+ );
8079
+ const detail = str(response?.detail) ?? str(response?.error);
8080
+ if (detail) lines.push(`Blocker: ${detail}`);
8081
+ lines.push("Next: fix the reported blocker and replay completion from the board.");
8082
+ }
8083
+ return { primary: lines.join("\n"), diagnostic: diagnosticJson(data) };
8084
+ }
8085
+
8086
+ // src/harness-notice/harness-notice.worker-status.ts
8087
+ function str2(value) {
8088
+ return typeof value === "string" && value.trim() ? value.trim() : null;
8089
+ }
8090
+ function record2(value) {
8091
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
8092
+ }
8093
+ function prFromFinalResult(finalResult) {
8094
+ const fr = record2(finalResult);
8095
+ if (!fr) return null;
8096
+ return str2(fr.prUrl) ?? str2(fr.pr);
8097
+ }
8098
+ function formatWorkerStatusNotice(status) {
8099
+ const rec = record2(status);
8100
+ if (!rec) {
8101
+ return {
8102
+ primary: "Harness worker status unavailable.",
8103
+ diagnostic: diagnosticJson(status)
8104
+ };
8105
+ }
8106
+ const worker = str2(rec.worker) ?? str2(rec.name) ?? "worker";
8107
+ const runId = str2(rec.runId);
8108
+ const workerStatus2 = str2(rec.status) ?? "unknown";
8109
+ const alive = rec.alive === true;
8110
+ const attention = record2(rec.attention);
8111
+ const attentionState = str2(attention?.state) ?? str2(rec.attentionState);
8112
+ const attentionReason = str2(attention?.reason) ?? str2(rec.attentionReason);
8113
+ const taskId = str2(rec.taskId);
8114
+ const prUrl = str2(rec.prUrl) ?? prFromFinalResult(rec.finalResult);
8115
+ const branch = str2(rec.branch);
8116
+ const headCommit = str2(rec.headCommit);
8117
+ const lines = [];
8118
+ lines.push(`Harness worker ${worker}${runId ? ` (${runId})` : ""}`);
8119
+ lines.push(`Process: ${alive ? "running" : "stopped"} \xB7 harness status: ${workerStatus2}`);
8120
+ if (attentionState) {
8121
+ lines.push(
8122
+ attentionReason ? `Attention: ${attentionState} \u2014 ${attentionReason}` : `Attention: ${attentionState}`
8123
+ );
8124
+ }
8125
+ if (taskId) lines.push(`AgentOS task: ${taskId}`);
8126
+ if (prUrl) lines.push(`PR: ${prUrl}`);
8127
+ if (branch) lines.push(`Branch: ${branch}`);
8128
+ if (headCommit) lines.push(`Commit: ${headCommit.slice(0, 12)}`);
8129
+ if (workerStatus2 === "done" || workerStatus2 === "exited") {
8130
+ lines.push(
8131
+ prUrl ? "Outcome: worker finished \u2014 open the PR or check Command Center for review routing." : "Outcome: worker finished \u2014 check Command Center for task status and next action."
8132
+ );
8133
+ } else if (attentionState === "blocked" || attentionState === "needs_attention") {
8134
+ lines.push("Next: resolve the blocker on the board or wait for the monitor to auto-complete when terminal.");
8135
+ } else if (alive) {
8136
+ lines.push("Next: wait for completion or poll again; background monitor will auto-complete when eligible.");
8137
+ }
8138
+ return { primary: lines.join("\n"), diagnostic: diagnosticJson(status) };
8139
+ }
8140
+
8141
+ // src/harness-notice/harness-notice.tool-response.ts
8142
+ var DIVIDER = "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 Diagnostic (JSON) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500";
8143
+ function joinHarnessNotice(notice) {
8144
+ if (!notice.diagnostic?.trim()) return notice.primary;
8145
+ return `${notice.primary}
8146
+
8147
+ ${DIVIDER}
8148
+ ${notice.diagnostic}`;
8149
+ }
8150
+ function failureNotice(ctx) {
8151
+ const lines = [`Harness ${ctx.scope} ${ctx.action} failed.`];
8152
+ if (ctx.timedOut) lines.push("Reason: command timed out on the runner host.");
8153
+ else if (ctx.error) lines.push(`Reason: ${ctx.error}`);
8154
+ else if (ctx.exitCode != null) lines.push(`Exit code: ${ctx.exitCode}`);
8155
+ const errText = ctx.stderr.trim();
8156
+ if (errText) lines.push(`Stderr: ${errText.split("\n").slice(-3).join(" ")}`);
8157
+ lines.push("Next: retry on the runner host or inspect harness logs.");
8158
+ const parsed = firstJsonFromStdout(ctx.stdout);
8159
+ return {
8160
+ primary: lines.join("\n"),
8161
+ diagnostic: diagnosticJson(parsed ?? { stdout: ctx.stdout, stderr: ctx.stderr })
8162
+ };
8163
+ }
8164
+ function formatHarnessToolReadable(ctx) {
8165
+ if (!ctx.ok) return failureNotice(ctx);
8166
+ const parsed = firstJsonFromStdout(ctx.stdout);
8167
+ if (parsed === null) {
8168
+ const text = ctx.stdout.trim() || ctx.stderr.trim() || "(no output)";
8169
+ return {
8170
+ primary: `Harness ${ctx.scope} ${ctx.action} finished.
8171
+ ${text.slice(0, 800)}`,
8172
+ diagnostic: diagnosticJson({ stdout: ctx.stdout, stderr: ctx.stderr })
8173
+ };
8174
+ }
8175
+ if (ctx.scope === "worker" && ctx.action === "status") {
8176
+ return formatWorkerStatusNotice(parsed);
8177
+ }
8178
+ if (ctx.scope === "worker" && ctx.action === "complete") {
8179
+ return formatWorkerCompleteNotice(parsed);
8180
+ }
8181
+ if (ctx.scope === "monitor" && (ctx.action === "tick" || ctx.action === "run-loop")) {
8182
+ return formatMonitorTickNotice(parsed);
8183
+ }
8184
+ if (ctx.scope === "monitor" && ctx.action === "auto-complete") {
8185
+ const rec = parsed;
8186
+ if (rec.outcome && rec.worker && rec.runId) {
8187
+ return formatAutoCompleteOutcomeNotice(parsed);
8188
+ }
8189
+ if (Array.isArray(rec.blockers)) {
8190
+ return {
8191
+ primary: [
8192
+ `Monitor auto-complete blocked \xB7 ${rec.runId ?? "run"} / ${rec.worker ?? "worker"}`,
8193
+ `Blockers: ${rec.blockers.join("; ")}`,
8194
+ "Next: resolve blockers on the runner, then retry auto-complete."
8195
+ ].join("\n"),
8196
+ diagnostic: diagnosticJson(parsed)
8197
+ };
8198
+ }
8199
+ }
8200
+ if (ctx.scope === "run" && ctx.action === "status") {
8201
+ const workers = parsed.workers;
8202
+ if (Array.isArray(workers) && workers.length === 1) {
8203
+ return formatWorkerStatusNotice(workers[0]);
8204
+ }
8205
+ return {
8206
+ primary: `Harness run status \xB7 ${parsed.runId ?? "run"} (${Array.isArray(workers) ? workers.length : 0} workers).`,
8207
+ diagnostic: diagnosticJson(parsed)
8208
+ };
8209
+ }
8210
+ return {
8211
+ primary: `Harness ${ctx.scope} ${ctx.action} completed successfully on the runner.`,
8212
+ diagnostic: diagnosticJson(parsed)
8213
+ };
8214
+ }
8215
+
7020
8216
  // src/monitor/monitor.service.ts
7021
- import path40 from "node:path";
8217
+ import path46 from "node:path";
7022
8218
 
7023
8219
  // src/monitor/monitor.classify.ts
7024
8220
  function expectedLeaseOwner(runId) {
@@ -7074,11 +8270,11 @@ function classifyWorkerHealth(input) {
7074
8270
  }
7075
8271
 
7076
8272
  // src/monitor/monitor.store.ts
7077
- import { existsSync as existsSync24, mkdirSync as mkdirSync6, readdirSync as readdirSync8, unlinkSync as unlinkSync2 } from "node:fs";
7078
- import path39 from "node:path";
8273
+ import { existsSync as existsSync28, mkdirSync as mkdirSync6, readdirSync as readdirSync9, unlinkSync as unlinkSync2 } from "node:fs";
8274
+ import path45 from "node:path";
7079
8275
  function monitorsDir() {
7080
8276
  const { harnessRoot } = getHarnessPaths();
7081
- const dir = path39.join(harnessRoot, "monitors");
8277
+ const dir = path45.join(harnessRoot, "monitors");
7082
8278
  mkdirSync6(dir, { recursive: true });
7083
8279
  return dir;
7084
8280
  }
@@ -7086,7 +8282,7 @@ function monitorIdFor(runId, workerName) {
7086
8282
  return workerName ? `${safeSlug(runId)}--${safeSlug(workerName)}` : safeSlug(runId);
7087
8283
  }
7088
8284
  function monitorPath(monitorId) {
7089
- return path39.join(monitorsDir(), `${monitorId}.json`);
8285
+ return path45.join(monitorsDir(), `${monitorId}.json`);
7090
8286
  }
7091
8287
  function loadMonitorSession(monitorId) {
7092
8288
  return readJson(monitorPath(monitorId), void 0);
@@ -7096,18 +8292,18 @@ function saveMonitorSession(session) {
7096
8292
  }
7097
8293
  function deleteMonitorSession(monitorId) {
7098
8294
  const file = monitorPath(monitorId);
7099
- if (!existsSync24(file)) return false;
8295
+ if (!existsSync28(file)) return false;
7100
8296
  unlinkSync2(file);
7101
8297
  return true;
7102
8298
  }
7103
8299
  function listMonitorSessions() {
7104
8300
  const dir = monitorsDir();
7105
- if (!existsSync24(dir)) return [];
8301
+ if (!existsSync28(dir)) return [];
7106
8302
  const entries = [];
7107
- for (const name of readdirSync8(dir)) {
8303
+ for (const name of readdirSync9(dir)) {
7108
8304
  if (!name.endsWith(".json")) continue;
7109
8305
  const session = readJson(
7110
- path39.join(dir, name),
8306
+ path45.join(dir, name),
7111
8307
  void 0
7112
8308
  );
7113
8309
  if (!session?.monitorId) continue;
@@ -7198,7 +8394,7 @@ async function fetchTaskLeasesForWorkers(input) {
7198
8394
  // src/monitor/monitor.service.ts
7199
8395
  function workerRecord2(runId, name) {
7200
8396
  return readJson(
7201
- path40.join(runDirectory(runId), "workers", safeSlug(name), "worker.json"),
8397
+ path46.join(runDirectory(runId), "workers", safeSlug(name), "worker.json"),
7202
8398
  void 0
7203
8399
  );
7204
8400
  }
@@ -7383,7 +8579,11 @@ async function runMonitorLoop(args) {
7383
8579
  autoComplete: args.autoComplete ?? true,
7384
8580
  renewLeases: args.renewLeases ?? true
7385
8581
  });
7386
- console.log(JSON.stringify({ monitorId, phase: "tick", ...tick }));
8582
+ const notice = formatMonitorTickNotice({ monitorId, phase: "tick", ...tick });
8583
+ console.log(notice.primary);
8584
+ if (notice.diagnostic) {
8585
+ console.error(`[monitor diagnostic] ${notice.diagnostic}`);
8586
+ }
7387
8587
  const allTerminal = tick.workers.length > 0 && tick.workers.every(
7388
8588
  (w) => w.autoComplete.terminalVerified && (w.autoComplete.eligible || w.autoComplete.blockers.some((b) => b.includes("already acknowledged")))
7389
8589
  );
@@ -7400,18 +8600,18 @@ async function runMonitorLoop(args) {
7400
8600
 
7401
8601
  // src/monitor/monitor-spawn.ts
7402
8602
  import { spawn as spawn4 } from "node:child_process";
7403
- import { closeSync as closeSync4, existsSync as existsSync25, openSync as openSync4 } from "node:fs";
7404
- import path41 from "node:path";
8603
+ import { closeSync as closeSync4, existsSync as existsSync29, openSync as openSync4 } from "node:fs";
8604
+ import path47 from "node:path";
7405
8605
  import { fileURLToPath as fileURLToPath4 } from "node:url";
7406
8606
  function resolveDefaultCliPath2() {
7407
- return path41.join(fileURLToPath4(new URL(".", import.meta.url)), "cli.js");
8607
+ return path47.join(fileURLToPath4(new URL(".", import.meta.url)), "cli.js");
7408
8608
  }
7409
8609
  function spawnMonitorSidecar(opts) {
7410
8610
  const cliPath = opts.cliPath ?? resolveDefaultCliPath2();
7411
- if (!existsSync25(cliPath)) return void 0;
8611
+ if (!existsSync29(cliPath)) return void 0;
7412
8612
  const monitorId = monitorIdFor(opts.runId, opts.workerName);
7413
8613
  const { harnessRoot } = getHarnessPaths();
7414
- const logPath = path41.join(harnessRoot, "monitors", `${monitorId}.log`);
8614
+ const logPath = path47.join(harnessRoot, "monitors", `${monitorId}.log`);
7415
8615
  let logFd;
7416
8616
  try {
7417
8617
  logFd = openSync4(logPath, "a");
@@ -7531,12 +8731,12 @@ async function monitorTickCli(args) {
7531
8731
  }
7532
8732
 
7533
8733
  // src/doctor/runtime-takeover.ts
7534
- import path43 from "node:path";
8734
+ import path49 from "node:path";
7535
8735
 
7536
8736
  // src/doctor/runtime-takeover.probes.ts
7537
- import { accessSync, constants, existsSync as existsSync26, readFileSync as readFileSync12 } from "node:fs";
7538
- import { homedir as homedir7 } from "node:os";
7539
- import path42 from "node:path";
8737
+ import { accessSync, constants, existsSync as existsSync30, readFileSync as readFileSync13 } from "node:fs";
8738
+ import { homedir as homedir8 } from "node:os";
8739
+ import path48 from "node:path";
7540
8740
  import { spawnSync as spawnSync7 } from "node:child_process";
7541
8741
  function captureCommand(bin, args) {
7542
8742
  try {
@@ -7565,7 +8765,7 @@ function tokenPrefix(token) {
7565
8765
  return trimmed.length <= 12 ? `${trimmed}\u2026` : `${trimmed.slice(0, 12)}\u2026`;
7566
8766
  }
7567
8767
  function isWritable(target) {
7568
- if (!existsSync26(target)) return false;
8768
+ if (!existsSync30(target)) return false;
7569
8769
  try {
7570
8770
  accessSync(target, constants.W_OK);
7571
8771
  return true;
@@ -7578,15 +8778,15 @@ var defaultRuntimeTakeoverProbes = {
7578
8778
  commandOnPath: (bin) => captureCommand(process.platform === "win32" ? "where" : "which", [bin]),
7579
8779
  kynverVersion: (bin) => captureCommand(bin, ["--version"]),
7580
8780
  loadConfig: () => loadUserConfig(),
7581
- configFilePath: () => path42.join(homedir7(), ".kynver", "config.json"),
7582
- credentialsFilePath: () => path42.join(homedir7(), ".kynver", "credentials"),
8781
+ configFilePath: () => path48.join(homedir8(), ".kynver", "config.json"),
8782
+ credentialsFilePath: () => path48.join(homedir8(), ".kynver", "credentials"),
7583
8783
  readCredentials: () => {
7584
- const credPath = path42.join(homedir7(), ".kynver", "credentials");
7585
- if (!existsSync26(credPath)) {
8784
+ const credPath = path48.join(homedir8(), ".kynver", "credentials");
8785
+ if (!existsSync30(credPath)) {
7586
8786
  return { hasApiKey: false };
7587
8787
  }
7588
8788
  try {
7589
- const parsed = JSON.parse(readFileSync12(credPath, "utf8"));
8789
+ const parsed = JSON.parse(readFileSync13(credPath, "utf8"));
7590
8790
  return {
7591
8791
  hasApiKey: Boolean(parsed.apiKey?.trim()),
7592
8792
  runnerTokenPrefix: tokenPrefix(parsed.runnerToken),
@@ -7613,8 +8813,8 @@ var defaultRuntimeTakeoverProbes = {
7613
8813
  })()
7614
8814
  }),
7615
8815
  harnessRoot: () => resolveHarnessRoot(),
7616
- legacyOpenclawHarnessRoot: () => path42.join(homedir7(), ".openclaw", "harness"),
7617
- pathExists: (target) => existsSync26(target),
8816
+ legacyOpenclawHarnessRoot: () => path48.join(homedir8(), ".openclaw", "harness"),
8817
+ pathExists: (target) => existsSync30(target),
7618
8818
  pathWritable: (target) => isWritable(target),
7619
8819
  vercelVersion: () => captureCommand("vercel", ["--version"]),
7620
8820
  vercelWhoami: () => captureCommand("vercel", ["whoami"])
@@ -7662,7 +8862,7 @@ function assessRuntimeTakeoverScheduler(env, ctx) {
7662
8862
  label: "Scheduler provider (runtime daemon vs OpenClaw cron)",
7663
8863
  status: "warn",
7664
8864
  summary: `OpenClaw local cron still active (${parts.join("; ")})`,
7665
- 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.",
8865
+ 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`.",
7666
8866
  details: schedulerDetails
7667
8867
  });
7668
8868
  }
@@ -7942,8 +9142,8 @@ function assessVercelCli(probes) {
7942
9142
  }
7943
9143
  function assessHarnessDirs(probes) {
7944
9144
  const harnessRoot = probes.harnessRoot();
7945
- const runsDir = path43.join(harnessRoot, "runs");
7946
- const worktreesDir = path43.join(harnessRoot, "worktrees");
9145
+ const runsDir = path49.join(harnessRoot, "runs");
9146
+ const worktreesDir = path49.join(harnessRoot, "worktrees");
7947
9147
  const displayHarnessRoot = redactHomePath(harnessRoot);
7948
9148
  const displayRunsDir = redactHomePath(runsDir);
7949
9149
  const displayWorktreesDir = redactHomePath(worktreesDir);
@@ -8131,6 +9331,131 @@ async function runCommandCenterContractCli(args) {
8131
9331
  console.log(JSON.stringify(res.response, null, 2));
8132
9332
  }
8133
9333
 
9334
+ // src/scheduler-cutover.ts
9335
+ var DEPLOYMENT_SCHEDULER_CUTOVER_STEPS = [
9336
+ "Vercel/hosted: set KYNVER_SCHEDULER_PROVIDER=qstash",
9337
+ "Vercel/hosted: ensure QSTASH_TOKEN (and QStash signing keys) are configured",
9338
+ "Vercel/hosted: unset OPENCLAW_CRON_STORE_PATH if present"
9339
+ ];
9340
+ var RUNNER_SCHEDULER_CUTOVER_STEPS = [
9341
+ "User runner: unset KYNVER_SCHEDULER_PROVIDER (scheduling is deployment-owned)",
9342
+ "User runner: unset OPENCLAW_CRON_STORE_PATH, OPENCLAW_CRON_SECRET, OPENCLAW_CRON_FIRE_BASE_URL",
9343
+ 'User runner: after deployment cutover, run `kynver scheduler attest-cutover` (or set deploymentSchedulerProvider to "qstash" in ~/.kynver/config.json)',
9344
+ "Verify: kynver doctor runtime-takeover \u2014 hotspot_openclaw_scheduler should pass"
9345
+ ];
9346
+ function readSchedulerCutoverEnv(env = process.env) {
9347
+ return {
9348
+ kynverSchedulerProvider: env.KYNVER_SCHEDULER_PROVIDER?.trim() || null,
9349
+ openclawCronStorePath: env.OPENCLAW_CRON_STORE_PATH?.trim() || null,
9350
+ openclawCronSecret: Boolean(env.OPENCLAW_CRON_SECRET?.trim()),
9351
+ openclawCronFireBaseUrl: env.OPENCLAW_CRON_FIRE_BASE_URL?.trim() || null
9352
+ };
9353
+ }
9354
+ function assessSchedulerCutover(config, env = readSchedulerCutoverEnv()) {
9355
+ const blockers = [];
9356
+ if (env.kynverSchedulerProvider === "openclaw-cron") {
9357
+ blockers.push("Runner still has KYNVER_SCHEDULER_PROVIDER=openclaw-cron");
9358
+ }
9359
+ if (env.openclawCronStorePath) {
9360
+ blockers.push("Runner still has OPENCLAW_CRON_STORE_PATH");
9361
+ }
9362
+ if (config.deploymentSchedulerProvider === "openclaw-cron") {
9363
+ blockers.push("~/.kynver/config.json deploymentSchedulerProvider is still openclaw-cron");
9364
+ }
9365
+ return {
9366
+ ok: blockers.length === 0,
9367
+ blockers,
9368
+ runnerEnv: env,
9369
+ deploymentSchedulerProvider: config.deploymentSchedulerProvider ?? null,
9370
+ deploymentSteps: [...DEPLOYMENT_SCHEDULER_CUTOVER_STEPS],
9371
+ runnerSteps: [...RUNNER_SCHEDULER_CUTOVER_STEPS]
9372
+ };
9373
+ }
9374
+ function applySchedulerCutoverAttestation(config) {
9375
+ return {
9376
+ ...config,
9377
+ deploymentSchedulerProvider: "qstash"
9378
+ };
9379
+ }
9380
+
9381
+ // src/scheduler-cutover-cli.ts
9382
+ import path50 from "node:path";
9383
+ import { homedir as homedir9 } from "node:os";
9384
+ var CONFIG_FILE2 = path50.join(homedir9(), ".kynver", "config.json");
9385
+ function runSchedulerCutoverCheckCli(json = false) {
9386
+ const config = loadUserConfig();
9387
+ const report = assessSchedulerCutover(config);
9388
+ const payload = {
9389
+ ...report,
9390
+ configPath: displayUserPath(CONFIG_FILE2),
9391
+ configAttestationExample: { deploymentSchedulerProvider: "qstash" }
9392
+ };
9393
+ if (json) {
9394
+ console.log(JSON.stringify(payload, null, 2));
9395
+ if (!report.ok) process.exitCode = 1;
9396
+ return;
9397
+ }
9398
+ console.log("AgentOS scheduler provider cutover checklist\n");
9399
+ console.log("Deployment (Vercel):");
9400
+ for (const step of DEPLOYMENT_SCHEDULER_CUTOVER_STEPS) console.log(` - ${step}`);
9401
+ console.log("\nUser runner:");
9402
+ for (const step of RUNNER_SCHEDULER_CUTOVER_STEPS) console.log(` - ${step}`);
9403
+ console.log("\nThis host:");
9404
+ console.log(` config: ${payload.configPath}`);
9405
+ console.log(
9406
+ ` deploymentSchedulerProvider: ${report.deploymentSchedulerProvider ?? "(unset)"}`
9407
+ );
9408
+ console.log(
9409
+ ` KYNVER_SCHEDULER_PROVIDER: ${report.runnerEnv.kynverSchedulerProvider ?? "(unset)"}`
9410
+ );
9411
+ console.log(
9412
+ ` OPENCLAW_CRON_STORE_PATH: ${report.runnerEnv.openclawCronStorePath ?? "(unset)"}`
9413
+ );
9414
+ if (report.blockers.length) {
9415
+ console.log("\nBlockers:");
9416
+ for (const b of report.blockers) console.log(` ! ${b}`);
9417
+ process.exitCode = 1;
9418
+ return;
9419
+ }
9420
+ console.log("\nNo local blockers detected on this runner.");
9421
+ }
9422
+ function runSchedulerAttestCutoverCli(json = false) {
9423
+ const existing = loadUserConfig();
9424
+ const report = assessSchedulerCutover(existing);
9425
+ if (!report.ok) {
9426
+ const payload2 = {
9427
+ ok: false,
9428
+ attested: false,
9429
+ blockers: report.blockers,
9430
+ remediation: "Clear local OpenClaw scheduler blockers before attesting qstash cutover."
9431
+ };
9432
+ if (json) {
9433
+ console.log(JSON.stringify(payload2, null, 2));
9434
+ } else {
9435
+ console.error("Cannot attest scheduler cutover \u2014 local blockers remain:");
9436
+ for (const b of report.blockers) console.error(` ! ${b}`);
9437
+ }
9438
+ process.exitCode = 1;
9439
+ return;
9440
+ }
9441
+ const next = applySchedulerCutoverAttestation(existing);
9442
+ saveUserConfig(next);
9443
+ const payload = {
9444
+ ok: true,
9445
+ attested: true,
9446
+ configPath: displayUserPath(CONFIG_FILE2),
9447
+ deploymentSchedulerProvider: "qstash",
9448
+ config: presentUserConfig(next),
9449
+ note: "Recorded deploymentSchedulerProvider=qstash in ~/.kynver/config.json. Confirm Vercel has KYNVER_SCHEDULER_PROVIDER=qstash and QSTASH_TOKEN before relying on hosted schedules."
9450
+ };
9451
+ if (json) {
9452
+ console.log(JSON.stringify(payload, null, 2));
9453
+ return;
9454
+ }
9455
+ console.log(payload.note);
9456
+ console.log(` config: ${payload.configPath}`);
9457
+ }
9458
+
8134
9459
  // src/cli.ts
8135
9460
  function isHelpFlag(arg) {
8136
9461
  return arg === "help" || arg === "--help" || arg === "-h";
@@ -8178,6 +9503,8 @@ function usage(code = 0) {
8178
9503
  " kynver monitor auto-complete --run RUN_ID --name worker [--agent-os-id AOS_ID] [--base-url URL] [--secret SECRET]",
8179
9504
  " kynver monitor run-loop --run RUN_ID --monitor-id ID [--name worker] [--agent-os-id AOS_ID] [--poll-ms MS] [--auto-complete] [--renew-leases]",
8180
9505
  " kynver doctor runtime-takeover",
9506
+ " kynver scheduler cutover-check [--json]",
9507
+ " kynver scheduler attest-cutover [--json]",
8181
9508
  " kynver board contract [--agent-os-id ID] [--base-url URL] [--since ISO] [--limit N]"
8182
9509
  ].join("\n")
8183
9510
  );
@@ -8189,7 +9516,7 @@ async function main(argv = process.argv.slice(2)) {
8189
9516
  const scope = argv.shift();
8190
9517
  let action;
8191
9518
  let rest;
8192
- if (scope === "run" || scope === "worker" || scope === "plan" || scope === "runner" || scope === "harness" || scope === "monitor" || scope === "doctor" || scope === "board") {
9519
+ if (scope === "run" || scope === "worker" || scope === "plan" || scope === "runner" || scope === "harness" || scope === "monitor" || scope === "doctor" || scope === "scheduler" || scope === "board") {
8193
9520
  action = argv.shift();
8194
9521
  rest = argv;
8195
9522
  } else {
@@ -8216,6 +9543,12 @@ async function main(argv = process.argv.slice(2)) {
8216
9543
  }
8217
9544
  if (scope === "cleanup") return runCleanupCli(args);
8218
9545
  if (scope === "doctor" && action === "runtime-takeover") return runRuntimeTakeoverDoctorCli();
9546
+ if (scope === "scheduler" && action === "cutover-check") {
9547
+ return runSchedulerCutoverCheckCli(args.json === true);
9548
+ }
9549
+ if (scope === "scheduler" && action === "attest-cutover") {
9550
+ return runSchedulerAttestCutoverCli(args.json === true);
9551
+ }
8219
9552
  if (scope === "board" && action === "contract") {
8220
9553
  return void await runCommandCenterContractCli(args);
8221
9554
  }
@@ -8255,6 +9588,19 @@ if (isCliEntry) {
8255
9588
 
8256
9589
  // src/vercel/vercel-url.ts
8257
9590
  var VERCEL_HOST_RE = /(^|\.)vercel\.app$/i;
9591
+ var DPL_ID_RE = /^dpl_[a-z0-9]+$/i;
9592
+ function isInspectableVercelTarget(target) {
9593
+ const trimmed = target.trim();
9594
+ if (!trimmed) return false;
9595
+ if (/vercel\.com/i.test(trimmed)) return false;
9596
+ if (DPL_ID_RE.test(trimmed)) return true;
9597
+ try {
9598
+ const url = new URL(trimmed.startsWith("http") ? trimmed : `https://${trimmed}`);
9599
+ return VERCEL_HOST_RE.test(url.hostname);
9600
+ } catch {
9601
+ return false;
9602
+ }
9603
+ }
8258
9604
  function tryParseUrl(raw) {
8259
9605
  const trimmed = raw.trim();
8260
9606
  if (!trimmed) return null;
@@ -8308,10 +9654,11 @@ function classifyVercelUrl(raw) {
8308
9654
  }
8309
9655
  if (url.hostname === "vercel.com" || url.hostname.endsWith(".vercel.com")) {
8310
9656
  const deploymentId = parseDeploymentsSegment(url) ?? parseDashboardDeployment(url);
9657
+ const inspectTarget = deploymentId && DPL_ID_RE.test(deploymentId) ? deploymentId : null;
8311
9658
  return {
8312
9659
  kind: "dashboard",
8313
9660
  previewUrl: null,
8314
- inspectTarget: deploymentId,
9661
+ inspectTarget,
8315
9662
  deploymentId
8316
9663
  };
8317
9664
  }
@@ -8386,14 +9733,14 @@ function resolveVercelInspectTarget(rawUrl) {
8386
9733
  return { target: null, classified: null, reason: "missing target_url" };
8387
9734
  }
8388
9735
  const classified = classifyVercelUrl(trimmed);
8389
- if (classified.inspectTarget) {
9736
+ if (classified.inspectTarget && isInspectableVercelTarget(classified.inspectTarget)) {
8390
9737
  return { target: classified.inspectTarget, classified, reason: null };
8391
9738
  }
8392
9739
  if (classified.kind === "dashboard") {
8393
9740
  return {
8394
9741
  target: null,
8395
9742
  classified,
8396
- reason: "dashboard URL is not valid for vercel inspect"
9743
+ reason: classified.deploymentId ? "dashboard deployment id is not CLI-inspectable; trust GitHub Vercel status" : "dashboard URL is not valid for vercel inspect"
8397
9744
  };
8398
9745
  }
8399
9746
  return { target: null, classified, reason: "unrecognized Vercel URL" };
@@ -8434,6 +9781,15 @@ function evidenceFromGitHubVercelStatus(statuses, options = {}) {
8434
9781
  };
8435
9782
  }
8436
9783
  function defaultRunVercelInspect(target, waitSeconds) {
9784
+ if (!isInspectableVercelTarget(target)) {
9785
+ return {
9786
+ ok: false,
9787
+ exitCode: 1,
9788
+ stdout: "",
9789
+ stderr: "",
9790
+ error: "refusing vercel inspect on non-inspectable target (dashboard URLs use GitHub status)"
9791
+ };
9792
+ }
8437
9793
  const args = ["inspect", target, "--wait", String(waitSeconds)];
8438
9794
  const result = spawnSync8("vercel", args, {
8439
9795
  encoding: "utf8",
@@ -8544,10 +9900,14 @@ function collectVercelEvidence(input) {
8544
9900
  export {
8545
9901
  DEFAULT_DISPATCH_LEASE_MS,
8546
9902
  DEFAULT_HARNESS_VERIFY_COMMANDS,
9903
+ DEFAULT_NODE_MODULES_AGE_MS,
9904
+ DEFAULT_WORKER_PROVIDER,
9905
+ DEFAULT_WORKTREES_AGE_MS,
8547
9906
  DEFAULT_WSL_HOST_CRITICAL_FREE_BYTES,
8548
9907
  DEFAULT_WSL_HOST_MOUNT,
8549
9908
  DEFAULT_WSL_HOST_WARN_FREE_BYTES,
8550
9909
  FORBIDDEN_WORKER_ENV_KEYS,
9910
+ HARNESS_BUILD_CACHE_RELATIVE_PATHS,
8551
9911
  PACKAGE_VERSION,
8552
9912
  applyProductionDatabaseToProcess,
8553
9913
  assessAutoCompleteEligibility,
@@ -8560,8 +9920,10 @@ export {
8560
9920
  auditWorkerEnv,
8561
9921
  autoCompleteWorker,
8562
9922
  autoCompleteWorkerCli,
9923
+ buildBoxResourceSnapshotFromGate,
8563
9924
  buildDispatchTaskText,
8564
9925
  buildPrompt,
9926
+ buildRunListRows,
8565
9927
  buildSystemdRunArgv,
8566
9928
  classifyNpmAuditOutcome,
8567
9929
  classifyShellCommandOutcome,
@@ -8572,26 +9934,37 @@ export {
8572
9934
  computeAttention,
8573
9935
  computeWorkerStatus,
8574
9936
  createRun,
9937
+ defaultBoxId,
8575
9938
  deriveRunStatus,
8576
9939
  discoverDefaultRepo,
8577
9940
  discoverDefaultRepoCandidates,
8578
9941
  dispatchRun,
8579
9942
  drainPlanOutbox,
9943
+ enforceCursorWorkerProvider,
8580
9944
  ensurePrReadyHandoff,
8581
9945
  evidenceFromGitHubVercelStatus,
8582
9946
  extractPlanOutboxFromTask,
8583
9947
  extractPrUrlFromText,
9948
+ formatAutoCompleteOutcomeNotice,
9949
+ formatHarnessToolReadable,
9950
+ formatHeartbeatLine,
9951
+ formatMonitorTickNotice,
8584
9952
  formatPlanOutboxHandoffBlock,
8585
9953
  formatResolvedDefaultRepo,
9954
+ formatWorkerCompleteNotice,
9955
+ formatWorkerStatusNotice,
8586
9956
  getHarnessPaths,
8587
9957
  getMonitorStatus,
8588
9958
  gitRepoRoot,
8589
9959
  harnessStorageSnapshot,
8590
9960
  hashPlanBody,
9961
+ isClaudeFamilyProvider,
8591
9962
  isDashboardVercelUrl,
8592
9963
  isEngagementRequiredSkip,
8593
9964
  isFinishedWorkerStatus,
8594
9965
  isForbiddenWorkerEnvKey,
9966
+ isGeneratedHarnessPath,
9967
+ isInspectableVercelTarget,
8595
9968
  isKynverMonorepoRoot,
8596
9969
  isLandingBlockedWorkerStatus,
8597
9970
  isPipelineCleanupEnabled,
@@ -8599,11 +9972,13 @@ export {
8599
9972
  isTerminalHeartbeatPhase,
8600
9973
  isVercelStatusContext,
8601
9974
  isWslHost,
9975
+ joinHarnessNotice,
8602
9976
  landingContractAttentionReason,
8603
9977
  listForbiddenWorkerEnvKeys,
8604
9978
  listMonitors,
8605
9979
  listOutboxItems,
8606
9980
  listRuns,
9981
+ listRunsCli,
8607
9982
  loadUserConfig,
8608
9983
  main,
8609
9984
  mergeNodeOptionsForBuildCheck,
@@ -8618,6 +9993,7 @@ export {
8618
9993
  persistPlan,
8619
9994
  pickVercelStatusContext,
8620
9995
  postJson,
9996
+ preferCursorExecutor,
8621
9997
  preflightCursorModel,
8622
9998
  readMemAvailableBytes,
8623
9999
  readProductionDbKeysFromEnvFile,
@@ -8626,8 +10002,10 @@ export {
8626
10002
  redactHarness,
8627
10003
  remediateDefaultRepo,
8628
10004
  resolveBaseUrl,
10005
+ resolveBoxKindFromEnv,
8629
10006
  resolveCallbackSecret,
8630
10007
  resolveCallbackSecretWithMint,
10008
+ resolveConfiguredWorkerProvider,
8631
10009
  resolveDefaultRepo,
8632
10010
  resolveHarnessRoot,
8633
10011
  resolveProductionDatabaseUrl,
@@ -8654,6 +10032,7 @@ export {
8654
10032
  summarizeWslRecoverySteps,
8655
10033
  sweepRun,
8656
10034
  tailWorker,
10035
+ taskAllowsClaudeWorker,
8657
10036
  terminalFinalResultFromHeartbeat,
8658
10037
  usage,
8659
10038
  validateOwnedPaths,