@kynver-app/runtime 0.1.56 → 0.1.59

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -136,12 +136,14 @@ var FORBIDDEN_WORKER_ENV_KEYS = [
136
136
  "NEXTAUTH_SECRET",
137
137
  "DATABASE_URL",
138
138
  "PRODUCTION_DATABASE_URL",
139
+ "KYNVER_PRODUCTION_DATABASE_URL",
139
140
  "REDIS_URL",
140
141
  "GOOGLE_CLIENT_SECRET",
141
142
  "GITHUB_CLIENT_SECRET",
142
143
  "KYNVER_API_KEY",
143
144
  "KYNVER_SERVICE_SECRET",
144
145
  "KYNVER_RUNTIME_SECRET",
146
+ "KYNVER_CRON_SECRET",
145
147
  "OPENCLAW_CRON_SECRET",
146
148
  "QSTASH_TOKEN",
147
149
  "QSTASH_CURRENT_SIGNING_KEY",
@@ -412,7 +414,7 @@ function presentUserConfig(config) {
412
414
  }
413
415
  function inferSetupFields(existing, args) {
414
416
  const creds = loadCredentialsFile();
415
- const apiBaseUrl = (typeof args.apiBaseUrl === "string" ? args.apiBaseUrl : void 0) || existing.apiBaseUrl?.trim() || process.env.KYNVER_API_URL?.trim() || process.env.OPENCLAW_CRON_FIRE_BASE_URL?.trim();
417
+ const apiBaseUrl = (typeof args.apiBaseUrl === "string" ? args.apiBaseUrl : void 0) || existing.apiBaseUrl?.trim() || process.env.KYNVER_API_URL?.trim() || process.env.KYNVER_CRON_FIRE_BASE_URL?.trim() || process.env.OPENCLAW_CRON_FIRE_BASE_URL?.trim();
416
418
  const agentOsId = (typeof args.agentOsId === "string" ? args.agentOsId : void 0) || existing.agentOsId?.trim() || process.env.KYNVER_AGENT_OS_ID?.trim() || (creds.runnerToken?.trim().startsWith("krc1.") ? creds.runnerTokenAgentOsId?.trim() : void 0);
417
419
  const explicitRepo = typeof args.repo === "string" ? args.repo : args.discoverRepo === true || args.discoverRepo === "true" ? discoverDefaultRepo()?.repo : void 0;
418
420
  const defaultRepo = explicitRepo || existing.defaultRepo?.trim() || process.env.KYNVER_DEFAULT_REPO?.trim() || process.env.KYNVER_HARNESS_REPO?.trim() || discoverDefaultRepo()?.repo;
@@ -464,17 +466,17 @@ function saveRunnerToken(agentOsId, token) {
464
466
  }
465
467
  function resolveBaseUrl(argsBaseUrl) {
466
468
  const baseUrl = resolveConfiguredBaseUrl(argsBaseUrl);
467
- if (!baseUrl) failConfig("requires --base-url, KYNVER_API_URL, OPENCLAW_CRON_FIRE_BASE_URL, or ~/.kynver/config.json apiBaseUrl");
469
+ if (!baseUrl) failConfig("requires --base-url, KYNVER_API_URL, KYNVER_CRON_FIRE_BASE_URL, or ~/.kynver/config.json apiBaseUrl");
468
470
  return baseUrl;
469
471
  }
470
472
  function resolveConfiguredBaseUrl(argsBaseUrl) {
471
- const baseUrl = argsBaseUrl || process.env.KYNVER_API_URL || process.env.OPENCLAW_CRON_FIRE_BASE_URL || loadUserConfig().apiBaseUrl;
473
+ const baseUrl = argsBaseUrl || process.env.KYNVER_API_URL || process.env.KYNVER_CRON_FIRE_BASE_URL || process.env.OPENCLAW_CRON_FIRE_BASE_URL || loadUserConfig().apiBaseUrl;
472
474
  return baseUrl ? trimTrailingSlash(String(baseUrl)) : void 0;
473
475
  }
474
476
  function resolveConfiguredCallbackSecret(argsSecret, agentOsId) {
475
477
  const scoped = argsSecret || loadRunnerToken(agentOsId) || (agentOsId ? void 0 : loadRunnerToken(loadUserConfig().agentOsId));
476
478
  if (scoped) return String(scoped);
477
- const globalSecret = process.env.KYNVER_RUNTIME_SECRET || process.env.OPENCLAW_CRON_SECRET;
479
+ const globalSecret = process.env.KYNVER_RUNTIME_SECRET || process.env.KYNVER_CRON_SECRET || process.env.OPENCLAW_CRON_SECRET;
478
480
  if (globalSecret) {
479
481
  console.warn(
480
482
  "[kynver] using deployment-level callback secret; run `kynver runner credential --agent-os-id <id>` for a scoped token"
@@ -498,7 +500,7 @@ async function resolveCallbackSecretWithMint(argsSecret, agentOsId, opts) {
498
500
  }
499
501
  }
500
502
  failConfig(
501
- "requires --secret, KYNVER_RUNNER_TOKEN, a scoped runner token (`kynver runner credential`), ~/.kynver/credentials runnerToken, KYNVER_API_KEY with an API base URL to mint one, or (legacy) KYNVER_RUNTIME_SECRET / OPENCLAW_CRON_SECRET"
503
+ "requires --secret, KYNVER_RUNNER_TOKEN, a scoped runner token (`kynver runner credential`), ~/.kynver/credentials runnerToken, KYNVER_API_KEY with an API base URL to mint one, or (legacy) KYNVER_RUNTIME_SECRET / KYNVER_CRON_SECRET / OPENCLAW_CRON_SECRET"
502
504
  );
503
505
  }
504
506
  async function refreshRunnerToken(agentOsId, opts) {
@@ -661,6 +663,11 @@ function buildHarnessCallbackHeaders(secret) {
661
663
  }
662
664
  return {
663
665
  "Content-Type": "application/json",
666
+ // Canonical header. We keep sending the legacy `X-OpenClaw-Cron-Secret`
667
+ // (and `X-Kynver-Runtime-Secret`) so an un-upgraded Kynver server that
668
+ // only reads the old header still authenticates this runner during the
669
+ // rename compat window.
670
+ "X-Kynver-Cron-Secret": trimmed,
664
671
  "X-OpenClaw-Cron-Secret": trimmed,
665
672
  "X-Kynver-Runtime-Secret": trimmed
666
673
  };
@@ -704,18 +711,88 @@ async function getJson(url, secret) {
704
711
  }
705
712
 
706
713
  // src/disk-gate.ts
707
- import { statfsSync } from "node:fs";
714
+ import { statfsSync as statfsSync2 } from "node:fs";
715
+
716
+ // src/wsl-host.ts
717
+ import { existsSync as existsSync4, readFileSync as readFileSync4, statfsSync } from "node:fs";
718
+ var DEFAULT_WSL_HOST_WARN_FREE_BYTES = 25 * 1024 * 1024 * 1024;
719
+ var DEFAULT_WSL_HOST_CRITICAL_FREE_BYTES = 12 * 1024 * 1024 * 1024;
720
+ var DEFAULT_WSL_HOST_MOUNT = "/mnt/c";
721
+ function isWslHost() {
722
+ if (process.platform !== "linux") return false;
723
+ for (const probe of ["/proc/sys/kernel/osrelease", "/proc/version"]) {
724
+ try {
725
+ if (!existsSync4(probe)) continue;
726
+ const text = readFileSync4(probe, "utf8");
727
+ if (/microsoft|wsl/i.test(text)) return true;
728
+ } catch {
729
+ }
730
+ }
731
+ return false;
732
+ }
733
+ function observeWslHostDisk(options = {}) {
734
+ const wsl = options.forceWsl === void 0 ? isWslHost() : options.forceWsl;
735
+ if (!wsl) return null;
736
+ const path43 = options.wslHostMount?.trim() || process.env.KYNVER_WSL_HOST_MOUNT?.trim() || DEFAULT_WSL_HOST_MOUNT;
737
+ const warnBelowBytes = options.wslHostFreeWarnBytes ?? DEFAULT_WSL_HOST_WARN_FREE_BYTES;
738
+ const criticalBelowBytes = options.wslHostFreeCriticalBytes ?? DEFAULT_WSL_HOST_CRITICAL_FREE_BYTES;
739
+ const statfs = options.statfs ?? statfsSync;
740
+ let stats;
741
+ try {
742
+ stats = statfs(path43);
743
+ } catch (error) {
744
+ return {
745
+ ok: false,
746
+ path: path43,
747
+ freeBytes: 0,
748
+ totalBytes: 0,
749
+ usedPercent: 100,
750
+ warnBelowBytes,
751
+ criticalBelowBytes,
752
+ reason: `Windows host disk probe failed at ${path43}: ${error.message}`,
753
+ probeError: error.message
754
+ };
755
+ }
756
+ const freeBytes = Number(stats.bavail) * Number(stats.bsize);
757
+ const totalBytes = Number(stats.blocks) * Number(stats.bsize);
758
+ const usedPercent = totalBytes > 0 ? (totalBytes - freeBytes) / totalBytes * 100 : 100;
759
+ const lowFree = freeBytes < warnBelowBytes;
760
+ const criticalFree = freeBytes < criticalBelowBytes;
761
+ const ok = !lowFree && !criticalFree;
762
+ const freeGiB = (freeBytes / (1024 * 1024 * 1024)).toFixed(1);
763
+ let reason = null;
764
+ if (!ok) {
765
+ const tag = criticalFree ? "critical" : "warning";
766
+ reason = `Windows host disk ${path43} at ${tag}: ${freeGiB} GiB free (<${(criticalFree ? criticalBelowBytes : warnBelowBytes) / 1024 / 1024 / 1024} GiB); WSL VHDX cannot grow safely. ${summarizeWslRecoverySteps()}`;
767
+ }
768
+ return {
769
+ ok,
770
+ path: path43,
771
+ freeBytes,
772
+ totalBytes,
773
+ usedPercent,
774
+ warnBelowBytes,
775
+ criticalBelowBytes,
776
+ reason,
777
+ probeError: null
778
+ };
779
+ }
780
+ function summarizeWslRecoverySteps() {
781
+ return "Recovery: 1) free Windows C: (empty Recycle Bin / Storage Sense / clear %TEMP%); 2) shut down WSL (`wsl --shutdown`) then compact the VHDX (`Optimize-VHD` or `diskpart compact vdisk`); 3) clear local node_modules / .next / harness worktrees before restarting workers. Full runbook: docs/runbooks/wsl-disk-pressure.md.";
782
+ }
783
+
784
+ // src/disk-gate.ts
708
785
  var DEFAULT_WARN_FREE_BYTES = 30 * 1024 * 1024 * 1024;
709
786
  var DEFAULT_CRITICAL_FREE_BYTES = 15 * 1024 * 1024 * 1024;
710
787
  var DEFAULT_MAX_USED_PERCENT = 80;
711
788
  var DEFAULT_HARD_MAX_USED_PERCENT = 90;
712
789
  function observeRunnerDiskGate(input = {}) {
713
- const path40 = input.diskPath?.trim() || "/";
790
+ const path43 = input.diskPath?.trim() || "/";
714
791
  const warnBelowBytes = input.diskFreeWarnBytes ?? DEFAULT_WARN_FREE_BYTES;
715
792
  const criticalBelowBytes = input.diskFreeCriticalBytes ?? DEFAULT_CRITICAL_FREE_BYTES;
716
793
  const maxUsedPercent = input.diskMaxUsedPercent ?? DEFAULT_MAX_USED_PERCENT;
717
794
  const hardMaxUsedPercent = input.diskHardMaxUsedPercent ?? DEFAULT_HARD_MAX_USED_PERCENT;
718
- const stats = statfsSync(path40);
795
+ const stats = statfsSync2(path43);
719
796
  const freeBytes = Number(stats.bavail) * Number(stats.bsize);
720
797
  const totalBytes = Number(stats.blocks) * Number(stats.bsize);
721
798
  const usedPercent = totalBytes > 0 ? (totalBytes - freeBytes) / totalBytes * 100 : 100;
@@ -723,19 +800,22 @@ function observeRunnerDiskGate(input = {}) {
723
800
  const criticalFree = freeBytes < criticalBelowBytes;
724
801
  const highUse = usedPercent > maxUsedPercent;
725
802
  const hardHighUse = usedPercent > hardMaxUsedPercent;
726
- const ok = !lowFree && !criticalFree && !highUse && !hardHighUse;
803
+ const localOk = !lowFree && !criticalFree && !highUse && !hardHighUse;
804
+ const wslHost = input.skipWslHostCheck ? null : observeWslHostDisk(input.wslHost);
805
+ const ok = localOk && (wslHost ? wslHost.ok : true);
727
806
  let reason = null;
728
807
  if (!ok) {
729
808
  reason = [
730
809
  criticalFree ? `free space below critical ${criticalBelowBytes} bytes` : null,
731
810
  lowFree ? `free space below warning ${warnBelowBytes} bytes` : null,
732
811
  hardHighUse ? `used percent above hard cap ${hardMaxUsedPercent}%` : null,
733
- highUse ? `used percent above cap ${maxUsedPercent}%` : null
812
+ highUse ? `used percent above cap ${maxUsedPercent}%` : null,
813
+ wslHost && !wslHost.ok ? wslHost.reason : null
734
814
  ].filter(Boolean).join("; ");
735
815
  }
736
816
  return {
737
817
  ok,
738
- path: path40,
818
+ path: path43,
739
819
  freeBytes,
740
820
  totalBytes,
741
821
  usedPercent,
@@ -743,7 +823,8 @@ function observeRunnerDiskGate(input = {}) {
743
823
  criticalBelowBytes,
744
824
  maxUsedPercent,
745
825
  hardMaxUsedPercent,
746
- reason
826
+ reason,
827
+ wslHost
747
828
  };
748
829
  }
749
830
 
@@ -751,7 +832,7 @@ function observeRunnerDiskGate(input = {}) {
751
832
  import os2 from "node:os";
752
833
 
753
834
  // src/bounded-build/meminfo.ts
754
- import { readFileSync as readFileSync4 } from "node:fs";
835
+ import { readFileSync as readFileSync5 } from "node:fs";
755
836
  import os from "node:os";
756
837
  function readMemAvailableBytes(meminfoText) {
757
838
  if (meminfoText !== void 0) {
@@ -761,7 +842,7 @@ function readMemAvailableBytes(meminfoText) {
761
842
  }
762
843
  if (process.platform === "linux") {
763
844
  try {
764
- const meminfo = readFileSync4("/proc/meminfo", "utf8");
845
+ const meminfo = readFileSync5("/proc/meminfo", "utf8");
765
846
  const match = meminfo.match(/^MemAvailable:\s+(\d+)\s*kB/m);
766
847
  if (match) return Number(match[1]) * 1024;
767
848
  } catch {
@@ -774,11 +855,11 @@ function readMemAvailableBytes(meminfoText) {
774
855
  import path7 from "node:path";
775
856
 
776
857
  // src/run-store.ts
777
- import { existsSync as existsSync5, readdirSync as readdirSync2 } from "node:fs";
858
+ import { existsSync as existsSync6, readdirSync as readdirSync2 } from "node:fs";
778
859
  import path6 from "node:path";
779
860
 
780
861
  // src/paths.ts
781
- import { existsSync as existsSync4 } from "node:fs";
862
+ import { existsSync as existsSync5 } from "node:fs";
782
863
  import { homedir as homedir4 } from "node:os";
783
864
  import path5 from "node:path";
784
865
  var LEGACY_ROOT = path5.join(homedir4(), ".openclaw", "harness");
@@ -788,8 +869,8 @@ function resolveHarnessRoot() {
788
869
  const configured = loadUserConfig().harnessRoot?.trim();
789
870
  if (configured) return resolveUserPath(configured);
790
871
  const kynverRoot = path5.join(homedir4(), ".kynver", "harness");
791
- if (existsSync4(kynverRoot)) return kynverRoot;
792
- if (existsSync4(LEGACY_ROOT)) return LEGACY_ROOT;
872
+ if (existsSync5(kynverRoot)) return kynverRoot;
873
+ if (existsSync5(LEGACY_ROOT)) return LEGACY_ROOT;
793
874
  return kynverRoot;
794
875
  }
795
876
  function getHarnessPaths() {
@@ -814,7 +895,7 @@ function loadRun(id) {
814
895
  }
815
896
  function listRunRecords() {
816
897
  const { runsDir } = getPaths();
817
- if (!existsSync5(runsDir)) return [];
898
+ if (!existsSync6(runsDir)) return [];
818
899
  const runs = [];
819
900
  for (const entry of readdirSync2(runsDir, { withFileTypes: true })) {
820
901
  if (!entry.isDirectory()) continue;
@@ -846,7 +927,7 @@ function runDirectory(id) {
846
927
  }
847
928
 
848
929
  // src/heartbeat.ts
849
- import { existsSync as existsSync6, readFileSync as readFileSync5 } from "node:fs";
930
+ import { existsSync as existsSync7, readFileSync as readFileSync6 } from "node:fs";
850
931
  var HEARTBEAT_FUTURE_SKEW_MS = 6e4;
851
932
  function isTerminalHeartbeatPhase(phase) {
852
933
  return phase === "complete";
@@ -865,10 +946,10 @@ function parseHeartbeat(file) {
865
946
  heartbeatBlocker: null,
866
947
  timestampAnomalies: []
867
948
  };
868
- if (!existsSync6(file)) return result;
949
+ if (!existsSync7(file)) return result;
869
950
  const maxFutureMs = Date.now() + HEARTBEAT_FUTURE_SKEW_MS;
870
951
  const clampedTo = new Date(maxFutureMs).toISOString();
871
- const lines = readFileSync5(file, "utf8").split("\n").filter(Boolean);
952
+ const lines = readFileSync6(file, "utf8").split("\n").filter(Boolean);
872
953
  for (const line of lines) {
873
954
  const entry = safeJson(line);
874
955
  if (!entry || typeof entry !== "object" || Array.isArray(entry)) continue;
@@ -895,7 +976,7 @@ function parseHeartbeat(file) {
895
976
  }
896
977
 
897
978
  // src/stream.ts
898
- import { existsSync as existsSync7, readFileSync as readFileSync6 } from "node:fs";
979
+ import { existsSync as existsSync8, readFileSync as readFileSync7 } from "node:fs";
899
980
 
900
981
  // src/shell-command-outcome.ts
901
982
  var NPM_AUDIT_RE = /\bnpm\s+audit\b/i;
@@ -1099,8 +1180,8 @@ function parseHarnessStream(file) {
1099
1180
  error: null,
1100
1181
  lastShellOutcome: null
1101
1182
  };
1102
- if (!existsSync7(file)) return result;
1103
- const lines = readFileSync6(file, "utf8").split("\n").filter(Boolean);
1183
+ if (!existsSync8(file)) return result;
1184
+ const lines = readFileSync7(file, "utf8").split("\n").filter(Boolean);
1104
1185
  for (const line of lines) {
1105
1186
  const event = safeJson(line);
1106
1187
  if (!event) continue;
@@ -2123,7 +2204,7 @@ function hasLiveWorkerForTask(runId, taskId) {
2123
2204
  }
2124
2205
 
2125
2206
  // src/supervisor.ts
2126
- import { existsSync as existsSync11, mkdirSync as mkdirSync3 } from "node:fs";
2207
+ import { existsSync as existsSync12, mkdirSync as mkdirSync3 } from "node:fs";
2127
2208
  import path14 from "node:path";
2128
2209
 
2129
2210
  // src/prompt.ts
@@ -2144,7 +2225,7 @@ function buildPrompt(input) {
2144
2225
  input.planId ? `Active planId: ${input.planId}${input.taskId ? ` \xB7 taskId: ${input.taskId}` : ""}` : "No planId on this worker \u2014 still emit progress when you touch plan-scoped work."
2145
2226
  ];
2146
2227
  const mergeGateLines = compact ? [
2147
- "Merge-gate cost control: run `node scripts/verify-pr-local.mjs --emit-json` and `node scripts/collect-pr-vercel-evidence.mjs --pr <url> --emit-json` (trusts GitHub Vercel status for dashboard target_url; never passes dashboard URLs to `vercel inspect`) before any GitHub Actions run; request merge-gate only via POST pr-merge-gate/request-run (one Actions run per PR head unless human approves extra)."
2228
+ "Merge-gate cost control: run `node scripts/agent-os-pr-merge-gate.mjs --pr <url> --agent-os-id <id>` (or `verify-pr-local.mjs --from-pr` + `collect-pr-vercel-evidence.mjs` + POST pr-merge-gate/refresh) before any GitHub Actions run; request merge-gate only via refresh then POST pr-merge-gate/request-run (one Actions run per PR head unless human approves extra)."
2148
2229
  ] : [
2149
2230
  "GitHub Actions merge-gate cost control (Kynver/Hermes PRs):",
2150
2231
  "- Prefer local cached package verification (`node scripts/verify-pr-local.mjs --emit-json`) and Vercel preview evidence before GitHub Actions.",
@@ -2171,7 +2252,7 @@ function buildPrompt(input) {
2171
2252
  "After each major step, append one JSON line to the heartbeat file with fields: ts, phase, summary, changedFiles, blocker.",
2172
2253
  "Final response must include files changed, verification commands, and unresolved risks.",
2173
2254
  "Structured final result (recommended): record completion as JSON with summary, laneExpertise { whatChanged, why, files, prUrls, verification, risks, blockers, lessonsLearned, laneGuidance }, and targetPrReconciliation [{ prUrl, outcome: merged|skipped|blocked, mergeCommit?, reason? }] for every target PR on landing-only tasks.",
2174
- "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). Exiting with only dirty files and no PR routes to salvage review, not production review.",
2255
+ "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.",
2175
2256
  "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.",
2176
2257
  "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.",
2177
2258
  "Worker resource guard: do not run full monorepo verification (`npm run typecheck`, `npm run build`, or equivalent) from this worker lane unless an operator explicitly requests it. Use targeted checks for touched paths and rely on CI/operator lanes for heavy gates.",
@@ -2193,12 +2274,12 @@ function buildPrompt(input) {
2193
2274
  }
2194
2275
 
2195
2276
  // src/providers/cursor.ts
2196
- import { closeSync as closeSync2, existsSync as existsSync9, openSync as openSync2 } from "node:fs";
2277
+ import { closeSync as closeSync2, existsSync as existsSync10, openSync as openSync2 } from "node:fs";
2197
2278
  import { spawn as spawn2 } from "node:child_process";
2198
2279
  import path10 from "node:path";
2199
2280
 
2200
2281
  // src/providers/cursor-windows.ts
2201
- import { existsSync as existsSync8, readdirSync as readdirSync3 } from "node:fs";
2282
+ import { existsSync as existsSync9, readdirSync as readdirSync3 } from "node:fs";
2202
2283
  import path9 from "node:path";
2203
2284
  var CURSOR_VERSION_DIR = /^\d{4}\.\d{1,2}\.\d{1,2}-[a-f0-9]+$/i;
2204
2285
  function parseCursorVersionSortKey(versionName) {
@@ -2211,7 +2292,7 @@ function parseCursorVersionSortKey(versionName) {
2211
2292
  }
2212
2293
  function pickLatestCursorVersionDir(agentRoot) {
2213
2294
  const versionsRoot = path9.join(agentRoot, "versions");
2214
- if (!existsSync8(versionsRoot)) return null;
2295
+ if (!existsSync9(versionsRoot)) return null;
2215
2296
  let bestDir = null;
2216
2297
  let bestKey = -1;
2217
2298
  for (const entry of readdirSync3(versionsRoot, { withFileTypes: true })) {
@@ -2227,14 +2308,14 @@ function resolveWindowsCursorBundled(agentRoot) {
2227
2308
  const root = agentRoot?.trim() || path9.join(process.env.LOCALAPPDATA || "", "cursor-agent");
2228
2309
  const directNode = path9.join(root, "node.exe");
2229
2310
  const directIndex = path9.join(root, "index.js");
2230
- if (existsSync8(directNode) && existsSync8(directIndex)) {
2311
+ if (existsSync9(directNode) && existsSync9(directIndex)) {
2231
2312
  return { nodeExe: directNode, indexJs: directIndex, versionDir: root };
2232
2313
  }
2233
2314
  const versionDir = pickLatestCursorVersionDir(root);
2234
2315
  if (!versionDir) return null;
2235
2316
  const nodeExe = path9.join(versionDir, "node.exe");
2236
2317
  const indexJs = path9.join(versionDir, "index.js");
2237
- if (!existsSync8(nodeExe) || !existsSync8(indexJs)) return null;
2318
+ if (!existsSync9(nodeExe) || !existsSync9(indexJs)) return null;
2238
2319
  return { nodeExe, indexJs, versionDir };
2239
2320
  }
2240
2321
 
@@ -2252,7 +2333,7 @@ function bundledSpawnTarget(nodeExe, indexJs, versionDir) {
2252
2333
  function resolveCursorSpawn(agentBin) {
2253
2334
  if (process.platform === "win32") {
2254
2335
  const isCursorWrapper = /\.(cmd|bat)$/i.test(agentBin);
2255
- const isBundledNode = /node\.exe$/i.test(agentBin) && existsSync9(path10.join(path10.dirname(agentBin), "index.js"));
2336
+ const isBundledNode = /node\.exe$/i.test(agentBin) && existsSync10(path10.join(path10.dirname(agentBin), "index.js"));
2256
2337
  const isDefaultShim = agentBin === "agent";
2257
2338
  if (isCursorWrapper || isBundledNode || isDefaultShim) {
2258
2339
  const bundled = isCursorWrapper ? resolveWindowsCursorBundled(path10.dirname(agentBin)) : isBundledNode ? {
@@ -2279,7 +2360,7 @@ function resolveAgentBin() {
2279
2360
  );
2280
2361
  if (bundled) return bundled.nodeExe;
2281
2362
  const localAgent = path10.join(process.env.LOCALAPPDATA || "", "cursor-agent", "agent.cmd");
2282
- if (existsSync9(localAgent)) return localAgent;
2363
+ if (existsSync10(localAgent)) return localAgent;
2283
2364
  }
2284
2365
  return "agent";
2285
2366
  }
@@ -2362,7 +2443,7 @@ function resolveWorkerProvider(name) {
2362
2443
 
2363
2444
  // src/auto-complete.ts
2364
2445
  import { spawn as spawn3 } from "node:child_process";
2365
- import { existsSync as existsSync10, openSync as openSync3, closeSync as closeSync3 } from "node:fs";
2446
+ import { existsSync as existsSync11, openSync as openSync3, closeSync as closeSync3 } from "node:fs";
2366
2447
  import path13 from "node:path";
2367
2448
  import { fileURLToPath as fileURLToPath2 } from "node:url";
2368
2449
 
@@ -2861,6 +2942,133 @@ function ensurePrReadyHandoff(input, exec = defaultPrHandoffExec) {
2861
2942
  };
2862
2943
  }
2863
2944
 
2945
+ // src/material-worktree-changes.ts
2946
+ function materialWorktreeChanges(changedFiles) {
2947
+ return changedFiles.filter((line) => {
2948
+ const trimmed = line.trim();
2949
+ const pathPart = trimmed.startsWith("??") ? trimmed.slice(2).trim() : trimmed.length > 3 ? trimmed.slice(3).trim() : trimmed;
2950
+ return pathPart !== "node_modules" && !pathPart.startsWith("node_modules/");
2951
+ });
2952
+ }
2953
+ function pathFromGitStatusLine(line) {
2954
+ const trimmed = line.trim();
2955
+ if (trimmed.startsWith("??")) return trimmed.slice(2).trim();
2956
+ if (trimmed.length > 3 && /^[ MADRCU?!]{2} /.test(trimmed.slice(0, 3))) {
2957
+ return trimmed.slice(3).trim();
2958
+ }
2959
+ return trimmed;
2960
+ }
2961
+
2962
+ // src/disposable-artifacts.ts
2963
+ function stringList(value) {
2964
+ if (!Array.isArray(value)) return [];
2965
+ return value.map((item) => typeof item === "string" ? item.trim() : "").filter(Boolean);
2966
+ }
2967
+ function asRecord2(value) {
2968
+ return value && typeof value === "object" && !Array.isArray(value) ? value : null;
2969
+ }
2970
+ function extractDisposableArtifactsRemoved(finalResult) {
2971
+ const record = asRecord2(finalResult);
2972
+ if (!record) return [];
2973
+ const nested = asRecord2(record.worktreeHandoff);
2974
+ const fromNested = stringList(nested?.disposableArtifactsRemoved);
2975
+ if (fromNested.length) return fromNested;
2976
+ return stringList(record.disposableArtifactsRemoved);
2977
+ }
2978
+ function normalizeRelativePath(value) {
2979
+ return value.replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/+$/, "");
2980
+ }
2981
+ function dirtyPathsCoveredByDisposableRemoval(changedFiles, removed) {
2982
+ const material = materialWorktreeChanges(changedFiles);
2983
+ if (material.length === 0) return true;
2984
+ if (removed.length === 0) return false;
2985
+ const removedSet = new Set(removed.map((p) => normalizeRelativePath(p)));
2986
+ return material.every((line) => {
2987
+ const path43 = normalizeRelativePath(pathFromGitStatusLine(line));
2988
+ return removedSet.has(path43);
2989
+ });
2990
+ }
2991
+
2992
+ // src/worktree-completion-handoff.ts
2993
+ function trimOrNull5(value) {
2994
+ if (typeof value !== "string") return null;
2995
+ const t = value.trim();
2996
+ return t.length ? t : null;
2997
+ }
2998
+ function stringList2(value) {
2999
+ if (!Array.isArray(value)) return [];
3000
+ return value.map((item) => typeof item === "string" ? item.trim() : "").filter(Boolean);
3001
+ }
3002
+ function hasFinalResult4(value) {
3003
+ if (value === void 0 || value === null) return false;
3004
+ if (typeof value === "string") return value.trim().length > 0;
3005
+ if (typeof value === "boolean") return value;
3006
+ if (Array.isArray(value)) return value.length > 0;
3007
+ if (typeof value === "object") return Object.keys(value).length > 0;
3008
+ return true;
3009
+ }
3010
+ function mergedDisposableRemoved(input) {
3011
+ const fromWorker = stringList2(input.disposableArtifactsRemoved);
3012
+ const fromResult = extractDisposableArtifactsRemoved(input.finalResult);
3013
+ return [.../* @__PURE__ */ new Set([...fromWorker, ...fromResult])];
3014
+ }
3015
+ function assessWorktreeCompletionHandoff(input) {
3016
+ const rawDirty = stringList2(input.changedFiles);
3017
+ const materialDirty = materialWorktreeChanges(rawDirty);
3018
+ const removed = mergedDisposableRemoved(input);
3019
+ const effectivelyClean = materialDirty.length === 0 || dirtyPathsCoveredByDisposableRemoval(rawDirty, removed);
3020
+ if (trimOrNull5(input.prUrl)) {
3021
+ if (!effectivelyClean) {
3022
+ return {
3023
+ allowed: false,
3024
+ state: "dirty_worktree",
3025
+ materialDirtyCount: materialDirty.length,
3026
+ detail: `Worktree has ${materialDirty.length} uncommitted change(s) with a PR attached; commit or discard before completing`
3027
+ };
3028
+ }
3029
+ return { allowed: true, state: "pr_handoff", materialDirtyCount: 0 };
3030
+ }
3031
+ if (trimOrNull5(input.headCommit)) {
3032
+ if (!effectivelyClean) {
3033
+ return {
3034
+ allowed: false,
3035
+ state: "dirty_worktree",
3036
+ materialDirtyCount: materialDirty.length,
3037
+ detail: `Worktree has ${materialDirty.length} uncommitted change(s) on top of a commit; commit or discard before completing`
3038
+ };
3039
+ }
3040
+ return { allowed: true, state: "commit_handoff", materialDirtyCount: 0 };
3041
+ }
3042
+ if (trimOrNull5(input.artifactBundlePath) || trimOrNull5(input.patchPath)) {
3043
+ if (!effectivelyClean) {
3044
+ return {
3045
+ allowed: false,
3046
+ state: "dirty_worktree",
3047
+ materialDirtyCount: materialDirty.length,
3048
+ detail: `Worktree has ${materialDirty.length} uncommitted change(s) with a patch/bundle handoff; clean the tree before completing`
3049
+ };
3050
+ }
3051
+ return { allowed: true, state: "commit_handoff", materialDirtyCount: 0 };
3052
+ }
3053
+ if (effectivelyClean) {
3054
+ return { allowed: true, state: "clean", materialDirtyCount: 0 };
3055
+ }
3056
+ if (hasFinalResult4(input.finalResult)) {
3057
+ return {
3058
+ allowed: false,
3059
+ state: "dirty_worktree_no_pr",
3060
+ materialDirtyCount: materialDirty.length,
3061
+ detail: `Worktree has ${materialDirty.length} uncommitted change(s) with no commit or PR; commit, open a PR, discard, or remove one-off files via \`kynver worker discard-disposable\` before completing`
3062
+ };
3063
+ }
3064
+ return {
3065
+ allowed: false,
3066
+ state: "dirty_worktree",
3067
+ materialDirtyCount: materialDirty.length,
3068
+ detail: `Worktree has ${materialDirty.length} uncommitted change(s) with no final result`
3069
+ };
3070
+ }
3071
+
2864
3072
  // src/worker-lifecycle.ts
2865
3073
  import path11 from "node:path";
2866
3074
  var TASK_LEFT_RUNNING = /* @__PURE__ */ new Set([
@@ -2957,7 +3165,7 @@ function completionErrorText(parsed) {
2957
3165
  }
2958
3166
  return void 0;
2959
3167
  }
2960
- function asRecord2(value) {
3168
+ function asRecord3(value) {
2961
3169
  return value && typeof value === "object" && !Array.isArray(value) ? value : null;
2962
3170
  }
2963
3171
  function asString2(value) {
@@ -3034,26 +3242,48 @@ async function tryCompleteWorker(args) {
3034
3242
  if (worker.localOnly) {
3035
3243
  return { ok: true, skipped: true, reason: "local-only-worker" };
3036
3244
  }
3245
+ const headCommit = status.gitAncestry.headIsAncestorOfBase === false && status.gitAncestry.head ? status.gitAncestry.head : status.headCommit;
3246
+ const handoff = assessWorktreeCompletionHandoff({
3247
+ changedFiles: status.changedFiles,
3248
+ finalResult: status.finalResult,
3249
+ prUrl: status.prUrl,
3250
+ headCommit,
3251
+ disposableArtifactsRemoved: worker.disposableArtifactsRemoved
3252
+ });
3253
+ if (!handoff.allowed) {
3254
+ const reason2 = handoff.detail ?? `worktree completion blocked (${handoff.state})`;
3255
+ persistCompletionBlocker(worker, reason2);
3256
+ return {
3257
+ ok: false,
3258
+ reason: reason2,
3259
+ nextAction: "Clean the worktree (commit, open a PR, or `kynver worker discard-disposable --path <file>`), then rerun `kynver worker complete`.",
3260
+ completionBlocked: true
3261
+ };
3262
+ }
3037
3263
  const skipPrHandoff = args.skipPrHandoff === true || args.skipPrHandoff === "true";
3038
3264
  if (!skipPrHandoff && worker.dispatched && taskId) {
3039
- const handoff = ensurePrReadyHandoff({ worker, run, status });
3040
- if (!handoff.ok) {
3041
- persistCompletionBlocker(worker, handoff.reason);
3265
+ const handoff2 = ensurePrReadyHandoff({ worker, run, status });
3266
+ if (!handoff2.ok) {
3267
+ persistCompletionBlocker(worker, handoff2.reason);
3042
3268
  return {
3043
3269
  ok: false,
3044
- reason: handoff.reason,
3045
- nextAction: handoff.nextAction,
3270
+ reason: handoff2.reason,
3271
+ nextAction: handoff2.nextAction,
3046
3272
  completionBlocked: true
3047
3273
  };
3048
3274
  }
3049
- if (handoff.prUrl || handoff.headCommit) {
3050
- status = applyPrHandoffToStatus(status, handoff);
3275
+ if (handoff2.prUrl || handoff2.headCommit) {
3276
+ status = applyPrHandoffToStatus(status, handoff2);
3051
3277
  }
3052
3278
  }
3053
3279
  const base = resolveBaseUrl(args.baseUrl ? String(args.baseUrl) : void 0);
3054
3280
  const explicitSecret = args.secret ? String(args.secret) : void 0;
3055
3281
  let secret = await resolveCallbackSecretWithMint(explicitSecret, agentOsId, { baseUrl: base });
3056
3282
  const url = `${base}/api/agent-os/by-id/${encodeURIComponent(agentOsId)}/harness/completion`;
3283
+ const statusPayload = { ...status };
3284
+ if (worker.disposableArtifactsRemoved?.length) {
3285
+ statusPayload.disposableArtifactsRemoved = worker.disposableArtifactsRemoved;
3286
+ }
3057
3287
  const body = {
3058
3288
  source: "kynver-harness",
3059
3289
  agentOsId,
@@ -3062,10 +3292,11 @@ async function tryCompleteWorker(args) {
3062
3292
  taskId,
3063
3293
  startedAt: worker.startedAt,
3064
3294
  finishedAt: status.lastActivityAt || (/* @__PURE__ */ new Date()).toISOString(),
3065
- status,
3295
+ status: statusPayload,
3066
3296
  workerInjection: {
3067
3297
  instructionPolicyFingerprint: worker.instructionPolicyFingerprint ?? null,
3068
3298
  instructionPolicyEvidence: worker.instructionPolicyEvidence ?? null,
3299
+ memoryQualityCapture: worker.memoryQualityCapture ?? null,
3069
3300
  policyAt: worker.instructionPolicyEvidence && typeof worker.instructionPolicyEvidence === "object" && "policyAt" in worker.instructionPolicyEvidence && typeof worker.instructionPolicyEvidence.policyAt === "string" ? worker.instructionPolicyEvidence.policyAt : null,
3070
3301
  personaSlug: worker.personaSlug ?? null,
3071
3302
  personaEvidence: worker.personaEvidence ?? null
@@ -3206,8 +3437,8 @@ function buildRunBoard(runId) {
3206
3437
  const completionBlocker = typeof rawBlocker === "string" && rawBlocker ? rawBlocker : void 0;
3207
3438
  const boardStatus = completionBlocker ? "blocked" : status.status;
3208
3439
  const boardAttention = completionBlocker ? "blocked" : status.attention.state;
3209
- const completionResponse = asRecord2(worker.completionResponse);
3210
- const completionTask = asRecord2(completionResponse?.task);
3440
+ const completionResponse = asRecord3(worker.completionResponse);
3441
+ const completionTask = asRecord3(completionResponse?.task);
3211
3442
  const completionOutcome = asString2(completionResponse?.outcome);
3212
3443
  const completionRouteStatus = asString2(completionResponse?.status);
3213
3444
  const completionWarnings = Array.isArray(completionResponse?.warnings) ? completionResponse.warnings.filter((w) => typeof w === "string" && w.trim().length > 0) : [];
@@ -3488,7 +3719,7 @@ function resolveDefaultCliPath() {
3488
3719
  }
3489
3720
  function spawnCompletionSidecar(opts) {
3490
3721
  const cliPath = opts.cliPath ?? resolveDefaultCliPath();
3491
- if (!existsSync10(cliPath)) return void 0;
3722
+ if (!existsSync11(cliPath)) return void 0;
3492
3723
  const logPath = path13.join(opts.workerDir, "auto-complete.log");
3493
3724
  let logFd;
3494
3725
  try {
@@ -3575,7 +3806,7 @@ function spawnWorkerProcess(run, opts) {
3575
3806
  mkdirSync3(workerDir, { recursive: true });
3576
3807
  const worktreePath = path14.join(worktreesDir, run.id, name);
3577
3808
  const branch = opts.branch || `agent/${run.id}/${name}`;
3578
- if (existsSync11(worktreePath)) throw new Error(`worktree path already exists: ${worktreePath}`);
3809
+ if (existsSync12(worktreePath)) throw new Error(`worktree path already exists: ${worktreePath}`);
3579
3810
  git(run.repo, ["fetch", "origin", "--prune"], { allowFailure: true });
3580
3811
  git(run.repo, ["worktree", "add", "-b", branch, worktreePath, run.baseCommit], { throwError: true });
3581
3812
  const stdoutPath = path14.join(workerDir, "stdout.jsonl");
@@ -3631,6 +3862,7 @@ function spawnWorkerProcess(run, opts) {
3631
3862
  ...opts.planId ? { planId: String(opts.planId) } : {},
3632
3863
  ...opts.instructionPolicyFingerprint ? { instructionPolicyFingerprint: String(opts.instructionPolicyFingerprint) } : {},
3633
3864
  ...opts.instructionPolicyEvidence ? { instructionPolicyEvidence: opts.instructionPolicyEvidence } : {},
3865
+ ...opts.memoryQualityCapture ? { memoryQualityCapture: opts.memoryQualityCapture } : {},
3634
3866
  ...opts.personaSlug ? { personaSlug: String(opts.personaSlug) } : {},
3635
3867
  ...opts.personaEvidence ? { personaEvidence: opts.personaEvidence } : {},
3636
3868
  ...opts.leaseOwner ? { leaseOwner: String(opts.leaseOwner) } : {},
@@ -3965,8 +4197,8 @@ function isTmpOnlyPath(filePath) {
3965
4197
 
3966
4198
  // src/plan-persist/outbox-store.ts
3967
4199
  import {
3968
- existsSync as existsSync13,
3969
- readFileSync as readFileSync7,
4200
+ existsSync as existsSync14,
4201
+ readFileSync as readFileSync8,
3970
4202
  renameSync,
3971
4203
  readdirSync as readdirSync4,
3972
4204
  writeFileSync as writeFileSync3,
@@ -3992,9 +4224,9 @@ function findOutboxByIdempotencyKey(key) {
3992
4224
  return null;
3993
4225
  }
3994
4226
  function readOutboxItem(jsonPath) {
3995
- if (!existsSync13(jsonPath)) return null;
4227
+ if (!existsSync14(jsonPath)) return null;
3996
4228
  try {
3997
- return JSON.parse(readFileSync7(jsonPath, "utf8"));
4229
+ return JSON.parse(readFileSync8(jsonPath, "utf8"));
3998
4230
  } catch {
3999
4231
  return null;
4000
4232
  }
@@ -4002,7 +4234,7 @@ function readOutboxItem(jsonPath) {
4002
4234
  function readOutboxBody(item) {
4003
4235
  const { outboxDir } = ensurePlanOutboxDirs();
4004
4236
  const bodyFile = path16.join(outboxDir, item.bodyPath);
4005
- return readFileSync7(bodyFile, "utf8");
4237
+ return readFileSync8(bodyFile, "utf8");
4006
4238
  }
4007
4239
  function writeOutboxItem(input, opts) {
4008
4240
  const { outboxDir } = ensurePlanOutboxDirs();
@@ -4056,8 +4288,8 @@ function archiveOutboxItem(item) {
4056
4288
  const bodySrc = path16.join(outboxDir, item.bodyPath);
4057
4289
  const jsonDst = path16.join(archiveDir, `${item.id}.json`);
4058
4290
  const bodyDst = path16.join(archiveDir, item.bodyPath);
4059
- if (existsSync13(jsonSrc)) renameSync(jsonSrc, jsonDst);
4060
- if (existsSync13(bodySrc)) renameSync(bodySrc, bodyDst);
4291
+ if (existsSync14(jsonSrc)) renameSync(jsonSrc, jsonDst);
4292
+ if (existsSync14(bodySrc)) renameSync(bodySrc, bodyDst);
4061
4293
  }
4062
4294
  function outboxItemPaths(item) {
4063
4295
  const { outboxDir } = ensurePlanOutboxDirs();
@@ -4326,10 +4558,12 @@ function readHarnessWorkerContext(decision) {
4326
4558
  const personaSlug = typeof ctx.personaEvidence === "object" && ctx.personaEvidence && typeof ctx.personaEvidence.injectedPersonaSlug === "string" ? ctx.personaEvidence.injectedPersonaSlug : null;
4327
4559
  const personaEvidence = ctx.personaEvidence && typeof ctx.personaEvidence === "object" ? ctx.personaEvidence : null;
4328
4560
  const personaInjectionReady = ctx.personaInjectionReady === true;
4561
+ const memoryQualityCapture = ctx.memoryQualityCapture && typeof ctx.memoryQualityCapture === "object" ? ctx.memoryQualityCapture : null;
4329
4562
  return {
4330
4563
  instructionPolicyMarkdown: markdown,
4331
4564
  instructionPolicyFingerprint: fingerprint,
4332
4565
  instructionPolicyEvidence: evidence,
4566
+ memoryQualityCapture,
4333
4567
  personaMarkdown,
4334
4568
  personaSlug,
4335
4569
  personaEvidence,
@@ -4518,6 +4752,7 @@ async function dispatchRun(args) {
4518
4752
  instructionPolicyMarkdown: harnessContext?.instructionPolicyMarkdown ?? null,
4519
4753
  instructionPolicyFingerprint: harnessContext?.instructionPolicyFingerprint ?? null,
4520
4754
  instructionPolicyEvidence: harnessContext?.instructionPolicyEvidence ?? null,
4755
+ memoryQualityCapture: harnessContext?.memoryQualityCapture ?? null,
4521
4756
  personaMarkdown: harnessContext?.personaMarkdown ?? null,
4522
4757
  personaSlug: harnessContext?.personaSlug ?? expectedPersona,
4523
4758
  personaEvidence: harnessContext?.personaEvidence ?? null,
@@ -4648,7 +4883,7 @@ async function sweepRun(args) {
4648
4883
  }
4649
4884
 
4650
4885
  // src/worktree.ts
4651
- import { existsSync as existsSync14, mkdirSync as mkdirSync5 } from "node:fs";
4886
+ import { existsSync as existsSync15, mkdirSync as mkdirSync5 } from "node:fs";
4652
4887
  import path22 from "node:path";
4653
4888
 
4654
4889
  // src/default-repo.ts
@@ -4721,7 +4956,7 @@ function createRun(args) {
4721
4956
  ensureGitRepo(repo);
4722
4957
  const id = args.id ? validateRunId(String(args.id)) : timestampSlug(String(args.name || "run"));
4723
4958
  const dir = runDirectory(id);
4724
- if (existsSync14(dir)) failExists(`run already exists: ${id}`);
4959
+ if (existsSync15(dir)) failExists(`run already exists: ${id}`);
4725
4960
  mkdirSync5(dir, { recursive: true });
4726
4961
  const base = String(args.base || "origin/main");
4727
4962
  const baseCommit = git(repo, ["rev-parse", base]).trim();
@@ -4754,8 +4989,61 @@ function failExists(message) {
4754
4989
  process.exit(1);
4755
4990
  }
4756
4991
 
4992
+ // src/discard-disposable.ts
4993
+ import { existsSync as existsSync16, rmSync } from "node:fs";
4994
+ import path23 from "node:path";
4995
+ function normalizeRelativePath2(value) {
4996
+ const normalized = value.replace(/\\/g, "/").replace(/^\.\//, "").trim();
4997
+ if (!normalized || normalized.startsWith("/") || normalized.includes("..")) {
4998
+ throw new Error(`unsafe path: ${value}`);
4999
+ }
5000
+ return normalized;
5001
+ }
5002
+ function parsePathsArg(raw) {
5003
+ if (typeof raw !== "string" || !raw.trim()) return [];
5004
+ return raw.split(",").map((p) => p.trim()).filter(Boolean);
5005
+ }
5006
+ function discardDisposableArtifacts(args) {
5007
+ const worker = loadWorker(String(args.run), String(args.name));
5008
+ const paths = [
5009
+ ...parsePathsArg(args.path),
5010
+ ...Array.isArray(args.paths) ? args.paths : []
5011
+ ];
5012
+ if (paths.length === 0) {
5013
+ return { ok: false, removed: [], reason: "requires at least one --path" };
5014
+ }
5015
+ const worktreeRoot = path23.resolve(worker.worktreePath);
5016
+ const removed = [];
5017
+ for (const raw of paths) {
5018
+ const rel = normalizeRelativePath2(raw);
5019
+ const abs = path23.resolve(worktreeRoot, rel);
5020
+ if (!abs.startsWith(worktreeRoot + path23.sep) && abs !== worktreeRoot) {
5021
+ return { ok: false, removed, reason: `path escapes worktree: ${raw}` };
5022
+ }
5023
+ if (!existsSync16(abs)) {
5024
+ return { ok: false, removed, reason: `path not found: ${raw}` };
5025
+ }
5026
+ rmSync(abs, { recursive: true, force: true });
5027
+ removed.push(rel);
5028
+ }
5029
+ const prior = Array.isArray(worker.disposableArtifactsRemoved) ? worker.disposableArtifactsRemoved.filter((p) => typeof p === "string") : [];
5030
+ worker.disposableArtifactsRemoved = [.../* @__PURE__ */ new Set([...prior, ...removed])];
5031
+ saveWorker(worker.runId, worker);
5032
+ const status = computeWorkerStatus(worker);
5033
+ return {
5034
+ ok: true,
5035
+ removed,
5036
+ ...status.changedFiles.length ? { reason: "worktree still has other changes" } : {}
5037
+ };
5038
+ }
5039
+ function discardDisposableCli(args) {
5040
+ const result = discardDisposableArtifacts(args);
5041
+ console.log(JSON.stringify(result, null, 2));
5042
+ if (!result.ok) process.exit(1);
5043
+ }
5044
+
4757
5045
  // src/pipeline-tick.ts
4758
- import path32 from "node:path";
5046
+ import path35 from "node:path";
4759
5047
 
4760
5048
  // src/pipeline-dispatch.ts
4761
5049
  var RESERVED_REVIEW_STARTS = 1;
@@ -4809,10 +5097,10 @@ async function runPipelineDispatch(args, slots) {
4809
5097
  }
4810
5098
 
4811
5099
  // src/stale-reconcile.ts
4812
- import path24 from "node:path";
5100
+ import path25 from "node:path";
4813
5101
 
4814
5102
  // src/finalize.ts
4815
- import path23 from "node:path";
5103
+ import path24 from "node:path";
4816
5104
  var ACTIVE_RUN_STATUSES = /* @__PURE__ */ new Set([
4817
5105
  "running",
4818
5106
  "dispatching",
@@ -4830,7 +5118,7 @@ function deriveTerminalRunStatus(run) {
4830
5118
  let anyLandingBlocked = false;
4831
5119
  for (const name of names) {
4832
5120
  const worker = readJson(
4833
- path23.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
5121
+ path24.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
4834
5122
  void 0
4835
5123
  );
4836
5124
  if (!worker) continue;
@@ -4882,7 +5170,7 @@ function reconcileStaleWorkers() {
4882
5170
  const now = Date.now();
4883
5171
  for (const run of listRunRecords()) {
4884
5172
  for (const name of Object.keys(run.workers || {})) {
4885
- const workerPath = path24.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json");
5173
+ const workerPath = path25.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json");
4886
5174
  const worker = readJson(workerPath, void 0);
4887
5175
  if (!worker || worker.status !== "running") {
4888
5176
  outcomes.push({
@@ -4976,7 +5264,7 @@ function reconcileRunsCli() {
4976
5264
  }
4977
5265
 
4978
5266
  // src/plan-progress-daemon-sync.ts
4979
- import path25 from "node:path";
5267
+ import path26 from "node:path";
4980
5268
 
4981
5269
  // src/plan-progress-sync.ts
4982
5270
  async function syncPlanProgress(args) {
@@ -5000,7 +5288,7 @@ async function syncActiveWorkerPlanProgress(runId, args) {
5000
5288
  const outcomes = [];
5001
5289
  for (const name of Object.keys(run.workers || {})) {
5002
5290
  const worker = readJson(
5003
- path25.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
5291
+ path26.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
5004
5292
  void 0
5005
5293
  );
5006
5294
  if (!worker?.dispatched || !worker.taskId) continue;
@@ -5049,7 +5337,7 @@ async function fetchWorkspaceRuntimePreferences(agentOsId, args) {
5049
5337
  }
5050
5338
 
5051
5339
  // src/cleanup.ts
5052
- import path30 from "node:path";
5340
+ import path33 from "node:path";
5053
5341
 
5054
5342
  // src/cleanup-run-liveness.ts
5055
5343
  function isWorkerProcessLive(indexed) {
@@ -5070,6 +5358,15 @@ function runBlocksWorktreeRemoval(indexed) {
5070
5358
  return deriveTerminalRunStatus(indexed.run) === null;
5071
5359
  }
5072
5360
 
5361
+ // src/cleanup-guards-helpers.ts
5362
+ function materialWorktreeChanges2(changedFiles) {
5363
+ return changedFiles.filter((line) => {
5364
+ const trimmed = line.trim();
5365
+ const pathPart = trimmed.startsWith("??") ? trimmed.slice(2).trim() : trimmed.length > 3 ? trimmed.slice(3).trim() : trimmed;
5366
+ return pathPart !== "node_modules" && !pathPart.startsWith("node_modules/");
5367
+ });
5368
+ }
5369
+
5073
5370
  // src/cleanup-guards.ts
5074
5371
  function prUrlFromFinalResult(finalResult) {
5075
5372
  if (typeof finalResult === "string") {
@@ -5092,29 +5389,25 @@ function isPrOrUnmergedWork(status) {
5092
5389
  if (status.changedFiles.length > 0 && status.finalResult) return true;
5093
5390
  return false;
5094
5391
  }
5095
- function materialWorktreeChanges(changedFiles) {
5096
- return changedFiles.filter((line) => {
5097
- const trimmed = line.trim();
5098
- const pathPart = trimmed.startsWith("??") ? trimmed.slice(2).trim() : trimmed.length > 3 ? trimmed.slice(3).trim() : trimmed;
5099
- return pathPart !== "node_modules" && !pathPart.startsWith("node_modules/");
5100
- });
5101
- }
5102
5392
  function hasUnrestorableWorktreeChanges(status) {
5103
- if (materialWorktreeChanges(status.changedFiles).length > 0) return true;
5393
+ if (materialWorktreeChanges2(status.changedFiles).length > 0) return true;
5104
5394
  if (status.gitAncestry?.relation === "diverged") return true;
5105
5395
  return false;
5106
5396
  }
5107
5397
  function skipWorktreeRemoval(input) {
5108
- const { indexed, includeOrphans, worktreesAgeMs, ageMs } = input;
5109
- if (worktreesAgeMs <= 0) return "worktrees_disabled";
5110
- if (ageMs < worktreesAgeMs) return "below_age_threshold";
5111
- if (!indexed) return includeOrphans ? null : "orphan_without_flag";
5398
+ const { indexed, includeOrphans, worktreesAgeMs, ageMs, orphanSafety, worktreeRemovalGuard } = input;
5399
+ if (!indexed) {
5400
+ if (!includeOrphans) return "orphan_without_flag";
5401
+ return orphanSafety ?? null;
5402
+ }
5403
+ if (worktreesAgeMs <= 0 && !includeOrphans) return "worktrees_disabled";
5404
+ if (worktreesAgeMs > 0 && ageMs < worktreesAgeMs) return "below_age_threshold";
5112
5405
  if (isWorkerProcessLive(indexed)) return "active_worker";
5113
5406
  if (indexed.worker.completionBlocker) return "completion_blocked";
5114
5407
  if (runBlocksWorktreeRemoval(indexed)) return "run_still_active";
5115
5408
  if (!isFinishedWorkerStatus(indexed.status)) return "run_still_active";
5116
5409
  if (isPrOrUnmergedWork(indexed.status)) return "pr_or_unmerged_commits";
5117
- if (materialWorktreeChanges(indexed.status.changedFiles).length > 0) return "dirty_worktree";
5410
+ if (materialWorktreeChanges2(indexed.status.changedFiles).length > 0) return "dirty_worktree";
5118
5411
  const landing = assessWorkerLanding({
5119
5412
  finalResult: indexed.status.finalResult,
5120
5413
  changedFiles: indexed.status.changedFiles,
@@ -5122,6 +5415,17 @@ function skipWorktreeRemoval(input) {
5122
5415
  prUrl: prUrlFromFinalResult(indexed.status.finalResult)
5123
5416
  });
5124
5417
  if (landing.blocked) return "landing_blocked";
5418
+ if (worktreeRemovalGuard && input.worktreePath) {
5419
+ const overlay = worktreeRemovalGuard({
5420
+ worktreePath: input.worktreePath,
5421
+ indexed: Boolean(indexed),
5422
+ runId: indexed?.runId,
5423
+ worker: indexed?.workerName
5424
+ });
5425
+ if (overlay) {
5426
+ return overlay.detail ? { reason: overlay.reason, detail: overlay.detail } : overlay.reason;
5427
+ }
5428
+ }
5125
5429
  return null;
5126
5430
  }
5127
5431
  function skipNodeModulesRemoval(input) {
@@ -5138,21 +5442,21 @@ function skipNodeModulesRemoval(input) {
5138
5442
  gitAncestry: indexed.status.gitAncestry,
5139
5443
  prUrl: prUrlFromFinalResult(indexed.status.finalResult)
5140
5444
  });
5141
- if (landing.blocked && materialWorktreeChanges(indexed.status.changedFiles).length > 0) {
5445
+ if (landing.blocked && materialWorktreeChanges2(indexed.status.changedFiles).length > 0) {
5142
5446
  return "landing_blocked";
5143
5447
  }
5144
5448
  return null;
5145
5449
  }
5146
5450
 
5147
5451
  // src/cleanup-execute.ts
5148
- import { existsSync as existsSync16, rmSync } from "node:fs";
5149
- import path27 from "node:path";
5452
+ import { existsSync as existsSync18, rmSync as rmSync2 } from "node:fs";
5453
+ import path28 from "node:path";
5150
5454
 
5151
5455
  // src/cleanup-dir-size.ts
5152
- import { existsSync as existsSync15, readdirSync as readdirSync5, statSync as statSync2 } from "node:fs";
5153
- import path26 from "node:path";
5456
+ import { existsSync as existsSync17, readdirSync as readdirSync5, statSync as statSync2 } from "node:fs";
5457
+ import path27 from "node:path";
5154
5458
  function directorySizeBytes(root, maxEntries = 5e4) {
5155
- if (!existsSync15(root)) return 0;
5459
+ if (!existsSync17(root)) return 0;
5156
5460
  let total = 0;
5157
5461
  let seen = 0;
5158
5462
  const stack = [root];
@@ -5166,7 +5470,7 @@ function directorySizeBytes(root, maxEntries = 5e4) {
5166
5470
  }
5167
5471
  for (const name of entries) {
5168
5472
  if (seen++ > maxEntries) return null;
5169
- const full = path26.join(current, name);
5473
+ const full = path27.join(current, name);
5170
5474
  let st;
5171
5475
  try {
5172
5476
  st = statSync2(full);
@@ -5182,7 +5486,7 @@ function directorySizeBytes(root, maxEntries = 5e4) {
5182
5486
 
5183
5487
  // src/cleanup-execute.ts
5184
5488
  function removeNodeModules(candidate, execute) {
5185
- if (!existsSync16(candidate.path)) {
5489
+ if (!existsSync18(candidate.path)) {
5186
5490
  return {
5187
5491
  ...candidate,
5188
5492
  executed: false,
@@ -5195,7 +5499,7 @@ function removeNodeModules(candidate, execute) {
5195
5499
  }
5196
5500
  try {
5197
5501
  const bytesBefore = candidate.bytes ?? directorySizeBytes(candidate.path);
5198
- rmSync(candidate.path, { recursive: true, force: true });
5502
+ rmSync2(candidate.path, { recursive: true, force: true });
5199
5503
  return {
5200
5504
  ...candidate,
5201
5505
  bytes: bytesBefore,
@@ -5213,7 +5517,7 @@ function removeNodeModules(candidate, execute) {
5213
5517
  }
5214
5518
  }
5215
5519
  function removeWorktree(candidate, execute) {
5216
- if (!existsSync16(candidate.path)) {
5520
+ if (!existsSync18(candidate.path)) {
5217
5521
  return {
5218
5522
  ...candidate,
5219
5523
  executed: false,
@@ -5230,8 +5534,8 @@ function removeWorktree(candidate, execute) {
5230
5534
  if (repo) {
5231
5535
  git(repo, ["worktree", "remove", "--force", candidate.path], { allowFailure: true });
5232
5536
  }
5233
- if (existsSync16(candidate.path)) {
5234
- rmSync(candidate.path, { recursive: true, force: true });
5537
+ if (existsSync18(candidate.path)) {
5538
+ rmSync2(candidate.path, { recursive: true, force: true });
5235
5539
  }
5236
5540
  return {
5237
5541
  ...candidate,
@@ -5250,20 +5554,20 @@ function removeWorktree(candidate, execute) {
5250
5554
  }
5251
5555
  }
5252
5556
  function isHarnessNodeModulesPath(targetPath, harnessRoot, worktreesDir) {
5253
- const resolved = path27.resolve(targetPath);
5254
- const nm = resolved.endsWith(`${path27.sep}node_modules`) ? resolved : null;
5557
+ const resolved = path28.resolve(targetPath);
5558
+ const nm = resolved.endsWith(`${path28.sep}node_modules`) ? resolved : null;
5255
5559
  if (!nm) return "path_outside_harness";
5256
- const rel = path27.relative(worktreesDir, nm);
5257
- if (rel.startsWith("..") || path27.isAbsolute(rel)) return "path_outside_harness";
5258
- const parts = rel.split(path27.sep);
5560
+ const rel = path28.relative(worktreesDir, nm);
5561
+ if (rel.startsWith("..") || path28.isAbsolute(rel)) return "path_outside_harness";
5562
+ const parts = rel.split(path28.sep);
5259
5563
  if (parts.length < 3 || parts[parts.length - 1] !== "node_modules") return "path_outside_harness";
5260
- if (!resolved.startsWith(path27.resolve(harnessRoot))) return "path_outside_harness";
5564
+ if (!resolved.startsWith(path28.resolve(harnessRoot))) return "path_outside_harness";
5261
5565
  return null;
5262
5566
  }
5263
5567
 
5264
5568
  // src/cleanup-scan.ts
5265
- import { existsSync as existsSync17, readdirSync as readdirSync6, statSync as statSync3 } from "node:fs";
5266
- import path28 from "node:path";
5569
+ import { existsSync as existsSync19, readdirSync as readdirSync6, statSync as statSync3 } from "node:fs";
5570
+ import path29 from "node:path";
5267
5571
  function pathAgeMs(target, now) {
5268
5572
  try {
5269
5573
  const mtime = statSync3(target).mtimeMs;
@@ -5273,17 +5577,17 @@ function pathAgeMs(target, now) {
5273
5577
  }
5274
5578
  }
5275
5579
  function isPathInside(child, parent) {
5276
- const rel = path28.relative(parent, child);
5277
- return rel === "" || !rel.startsWith("..") && !path28.isAbsolute(rel);
5580
+ const rel = path29.relative(parent, child);
5581
+ return rel === "" || !rel.startsWith("..") && !path29.isAbsolute(rel);
5278
5582
  }
5279
5583
  function scanNodeModulesCandidates(opts) {
5280
5584
  const candidates = [];
5281
5585
  const seen = /* @__PURE__ */ new Set();
5282
5586
  for (const entry of opts.index.values()) {
5283
5587
  if (opts.runIdFilter && entry.runId !== opts.runIdFilter) continue;
5284
- const nm = path28.join(entry.worktreePath, "node_modules");
5285
- if (!existsSync17(nm)) continue;
5286
- const resolved = path28.resolve(nm);
5588
+ const nm = path29.join(entry.worktreePath, "node_modules");
5589
+ if (!existsSync19(nm)) continue;
5590
+ const resolved = path29.resolve(nm);
5287
5591
  if (seen.has(resolved)) continue;
5288
5592
  seen.add(resolved);
5289
5593
  candidates.push({
@@ -5296,16 +5600,16 @@ function scanNodeModulesCandidates(opts) {
5296
5600
  ageMs: pathAgeMs(resolved, opts.now)
5297
5601
  });
5298
5602
  }
5299
- if (!opts.includeOrphans || !existsSync17(opts.worktreesDir)) return candidates;
5603
+ if (!opts.includeOrphans || !existsSync19(opts.worktreesDir)) return candidates;
5300
5604
  for (const runEntry of readdirSync6(opts.worktreesDir, { withFileTypes: true })) {
5301
5605
  if (!runEntry.isDirectory()) continue;
5302
- const runPath = path28.join(opts.worktreesDir, runEntry.name);
5606
+ const runPath = path29.join(opts.worktreesDir, runEntry.name);
5303
5607
  for (const workerEntry of readdirSync6(runPath, { withFileTypes: true })) {
5304
5608
  if (!workerEntry.isDirectory()) continue;
5305
- const worktreePath = path28.join(runPath, workerEntry.name);
5306
- const nm = path28.join(worktreePath, "node_modules");
5307
- if (!existsSync17(nm)) continue;
5308
- const resolved = path28.resolve(nm);
5609
+ const worktreePath = path29.join(runPath, workerEntry.name);
5610
+ const nm = path29.join(worktreePath, "node_modules");
5611
+ if (!existsSync19(nm)) continue;
5612
+ const resolved = path29.resolve(nm);
5309
5613
  if (seen.has(resolved)) continue;
5310
5614
  if (!isPathInside(resolved, opts.harnessRoot)) continue;
5311
5615
  seen.add(resolved);
@@ -5322,40 +5626,76 @@ function scanNodeModulesCandidates(opts) {
5322
5626
  return candidates;
5323
5627
  }
5324
5628
  function scanWorktreeCandidates(opts) {
5325
- if (opts.worktreesAgeMs <= 0) return [];
5629
+ const indexedEnabled = opts.worktreesAgeMs > 0 || opts.includeOrphans;
5630
+ const orphanEnabled = opts.includeOrphans;
5631
+ if (!indexedEnabled && !orphanEnabled) return [];
5326
5632
  const candidates = [];
5327
5633
  const seen = /* @__PURE__ */ new Set();
5634
+ if (indexedEnabled) {
5635
+ for (const entry of opts.index.values()) {
5636
+ if (opts.runIdFilter && entry.runId !== opts.runIdFilter) continue;
5637
+ const resolved = entry.worktreePath;
5638
+ if (!existsSync19(resolved)) continue;
5639
+ if (seen.has(resolved)) continue;
5640
+ seen.add(resolved);
5641
+ candidates.push({
5642
+ kind: "remove_worktree",
5643
+ path: resolved,
5644
+ bytes: null,
5645
+ runId: entry.runId,
5646
+ worker: entry.workerName,
5647
+ repo: entry.run.repo,
5648
+ ageMs: pathAgeMs(resolved, opts.now)
5649
+ });
5650
+ }
5651
+ }
5652
+ if (!orphanEnabled || !existsSync19(opts.worktreesDir)) return candidates;
5653
+ const indexedPaths = /* @__PURE__ */ new Set();
5328
5654
  for (const entry of opts.index.values()) {
5329
- if (opts.runIdFilter && entry.runId !== opts.runIdFilter) continue;
5330
- const resolved = entry.worktreePath;
5331
- if (!existsSync17(resolved)) continue;
5332
- if (seen.has(resolved)) continue;
5333
- seen.add(resolved);
5334
- candidates.push({
5335
- kind: "remove_worktree",
5336
- path: resolved,
5337
- bytes: null,
5338
- runId: entry.runId,
5339
- worker: entry.workerName,
5340
- repo: entry.run.repo,
5341
- ageMs: pathAgeMs(resolved, opts.now)
5342
- });
5655
+ indexedPaths.add(path29.resolve(entry.worktreePath));
5656
+ }
5657
+ for (const runEntry of readdirSync6(opts.worktreesDir, { withFileTypes: true })) {
5658
+ if (!runEntry.isDirectory()) continue;
5659
+ if (opts.runIdFilter && runEntry.name !== opts.runIdFilter) continue;
5660
+ const runPath = path29.join(opts.worktreesDir, runEntry.name);
5661
+ let workerEntries;
5662
+ try {
5663
+ workerEntries = readdirSync6(runPath, { withFileTypes: true });
5664
+ } catch {
5665
+ continue;
5666
+ }
5667
+ for (const workerEntry of workerEntries) {
5668
+ if (!workerEntry.isDirectory()) continue;
5669
+ const worktreePath = path29.resolve(path29.join(runPath, workerEntry.name));
5670
+ if (seen.has(worktreePath)) continue;
5671
+ if (indexedPaths.has(worktreePath)) continue;
5672
+ if (!isPathInside(worktreePath, opts.harnessRoot)) continue;
5673
+ seen.add(worktreePath);
5674
+ candidates.push({
5675
+ kind: "remove_worktree",
5676
+ path: worktreePath,
5677
+ bytes: null,
5678
+ runId: runEntry.name,
5679
+ worker: workerEntry.name,
5680
+ ageMs: pathAgeMs(worktreePath, opts.now)
5681
+ });
5682
+ }
5343
5683
  }
5344
5684
  return candidates;
5345
5685
  }
5346
5686
 
5347
5687
  // src/cleanup-worktree-index.ts
5348
- import path29 from "node:path";
5688
+ import path30 from "node:path";
5349
5689
  function buildWorktreeIndex() {
5350
5690
  const index = /* @__PURE__ */ new Map();
5351
5691
  for (const run of listRunRecords()) {
5352
5692
  for (const name of Object.keys(run.workers || {})) {
5353
- const workerPath = path29.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json");
5693
+ const workerPath = path30.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json");
5354
5694
  const worker = readJson(workerPath, void 0);
5355
5695
  if (!worker?.worktreePath) continue;
5356
5696
  const status = computeWorkerStatus(worker, { base: run.base, baseCommit: run.baseCommit });
5357
- index.set(path29.resolve(worker.worktreePath), {
5358
- worktreePath: path29.resolve(worker.worktreePath),
5697
+ index.set(path30.resolve(worker.worktreePath), {
5698
+ worktreePath: path30.resolve(worker.worktreePath),
5359
5699
  runId: run.id,
5360
5700
  workerName: name,
5361
5701
  run,
@@ -5413,13 +5753,143 @@ function resolvePipelineHarnessRetention(runId) {
5413
5753
  });
5414
5754
  }
5415
5755
 
5756
+ // src/cleanup-orphan-safety.ts
5757
+ import { existsSync as existsSync20, statSync as statSync4 } from "node:fs";
5758
+ import path31 from "node:path";
5759
+ var DEFAULT_HEARTBEAT_FRESH_MS = 30 * 60 * 1e3;
5760
+ function assessOrphanWorktreeSafety(input) {
5761
+ const now = input.now ?? Date.now();
5762
+ const heartbeatFreshMs = input.heartbeatFreshMs ?? DEFAULT_HEARTBEAT_FRESH_MS;
5763
+ if (!existsSync20(input.worktreePath)) return null;
5764
+ if (input.runId && input.workerName) {
5765
+ const heartbeatPath = path31.join(
5766
+ input.harnessRoot,
5767
+ "runs",
5768
+ input.runId,
5769
+ "workers",
5770
+ input.workerName,
5771
+ "heartbeat.jsonl"
5772
+ );
5773
+ try {
5774
+ const mtime = statSync4(heartbeatPath).mtimeMs;
5775
+ if (now - mtime < heartbeatFreshMs) return "active_worker";
5776
+ } catch {
5777
+ }
5778
+ }
5779
+ const gitDir = path31.join(input.worktreePath, ".git");
5780
+ if (!existsSync20(gitDir)) return null;
5781
+ const porcelain = gitCapture(input.worktreePath, ["status", "--porcelain"]);
5782
+ if (porcelain.status !== 0) return "pr_or_unmerged_commits";
5783
+ const dirtyLines = porcelain.stdout.split("\n").map((line) => line.trim()).filter((line) => line.length > 0);
5784
+ if (materialWorktreeChanges2(dirtyLines).length > 0) return "dirty_worktree";
5785
+ const upstreamAhead = gitCapture(input.worktreePath, [
5786
+ "rev-list",
5787
+ "--count",
5788
+ "@{u}..HEAD"
5789
+ ]);
5790
+ if (upstreamAhead.status === 0) {
5791
+ const count = Number(upstreamAhead.stdout.trim());
5792
+ if (Number.isFinite(count) && count > 0) return "pr_or_unmerged_commits";
5793
+ }
5794
+ const mainAhead = gitCapture(input.worktreePath, [
5795
+ "rev-list",
5796
+ "--count",
5797
+ "origin/main..HEAD"
5798
+ ]);
5799
+ if (mainAhead.status !== 0) {
5800
+ if (upstreamAhead.status !== 0) return "pr_or_unmerged_commits";
5801
+ return null;
5802
+ }
5803
+ const mainCount = Number(mainAhead.stdout.trim());
5804
+ if (Number.isFinite(mainCount) && mainCount > 0) return "pr_or_unmerged_commits";
5805
+ return null;
5806
+ }
5807
+
5808
+ // src/harness-storage-snapshot.ts
5809
+ import { existsSync as existsSync21, readdirSync as readdirSync7, statSync as statSync5 } from "node:fs";
5810
+ import path32 from "node:path";
5811
+ function harnessStorageSnapshot(opts = {}) {
5812
+ const harnessRoot = opts.harnessRoot ?? resolveHarnessRoot();
5813
+ const worktreesDir = path32.join(harnessRoot, "worktrees");
5814
+ const now = opts.now ?? Date.now();
5815
+ const scannedAt = new Date(now).toISOString();
5816
+ if (!existsSync21(worktreesDir)) {
5817
+ return {
5818
+ harnessRoot,
5819
+ worktreesDir,
5820
+ worktreesBytes: 0,
5821
+ runCount: 0,
5822
+ workerCount: 0,
5823
+ oldestRunAt: null,
5824
+ scannedAt
5825
+ };
5826
+ }
5827
+ let totalBytes = 0;
5828
+ let runCount = 0;
5829
+ let workerCount = 0;
5830
+ let oldestMs = null;
5831
+ let entries;
5832
+ try {
5833
+ entries = readdirSync7(worktreesDir, { withFileTypes: true });
5834
+ } catch {
5835
+ return {
5836
+ harnessRoot,
5837
+ worktreesDir,
5838
+ worktreesBytes: null,
5839
+ runCount: 0,
5840
+ workerCount: 0,
5841
+ oldestRunAt: null,
5842
+ scannedAt
5843
+ };
5844
+ }
5845
+ for (const runEntry of entries) {
5846
+ if (!runEntry.isDirectory()) continue;
5847
+ runCount += 1;
5848
+ const runPath = path32.join(worktreesDir, runEntry.name);
5849
+ try {
5850
+ const st = statSync5(runPath);
5851
+ oldestMs = oldestMs === null ? st.mtimeMs : Math.min(oldestMs, st.mtimeMs);
5852
+ } catch {
5853
+ }
5854
+ try {
5855
+ for (const workerEntry of readdirSync7(runPath, { withFileTypes: true })) {
5856
+ if (workerEntry.isDirectory()) workerCount += 1;
5857
+ }
5858
+ } catch {
5859
+ }
5860
+ if (totalBytes !== null && opts.perRunEntryCap !== null) {
5861
+ const cap = opts.perRunEntryCap ?? 5e4;
5862
+ if (cap <= 0) {
5863
+ totalBytes = null;
5864
+ } else {
5865
+ const runBytes = directorySizeBytes(runPath, cap);
5866
+ if (runBytes === null) totalBytes = null;
5867
+ else totalBytes += runBytes;
5868
+ }
5869
+ }
5870
+ }
5871
+ return {
5872
+ harnessRoot,
5873
+ worktreesDir,
5874
+ worktreesBytes: totalBytes,
5875
+ runCount,
5876
+ workerCount,
5877
+ oldestRunAt: oldestMs === null ? null : new Date(oldestMs).toISOString(),
5878
+ scannedAt
5879
+ };
5880
+ }
5881
+
5416
5882
  // src/cleanup.ts
5417
5883
  function resolvePaths(options = {}) {
5418
5884
  const harnessRoot = options.harnessRoot ? resolveUserPath(options.harnessRoot) : resolveHarnessRoot();
5419
- const { worktreesDir } = options.harnessRoot ? { worktreesDir: path30.join(harnessRoot, "worktrees") } : getHarnessPaths();
5885
+ const { worktreesDir } = options.harnessRoot ? { worktreesDir: path33.join(harnessRoot, "worktrees") } : getHarnessPaths();
5420
5886
  const now = options.now ?? Date.now();
5421
5887
  return { harnessRoot, worktreesDir, now };
5422
5888
  }
5889
+ function normalizeGuardSkip(skip) {
5890
+ if (typeof skip === "string") return { reason: skip };
5891
+ return skip;
5892
+ }
5423
5893
  function recordSkip(skips, pathValue, reason, detail) {
5424
5894
  skips.push({ path: pathValue, reason, ...detail ? { detail } : {} });
5425
5895
  }
@@ -5464,7 +5934,7 @@ function runHarnessCleanup(options = {}) {
5464
5934
  actions.push({ ...candidate, executed: false, skipped: true, skipReason: pathSkip });
5465
5935
  continue;
5466
5936
  }
5467
- const worktreePath = path30.resolve(candidate.path, "..");
5937
+ const worktreePath = path33.resolve(candidate.path, "..");
5468
5938
  const indexed = index.get(worktreePath) ?? null;
5469
5939
  const guardReason = skipNodeModulesRemoval({
5470
5940
  indexed,
@@ -5481,15 +5951,26 @@ function runHarnessCleanup(options = {}) {
5481
5951
  }
5482
5952
  for (const raw of scanWorktreeCandidates(scanOpts)) {
5483
5953
  const candidate = attachCandidateBytes(raw, retention.accountBytes);
5484
- const indexed = index.get(path30.resolve(candidate.path)) ?? null;
5485
- const guardReason = skipWorktreeRemoval({
5954
+ const indexed = index.get(path33.resolve(candidate.path)) ?? null;
5955
+ const orphanSafety = indexed ? null : assessOrphanWorktreeSafety({
5956
+ worktreePath: candidate.path,
5957
+ harnessRoot: paths.harnessRoot,
5958
+ runId: candidate.runId,
5959
+ workerName: candidate.worker,
5960
+ now: paths.now
5961
+ });
5962
+ const guardSkip = skipWorktreeRemoval({
5486
5963
  indexed,
5964
+ worktreePath: path33.resolve(candidate.path),
5487
5965
  includeOrphans: retention.includeOrphans,
5488
5966
  worktreesAgeMs: retention.worktreesAgeMs,
5489
- ageMs: candidate.ageMs
5967
+ ageMs: candidate.ageMs,
5968
+ orphanSafety,
5969
+ worktreeRemovalGuard: options.worktreeRemovalGuard
5490
5970
  });
5491
- if (guardReason) {
5492
- recordSkip(skips, candidate.path, guardReason);
5971
+ if (guardSkip) {
5972
+ const { reason: guardReason, detail: guardDetail } = normalizeGuardSkip(guardSkip);
5973
+ recordSkip(skips, candidate.path, guardReason, guardDetail);
5493
5974
  actions.push({ ...candidate, executed: false, skipped: true, skipReason: guardReason });
5494
5975
  continue;
5495
5976
  }
@@ -5511,6 +5992,7 @@ function runHarnessCleanup(options = {}) {
5511
5992
  if (action.skipReason === "dry_run" && action.bytes) reclaimableBytes += action.bytes;
5512
5993
  }
5513
5994
  }
5995
+ const storage = retention.accountBytes ? harnessStorageSnapshot({ harnessRoot: paths.harnessRoot, now: paths.now }) : void 0;
5514
5996
  return {
5515
5997
  harnessRoot: paths.harnessRoot,
5516
5998
  dryRun: !retention.execute,
@@ -5529,7 +6011,8 @@ function runHarnessCleanup(options = {}) {
5529
6011
  removedPaths,
5530
6012
  skippedPaths,
5531
6013
  skipReasons: tallySkipReasons(actions, skips)
5532
- }
6014
+ },
6015
+ ...storage ? { storage } : {}
5533
6016
  };
5534
6017
  }
5535
6018
  function runPipelineHarnessCleanup(runId) {
@@ -5551,7 +6034,7 @@ function isPipelineCleanupEnabled() {
5551
6034
  // src/installed-package-versions.ts
5552
6035
  import { readFile } from "node:fs/promises";
5553
6036
  import { homedir as homedir6 } from "node:os";
5554
- import path31 from "node:path";
6037
+ import path34 from "node:path";
5555
6038
  var MANAGED_PACKAGES = [
5556
6039
  "@kynver-app/runtime",
5557
6040
  "@kynver-app/openclaw-agent-os",
@@ -5566,12 +6049,12 @@ function unique(values) {
5566
6049
  }
5567
6050
  function moduleRoots() {
5568
6051
  const home = homedir6();
5569
- const openClawPrefix = trim(process.env.KYNVER_OPENCLAW_NPM_ROOT) ?? trim(process.env.OPENCLAW_NPM_ROOT) ?? path31.join(home, ".openclaw", "npm");
5570
- const npmGlobalRoot = trim(process.env.KYNVER_NPM_GLOBAL_ROOT) ?? trim(process.env.KYNVER_NPM_GLOBAL_MODULES_ROOT) ?? (trim(process.env.NPM_CONFIG_PREFIX) ? path31.join(trim(process.env.NPM_CONFIG_PREFIX), "lib", "node_modules") : path31.join(home, ".npm-global", "lib", "node_modules"));
6052
+ const openClawPrefix = trim(process.env.KYNVER_OPENCLAW_NPM_ROOT) ?? trim(process.env.OPENCLAW_NPM_ROOT) ?? path34.join(home, ".openclaw", "npm");
6053
+ const npmGlobalRoot = trim(process.env.KYNVER_NPM_GLOBAL_ROOT) ?? trim(process.env.KYNVER_NPM_GLOBAL_MODULES_ROOT) ?? (trim(process.env.NPM_CONFIG_PREFIX) ? path34.join(trim(process.env.NPM_CONFIG_PREFIX), "lib", "node_modules") : path34.join(home, ".npm-global", "lib", "node_modules"));
5571
6054
  return unique([
5572
- path31.join(openClawPrefix, "lib", "node_modules"),
5573
- path31.join(openClawPrefix, "node_modules"),
5574
- npmGlobalRoot.endsWith("node_modules") ? npmGlobalRoot : path31.join(npmGlobalRoot, "lib", "node_modules")
6055
+ path34.join(openClawPrefix, "lib", "node_modules"),
6056
+ path34.join(openClawPrefix, "node_modules"),
6057
+ npmGlobalRoot.endsWith("node_modules") ? npmGlobalRoot : path34.join(npmGlobalRoot, "lib", "node_modules")
5575
6058
  ]);
5576
6059
  }
5577
6060
  async function readVersion(packageJsonPath) {
@@ -5587,7 +6070,7 @@ async function collectInstalledPackageVersions(observedAt = (/* @__PURE__ */ new
5587
6070
  const out = {};
5588
6071
  for (const packageName of MANAGED_PACKAGES) {
5589
6072
  for (const root of roots) {
5590
- const packageJsonPath = path31.join(root, packageName, "package.json");
6073
+ const packageJsonPath = path34.join(root, packageName, "package.json");
5591
6074
  const version = await readVersion(packageJsonPath);
5592
6075
  if (!version) continue;
5593
6076
  out[packageName] = { version, observedAt, path: packageJsonPath };
@@ -5603,7 +6086,7 @@ async function completeFinishedWorkers(runId, args) {
5603
6086
  const outcomes = [];
5604
6087
  for (const name of Object.keys(run.workers || {})) {
5605
6088
  const worker = readJson(
5606
- path32.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
6089
+ path35.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
5607
6090
  void 0
5608
6091
  );
5609
6092
  if (!worker?.taskId || worker.localOnly) continue;
@@ -5745,7 +6228,7 @@ async function runDaemon(args) {
5745
6228
  }
5746
6229
 
5747
6230
  // src/plan-progress.ts
5748
- import path33 from "node:path";
6231
+ import path36 from "node:path";
5749
6232
 
5750
6233
  // src/bounded-build/constants.ts
5751
6234
  var DEFAULT_BUILD_MEM_BUDGET_BYTES = 1536 * 1024 * 1024;
@@ -6031,7 +6514,7 @@ async function emitPlanProgress(args) {
6031
6514
  }
6032
6515
  function verifyPlanLocal(args) {
6033
6516
  const worktree = required(args.worktree ? String(args.worktree) : void 0, "worktree");
6034
- const cwd = path33.resolve(worktree);
6517
+ const cwd = path36.resolve(worktree);
6035
6518
  const summary = runHarnessVerifyCommands(cwd);
6036
6519
  const emitJson = args.json === true || args.json === "true";
6037
6520
  const payload = { passed: summary.passed, worktree: cwd, steps: summary.steps };
@@ -6080,9 +6563,9 @@ async function verifyPlan(args) {
6080
6563
  }
6081
6564
 
6082
6565
  // src/harness-verify-cli.ts
6083
- import path34 from "node:path";
6566
+ import path37 from "node:path";
6084
6567
  function runHarnessVerifyCli(args) {
6085
- const cwd = path34.resolve(required(args.worktree ? String(args.worktree) : void 0, "worktree"));
6568
+ const cwd = path37.resolve(required(args.worktree ? String(args.worktree) : void 0, "worktree"));
6086
6569
  const emitJson = args.json === true || args.json === "true" || args.emitJson === true || args.emitJson === "true";
6087
6570
  const commands = [];
6088
6571
  const rawCmd = args.command;
@@ -6126,7 +6609,7 @@ function runHarnessVerifyCli(args) {
6126
6609
  }
6127
6610
 
6128
6611
  // src/plan-persist-cli.ts
6129
- import { readFileSync as readFileSync8 } from "node:fs";
6612
+ import { readFileSync as readFileSync9 } from "node:fs";
6130
6613
  var OPERATIONS = ["create", "add_version", "update_metadata"];
6131
6614
  var FAILURE_KINDS = [
6132
6615
  "approval_guard",
@@ -6138,7 +6621,7 @@ var FAILURE_KINDS = [
6138
6621
  function readBodyArg(args) {
6139
6622
  const bodyFile = args.bodyFile ? String(args.bodyFile) : void 0;
6140
6623
  if (bodyFile) {
6141
- return { body: readFileSync8(bodyFile, "utf8"), bodyPathHint: bodyFile };
6624
+ return { body: readFileSync9(bodyFile, "utf8"), bodyPathHint: bodyFile };
6142
6625
  }
6143
6626
  const inline = args.body ? String(args.body) : void 0;
6144
6627
  if (inline) return { body: inline };
@@ -6228,7 +6711,7 @@ function runCleanupCli(args) {
6228
6711
  }
6229
6712
 
6230
6713
  // src/monitor/monitor.service.ts
6231
- import path36 from "node:path";
6714
+ import path39 from "node:path";
6232
6715
 
6233
6716
  // src/monitor/monitor.classify.ts
6234
6717
  function expectedLeaseOwner(runId) {
@@ -6284,11 +6767,11 @@ function classifyWorkerHealth(input) {
6284
6767
  }
6285
6768
 
6286
6769
  // src/monitor/monitor.store.ts
6287
- import { existsSync as existsSync18, mkdirSync as mkdirSync6, readdirSync as readdirSync7, unlinkSync as unlinkSync2 } from "node:fs";
6288
- import path35 from "node:path";
6770
+ import { existsSync as existsSync22, mkdirSync as mkdirSync6, readdirSync as readdirSync8, unlinkSync as unlinkSync2 } from "node:fs";
6771
+ import path38 from "node:path";
6289
6772
  function monitorsDir() {
6290
6773
  const { harnessRoot } = getHarnessPaths();
6291
- const dir = path35.join(harnessRoot, "monitors");
6774
+ const dir = path38.join(harnessRoot, "monitors");
6292
6775
  mkdirSync6(dir, { recursive: true });
6293
6776
  return dir;
6294
6777
  }
@@ -6296,7 +6779,7 @@ function monitorIdFor(runId, workerName) {
6296
6779
  return workerName ? `${safeSlug(runId)}--${safeSlug(workerName)}` : safeSlug(runId);
6297
6780
  }
6298
6781
  function monitorPath(monitorId) {
6299
- return path35.join(monitorsDir(), `${monitorId}.json`);
6782
+ return path38.join(monitorsDir(), `${monitorId}.json`);
6300
6783
  }
6301
6784
  function loadMonitorSession(monitorId) {
6302
6785
  return readJson(monitorPath(monitorId), void 0);
@@ -6306,18 +6789,18 @@ function saveMonitorSession(session) {
6306
6789
  }
6307
6790
  function deleteMonitorSession(monitorId) {
6308
6791
  const file = monitorPath(monitorId);
6309
- if (!existsSync18(file)) return false;
6792
+ if (!existsSync22(file)) return false;
6310
6793
  unlinkSync2(file);
6311
6794
  return true;
6312
6795
  }
6313
6796
  function listMonitorSessions() {
6314
6797
  const dir = monitorsDir();
6315
- if (!existsSync18(dir)) return [];
6798
+ if (!existsSync22(dir)) return [];
6316
6799
  const entries = [];
6317
- for (const name of readdirSync7(dir)) {
6800
+ for (const name of readdirSync8(dir)) {
6318
6801
  if (!name.endsWith(".json")) continue;
6319
6802
  const session = readJson(
6320
- path35.join(dir, name),
6803
+ path38.join(dir, name),
6321
6804
  void 0
6322
6805
  );
6323
6806
  if (!session?.monitorId) continue;
@@ -6408,7 +6891,7 @@ async function fetchTaskLeasesForWorkers(input) {
6408
6891
  // src/monitor/monitor.service.ts
6409
6892
  function workerRecord2(runId, name) {
6410
6893
  return readJson(
6411
- path36.join(runDirectory(runId), "workers", safeSlug(name), "worker.json"),
6894
+ path39.join(runDirectory(runId), "workers", safeSlug(name), "worker.json"),
6412
6895
  void 0
6413
6896
  );
6414
6897
  }
@@ -6610,18 +7093,18 @@ async function runMonitorLoop(args) {
6610
7093
 
6611
7094
  // src/monitor/monitor-spawn.ts
6612
7095
  import { spawn as spawn4 } from "node:child_process";
6613
- import { closeSync as closeSync4, existsSync as existsSync19, openSync as openSync4 } from "node:fs";
6614
- import path37 from "node:path";
7096
+ import { closeSync as closeSync4, existsSync as existsSync23, openSync as openSync4 } from "node:fs";
7097
+ import path40 from "node:path";
6615
7098
  import { fileURLToPath as fileURLToPath3 } from "node:url";
6616
7099
  function resolveDefaultCliPath2() {
6617
- return path37.join(fileURLToPath3(new URL(".", import.meta.url)), "..", "cli.js");
7100
+ return path40.join(fileURLToPath3(new URL(".", import.meta.url)), "cli.js");
6618
7101
  }
6619
7102
  function spawnMonitorSidecar(opts) {
6620
7103
  const cliPath = opts.cliPath ?? resolveDefaultCliPath2();
6621
- if (!existsSync19(cliPath)) return void 0;
7104
+ if (!existsSync23(cliPath)) return void 0;
6622
7105
  const monitorId = monitorIdFor(opts.runId, opts.workerName);
6623
7106
  const { harnessRoot } = getHarnessPaths();
6624
- const logPath = path37.join(harnessRoot, "monitors", `${monitorId}.log`);
7107
+ const logPath = path40.join(harnessRoot, "monitors", `${monitorId}.log`);
6625
7108
  let logFd;
6626
7109
  try {
6627
7110
  logFd = openSync4(logPath, "a");
@@ -6741,13 +7224,13 @@ async function monitorTickCli(args) {
6741
7224
  }
6742
7225
 
6743
7226
  // src/package-version.ts
6744
- import { existsSync as existsSync20, readFileSync as readFileSync9 } from "node:fs";
7227
+ import { existsSync as existsSync24, readFileSync as readFileSync10 } from "node:fs";
6745
7228
  import { dirname, join } from "node:path";
6746
7229
  import { fileURLToPath as fileURLToPath4 } from "node:url";
6747
7230
  function resolvePackageRoot(moduleUrl) {
6748
7231
  let dir = dirname(fileURLToPath4(moduleUrl));
6749
7232
  for (let depth = 0; depth < 6; depth += 1) {
6750
- if (existsSync20(join(dir, "package.json"))) return dir;
7233
+ if (existsSync24(join(dir, "package.json"))) return dir;
6751
7234
  const parent = dirname(dir);
6752
7235
  if (parent === dir) break;
6753
7236
  dir = parent;
@@ -6756,7 +7239,7 @@ function resolvePackageRoot(moduleUrl) {
6756
7239
  }
6757
7240
  function readOwnPackageVersion(moduleUrl = import.meta.url) {
6758
7241
  const pkgPath = join(resolvePackageRoot(moduleUrl), "package.json");
6759
- const pkg = JSON.parse(readFileSync9(pkgPath, "utf8"));
7242
+ const pkg = JSON.parse(readFileSync10(pkgPath, "utf8"));
6760
7243
  if (typeof pkg.version !== "string" || !pkg.version.trim()) {
6761
7244
  throw new Error(`Missing package.json version at ${pkgPath}`);
6762
7245
  }
@@ -6777,12 +7260,12 @@ function handleCliVersionFlag(argv, moduleUrl = import.meta.url, binName) {
6777
7260
  }
6778
7261
 
6779
7262
  // src/doctor/runtime-takeover.ts
6780
- import path39 from "node:path";
7263
+ import path42 from "node:path";
6781
7264
 
6782
7265
  // src/doctor/runtime-takeover.probes.ts
6783
- import { accessSync, constants, existsSync as existsSync21, readFileSync as readFileSync10 } from "node:fs";
7266
+ import { accessSync, constants, existsSync as existsSync25, readFileSync as readFileSync11 } from "node:fs";
6784
7267
  import { homedir as homedir7 } from "node:os";
6785
- import path38 from "node:path";
7268
+ import path41 from "node:path";
6786
7269
  import { spawnSync as spawnSync6 } from "node:child_process";
6787
7270
  function captureCommand(bin, args) {
6788
7271
  try {
@@ -6811,7 +7294,7 @@ function tokenPrefix(token) {
6811
7294
  return trimmed.length <= 12 ? `${trimmed}\u2026` : `${trimmed.slice(0, 12)}\u2026`;
6812
7295
  }
6813
7296
  function isWritable(target) {
6814
- if (!existsSync21(target)) return false;
7297
+ if (!existsSync25(target)) return false;
6815
7298
  try {
6816
7299
  accessSync(target, constants.W_OK);
6817
7300
  return true;
@@ -6824,15 +7307,15 @@ var defaultRuntimeTakeoverProbes = {
6824
7307
  commandOnPath: (bin) => captureCommand(process.platform === "win32" ? "where" : "which", [bin]),
6825
7308
  kynverVersion: (bin) => captureCommand(bin, ["--version"]),
6826
7309
  loadConfig: () => loadUserConfig(),
6827
- configFilePath: () => path38.join(homedir7(), ".kynver", "config.json"),
6828
- credentialsFilePath: () => path38.join(homedir7(), ".kynver", "credentials"),
7310
+ configFilePath: () => path41.join(homedir7(), ".kynver", "config.json"),
7311
+ credentialsFilePath: () => path41.join(homedir7(), ".kynver", "credentials"),
6829
7312
  readCredentials: () => {
6830
- const credPath = path38.join(homedir7(), ".kynver", "credentials");
6831
- if (!existsSync21(credPath)) {
7313
+ const credPath = path41.join(homedir7(), ".kynver", "credentials");
7314
+ if (!existsSync25(credPath)) {
6832
7315
  return { hasApiKey: false };
6833
7316
  }
6834
7317
  try {
6835
- const parsed = JSON.parse(readFileSync10(credPath, "utf8"));
7318
+ const parsed = JSON.parse(readFileSync11(credPath, "utf8"));
6836
7319
  return {
6837
7320
  hasApiKey: Boolean(parsed.apiKey?.trim()),
6838
7321
  runnerTokenPrefix: tokenPrefix(parsed.runnerToken),
@@ -6851,6 +7334,7 @@ var defaultRuntimeTakeoverProbes = {
6851
7334
  kynverHarnessRoot: process.env.KYNVER_HARNESS_ROOT?.trim() || void 0,
6852
7335
  opusHarnessRoot: process.env.OPUS_HARNESS_ROOT?.trim() || void 0,
6853
7336
  kynverSchedulerProvider: process.env.KYNVER_SCHEDULER_PROVIDER?.trim() || void 0,
7337
+ openclawCronStorePath: Boolean(process.env.OPENCLAW_CRON_STORE_PATH?.trim()),
6854
7338
  qstashTokenPresent: Boolean(process.env.QSTASH_TOKEN?.trim()),
6855
7339
  kynverHostedDeployment: (() => {
6856
7340
  const v = process.env.KYNVER_HOSTED_DEPLOYMENT?.trim().toLowerCase();
@@ -6858,18 +7342,59 @@ var defaultRuntimeTakeoverProbes = {
6858
7342
  })()
6859
7343
  }),
6860
7344
  harnessRoot: () => resolveHarnessRoot(),
6861
- legacyOpenclawHarnessRoot: () => path38.join(homedir7(), ".openclaw", "harness"),
6862
- pathExists: (target) => existsSync21(target),
7345
+ legacyOpenclawHarnessRoot: () => path41.join(homedir7(), ".openclaw", "harness"),
7346
+ pathExists: (target) => existsSync25(target),
6863
7347
  pathWritable: (target) => isWritable(target),
6864
7348
  vercelVersion: () => captureCommand("vercel", ["--version"]),
6865
7349
  vercelWhoami: () => captureCommand("vercel", ["whoami"])
6866
7350
  };
6867
7351
 
6868
7352
  // src/doctor/runtime-takeover-scheduler.ts
7353
+ function hasLocalOpenClawDependency(env, ctx) {
7354
+ return env.kynverSchedulerProvider === "kynver-cron" || env.kynverSchedulerProvider === "openclaw-cron" || ctx.deploymentSchedulerProvider === "kynver-cron" || ctx.deploymentSchedulerProvider === "openclaw-cron" || Boolean(env.openclawCronStorePath);
7355
+ }
7356
+ function hasQstashCutover(env, ctx) {
7357
+ return env.kynverSchedulerProvider === "qstash" || ctx.deploymentSchedulerProvider === "qstash";
7358
+ }
6869
7359
  function check(partial) {
6870
7360
  return partial;
6871
7361
  }
6872
7362
  function assessRuntimeTakeoverScheduler(env, ctx) {
7363
+ const schedulerDetails = {
7364
+ schedulerProvider: env.kynverSchedulerProvider ?? null,
7365
+ deploymentSchedulerProvider: ctx.deploymentSchedulerProvider ?? null,
7366
+ openclawCronStorePath: Boolean(env.openclawCronStorePath)
7367
+ };
7368
+ if (hasQstashCutover(env, ctx) && !hasLocalOpenClawDependency(env, ctx)) {
7369
+ const source = env.kynverSchedulerProvider === "qstash" ? "KYNVER_SCHEDULER_PROVIDER=qstash on this host" : "deploymentSchedulerProvider=qstash in ~/.kynver/config.json";
7370
+ return check({
7371
+ id: "hotspot_openclaw_scheduler",
7372
+ label: "Scheduler provider (runtime daemon vs OpenClaw cron)",
7373
+ status: "pass",
7374
+ summary: `AgentOS scheduler cut over to QStash (${source})`,
7375
+ details: schedulerDetails
7376
+ });
7377
+ }
7378
+ if (hasLocalOpenClawDependency(env, ctx)) {
7379
+ const parts = [];
7380
+ if (env.kynverSchedulerProvider === "openclaw-cron") {
7381
+ parts.push("KYNVER_SCHEDULER_PROVIDER=openclaw-cron");
7382
+ }
7383
+ if (ctx.deploymentSchedulerProvider === "openclaw-cron") {
7384
+ parts.push("deploymentSchedulerProvider=openclaw-cron in config");
7385
+ }
7386
+ if (env.openclawCronStorePath) {
7387
+ parts.push("OPENCLAW_CRON_STORE_PATH set (local cron bridge)");
7388
+ }
7389
+ return check({
7390
+ id: "hotspot_openclaw_scheduler",
7391
+ label: "Scheduler provider (runtime daemon vs OpenClaw cron)",
7392
+ status: "warn",
7393
+ summary: `OpenClaw local cron still active (${parts.join("; ")})`,
7394
+ 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.",
7395
+ details: schedulerDetails
7396
+ });
7397
+ }
6873
7398
  const runnerOpenclaw = env.kynverSchedulerProvider === "openclaw-cron";
6874
7399
  const runnerQstash = env.kynverSchedulerProvider === "qstash";
6875
7400
  const hostedDeployment = Boolean(env.qstashTokenPresent) || Boolean(env.kynverHostedDeployment);
@@ -7146,8 +7671,8 @@ function assessVercelCli(probes) {
7146
7671
  }
7147
7672
  function assessHarnessDirs(probes) {
7148
7673
  const harnessRoot = probes.harnessRoot();
7149
- const runsDir = path39.join(harnessRoot, "runs");
7150
- const worktreesDir = path39.join(harnessRoot, "worktrees");
7674
+ const runsDir = path42.join(harnessRoot, "runs");
7675
+ const worktreesDir = path42.join(harnessRoot, "worktrees");
7151
7676
  const displayHarnessRoot = redactHomePath(harnessRoot);
7152
7677
  const displayRunsDir = redactHomePath(runsDir);
7153
7678
  const displayWorktreesDir = redactHomePath(worktreesDir);
@@ -7256,7 +7781,8 @@ function assessOpenclawHotspots(probes) {
7256
7781
  assessRuntimeTakeoverScheduler(env, {
7257
7782
  agentOsId: targetAgentOsId ?? null,
7258
7783
  apiBaseUrl: config.apiBaseUrl?.trim() ?? env.kynverApiUrl ?? null,
7259
- hasScopedRunnerToken
7784
+ hasScopedRunnerToken,
7785
+ deploymentSchedulerProvider: config.deploymentSchedulerProvider === "qstash" || config.deploymentSchedulerProvider === "kynver-cron" || config.deploymentSchedulerProvider === "openclaw-cron" ? config.deploymentSchedulerProvider : void 0
7260
7786
  }),
7261
7787
  check2({
7262
7788
  id: "hotspot_lease_source_names",
@@ -7362,6 +7888,7 @@ function usage(code = 0) {
7362
7888
  " kynver worker tail --run RUN_ID --name worker [--lines 40] [--raw]",
7363
7889
  " kynver worker stop --run RUN_ID --name worker",
7364
7890
  " kynver worker complete --run RUN_ID --name worker [--agent-os-id AOS_ID] [--task-id TASK_ID] [--base-url URL] [--secret SECRET]",
7891
+ " kynver worker discard-disposable --run RUN_ID --name worker --path scripts/helper.mjs[,other]",
7365
7892
  " kynver worker auto-complete --run RUN_ID --name worker [--agent-os-id AOS_ID] [--poll-ms 5000] [--max-total-ms 21600000] [--complete-attempts 3] [--complete-backoff-ms 5000] [--base-url URL] [--secret SECRET]",
7366
7893
  " kynver run reconcile",
7367
7894
  " kynver plan progress --plan PLAN_ID --row ROW_KEY --role ROLE --status STATUS [--task TASK_ID] [--note NOTE] [--evidence type:value] [--agent-os-id AOS_ID]",
@@ -7371,6 +7898,7 @@ function usage(code = 0) {
7371
7898
  " kynver plan outbox list",
7372
7899
  " kynver plan outbox drain [--max N] [--id OUTBOX_ID]",
7373
7900
  " kynver cleanup [--execute] [--node-modules-age-ms MS] [--worktrees-age-ms MS] [--harness-root PATH] [--include-orphans] [--skip-finalize]",
7901
+ " --include-orphans also scans whole worktree directories (<harnessRoot>/worktrees/<runId>/<workerId>/) that no run/worker.json references; orphans pass salvage gates (recent heartbeat, dirty git, ahead of origin/main) before removal.",
7374
7902
  " kynver monitor start --run RUN_ID [--name worker] [--agent-os-id AOS_ID] [--poll-ms MS]",
7375
7903
  " kynver monitor status [--run RUN_ID] [--name worker] [--tick]",
7376
7904
  " kynver monitor stop --run RUN_ID [--name worker]",
@@ -7431,6 +7959,7 @@ async function main(argv = process.argv.slice(2)) {
7431
7959
  if (scope === "worker" && action === "tail") return tailWorker(args);
7432
7960
  if (scope === "worker" && action === "stop") return stopWorker(args);
7433
7961
  if (scope === "worker" && action === "complete") return void await completeWorker(args);
7962
+ if (scope === "worker" && action === "discard-disposable") return discardDisposableCli(args);
7434
7963
  if (scope === "worker" && action === "auto-complete") return void await autoCompleteWorkerCli(args);
7435
7964
  if (scope === "monitor" && action === "start") {
7436
7965
  const result = await startMonitorCli(args);