@kynver-app/runtime 0.1.91 → 0.1.92

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -319,9 +319,9 @@ function shouldEnforceMemoryCostPackageGuardCli(scope, action) {
319
319
  }
320
320
 
321
321
  // src/config.ts
322
- import { existsSync as existsSync5, mkdirSync as mkdirSync2, readFileSync as readFileSync5, writeFileSync as writeFileSync2 } from "node:fs";
323
- import { homedir as homedir4 } from "node:os";
324
- import path6 from "node:path";
322
+ import { existsSync as existsSync11, mkdirSync as mkdirSync2, readFileSync as readFileSync9, writeFileSync as writeFileSync2 } from "node:fs";
323
+ import { homedir as homedir5, totalmem } from "node:os";
324
+ import path10 from "node:path";
325
325
 
326
326
  // src/default-repo-discovery.ts
327
327
  import { existsSync as existsSync4, readFileSync as readFileSync4 } from "node:fs";
@@ -705,775 +705,440 @@ function discoverDefaultRepo(opts) {
705
705
  return discoverDefaultRepoCandidates(opts)[0] ?? null;
706
706
  }
707
707
 
708
- // src/config.ts
709
- var CONFIG_DIR = path6.join(homedir4(), ".kynver");
710
- var CONFIG_FILE = path6.join(CONFIG_DIR, "config.json");
711
- var CREDENTIALS_FILE = path6.join(CONFIG_DIR, "credentials");
712
- function loadUserConfig() {
713
- if (!existsSync5(CONFIG_FILE)) return {};
714
- try {
715
- return JSON.parse(readFileSync5(CONFIG_FILE, "utf8"));
716
- } catch {
717
- return {};
708
+ // src/box-identity.ts
709
+ function normalizeWorkerPoolBoxKind(raw) {
710
+ const kind = (raw ?? "").trim().toLowerCase();
711
+ if (kind === "ghost" || kind === "forge") return kind;
712
+ if (kind.includes("forge")) return "forge";
713
+ if (kind.includes("ghost") || kind.includes("openclaw")) return "ghost";
714
+ return "forge";
715
+ }
716
+ function trimEnv(env, key) {
717
+ const value = env[key]?.trim();
718
+ return value || null;
719
+ }
720
+ function resolveBoxIdentity(env = process.env, config = {}) {
721
+ const warnings = [];
722
+ const configKind = config.boxKind?.trim();
723
+ if (configKind) {
724
+ return {
725
+ boxKind: normalizeWorkerPoolBoxKind(configKind),
726
+ source: "config",
727
+ slugInferenceBlocked: false,
728
+ warnings
729
+ };
730
+ }
731
+ const envKind = trimEnv(env, "KYNVER_BOX_KIND");
732
+ if (envKind) {
733
+ return {
734
+ boxKind: normalizeWorkerPoolBoxKind(envKind),
735
+ source: "env",
736
+ slugInferenceBlocked: false,
737
+ warnings
738
+ };
739
+ }
740
+ const agentOsSlug = trimEnv(env, "KYNVER_AGENT_OS_SLUG");
741
+ if (agentOsSlug) {
742
+ warnings.push(
743
+ `KYNVER_AGENT_OS_SLUG=${agentOsSlug} is a workspace slug, not box identity \u2014 set boxKind via \`kynver setup --box-kind forge|ghost\` or KYNVER_BOX_KIND (defaulting box kind to forge)`
744
+ );
718
745
  }
746
+ return {
747
+ boxKind: "forge",
748
+ source: "default",
749
+ slugInferenceBlocked: Boolean(agentOsSlug),
750
+ warnings
751
+ };
719
752
  }
720
- function saveUserConfig(config) {
721
- mkdirSync2(CONFIG_DIR, { recursive: true });
722
- writeFileSync2(CONFIG_FILE, `${JSON.stringify(normalizeConfigPaths(config), null, 2)}
723
- `, { mode: 384 });
753
+ function resolveBoxKindFromConfig(config = {}, env = process.env) {
754
+ return resolveBoxIdentity(env, config).boxKind;
724
755
  }
725
- function normalizeConfigPaths(config) {
756
+
757
+ // src/resource-gate.ts
758
+ import os2 from "node:os";
759
+
760
+ // src/bounded-build/meminfo.ts
761
+ import { readFileSync as readFileSync5 } from "node:fs";
762
+ import os from "node:os";
763
+ function readMemAvailableBytes(meminfoText) {
764
+ if (meminfoText !== void 0) {
765
+ const match = meminfoText.match(/^MemAvailable:\s+(\d+)\s*kB/m);
766
+ if (match) return Number(match[1]) * 1024;
767
+ return os.freemem();
768
+ }
769
+ if (process.platform === "linux") {
770
+ try {
771
+ const meminfo = readFileSync5("/proc/meminfo", "utf8");
772
+ const match = meminfo.match(/^MemAvailable:\s+(\d+)\s*kB/m);
773
+ if (match) return Number(match[1]) * 1024;
774
+ } catch {
775
+ }
776
+ }
777
+ return os.freemem();
778
+ }
779
+
780
+ // src/resource-gate.ts
781
+ import path9 from "node:path";
782
+
783
+ // src/disk-gate.ts
784
+ import { statfsSync as statfsSync2 } from "node:fs";
785
+
786
+ // src/wsl-host.ts
787
+ import { existsSync as existsSync5, readFileSync as readFileSync6, statfsSync } from "node:fs";
788
+ var DEFAULT_WSL_HOST_WARN_FREE_BYTES = 25 * 1024 * 1024 * 1024;
789
+ var DEFAULT_WSL_HOST_CRITICAL_FREE_BYTES = 12 * 1024 * 1024 * 1024;
790
+ var DEFAULT_WSL_HOST_MOUNT = "/mnt/c";
791
+ function isWslHost() {
792
+ if (process.platform !== "linux") return false;
793
+ for (const probe of ["/proc/sys/kernel/osrelease", "/proc/version"]) {
794
+ try {
795
+ if (!existsSync5(probe)) continue;
796
+ const text = readFileSync6(probe, "utf8");
797
+ if (/microsoft|wsl/i.test(text)) return true;
798
+ } catch {
799
+ }
800
+ }
801
+ return false;
802
+ }
803
+ function observeWslHostDisk(options = {}) {
804
+ const wsl = options.forceWsl === void 0 ? isWslHost() : options.forceWsl;
805
+ if (!wsl) return null;
806
+ const path69 = options.wslHostMount?.trim() || process.env.KYNVER_WSL_HOST_MOUNT?.trim() || DEFAULT_WSL_HOST_MOUNT;
807
+ const warnBelowBytes = options.wslHostFreeWarnBytes ?? DEFAULT_WSL_HOST_WARN_FREE_BYTES;
808
+ const criticalBelowBytes = options.wslHostFreeCriticalBytes ?? DEFAULT_WSL_HOST_CRITICAL_FREE_BYTES;
809
+ const statfs = options.statfs ?? statfsSync;
810
+ let stats;
811
+ try {
812
+ stats = statfs(path69);
813
+ } catch (error) {
814
+ return {
815
+ ok: false,
816
+ path: path69,
817
+ freeBytes: 0,
818
+ totalBytes: 0,
819
+ usedPercent: 100,
820
+ warnBelowBytes,
821
+ criticalBelowBytes,
822
+ reason: `Windows host disk probe failed at ${path69}: ${error.message}`,
823
+ probeError: error.message
824
+ };
825
+ }
826
+ const freeBytes = Number(stats.bavail) * Number(stats.bsize);
827
+ const totalBytes = Number(stats.blocks) * Number(stats.bsize);
828
+ const usedPercent = totalBytes > 0 ? (totalBytes - freeBytes) / totalBytes * 100 : 100;
829
+ const lowFree = freeBytes < warnBelowBytes;
830
+ const criticalFree = freeBytes < criticalBelowBytes;
831
+ const ok = !lowFree && !criticalFree;
832
+ const freeGiB = (freeBytes / (1024 * 1024 * 1024)).toFixed(1);
833
+ let reason = null;
834
+ if (!ok) {
835
+ const tag = criticalFree ? "critical" : "warning";
836
+ reason = `Windows host disk ${path69} at ${tag}: ${freeGiB} GiB free (<${(criticalFree ? criticalBelowBytes : warnBelowBytes) / 1024 / 1024 / 1024} GiB); WSL VHDX cannot grow safely. ${summarizeWslRecoverySteps()}`;
837
+ }
726
838
  return {
727
- ...config,
728
- ...config.harnessRoot?.trim() ? { harnessRoot: redactHomePath(config.harnessRoot.trim()) } : {},
729
- ...config.defaultRepo?.trim() ? { defaultRepo: redactHomePath(config.defaultRepo.trim()) } : {}
839
+ ok,
840
+ path: path69,
841
+ freeBytes,
842
+ totalBytes,
843
+ usedPercent,
844
+ warnBelowBytes,
845
+ criticalBelowBytes,
846
+ reason,
847
+ probeError: null
730
848
  };
731
849
  }
732
- function presentUserConfig(config) {
733
- return normalizeConfigPaths(config);
850
+ function summarizeWslRecoverySteps() {
851
+ 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.";
734
852
  }
735
- function inferSetupFields(existing, args) {
736
- const creds = loadCredentialsFile();
737
- 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();
738
- 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);
739
- const explicitRepo = typeof args.repo === "string" ? args.repo : args.discoverRepo === true || args.discoverRepo === "true" ? discoverDefaultRepo()?.repo : void 0;
740
- const defaultRepo = explicitRepo || existing.defaultRepo?.trim() || process.env.KYNVER_DEFAULT_REPO?.trim() || process.env.KYNVER_HARNESS_REPO?.trim() || discoverDefaultRepo()?.repo;
741
- const harnessRoot = (typeof args.harnessRoot === "string" ? args.harnessRoot : void 0) || existing.harnessRoot?.trim() || process.env.KYNVER_HARNESS_ROOT?.trim() || process.env.OPUS_HARNESS_ROOT?.trim();
853
+
854
+ // src/disk-gate.ts
855
+ var DEFAULT_WARN_FREE_BYTES = 30 * 1024 * 1024 * 1024;
856
+ var DEFAULT_CRITICAL_FREE_BYTES = 15 * 1024 * 1024 * 1024;
857
+ var DEFAULT_MAX_USED_PERCENT = 80;
858
+ var DEFAULT_HARD_MAX_USED_PERCENT = 90;
859
+ function observeRunnerDiskGate(input = {}) {
860
+ const path69 = input.diskPath?.trim() || "/";
861
+ const warnBelowBytes = input.diskFreeWarnBytes ?? DEFAULT_WARN_FREE_BYTES;
862
+ const criticalBelowBytes = input.diskFreeCriticalBytes ?? DEFAULT_CRITICAL_FREE_BYTES;
863
+ const maxUsedPercent = input.diskMaxUsedPercent ?? DEFAULT_MAX_USED_PERCENT;
864
+ const hardMaxUsedPercent = input.diskHardMaxUsedPercent ?? DEFAULT_HARD_MAX_USED_PERCENT;
865
+ const stats = statfsSync2(path69);
866
+ const freeBytes = Number(stats.bavail) * Number(stats.bsize);
867
+ const totalBytes = Number(stats.blocks) * Number(stats.bsize);
868
+ const usedPercent = totalBytes > 0 ? (totalBytes - freeBytes) / totalBytes * 100 : 100;
869
+ const lowFree = freeBytes < warnBelowBytes;
870
+ const criticalFree = freeBytes < criticalBelowBytes;
871
+ const highUse = usedPercent > maxUsedPercent;
872
+ const hardHighUse = usedPercent > hardMaxUsedPercent;
873
+ const localOk = !lowFree && !criticalFree && !highUse && !hardHighUse;
874
+ const wslHost = input.skipWslHostCheck ? null : observeWslHostDisk(input.wslHost);
875
+ const ok = localOk && (wslHost ? wslHost.ok : true);
876
+ let reason = null;
877
+ if (!ok) {
878
+ reason = [
879
+ criticalFree ? `free space below critical ${criticalBelowBytes} bytes` : null,
880
+ lowFree ? `free space below warning ${warnBelowBytes} bytes` : null,
881
+ hardHighUse ? `used percent above hard cap ${hardMaxUsedPercent}%` : null,
882
+ highUse ? `used percent above cap ${maxUsedPercent}%` : null,
883
+ wslHost && !wslHost.ok ? wslHost.reason : null
884
+ ].filter(Boolean).join("; ");
885
+ }
742
886
  return {
743
- ...apiBaseUrl ? { apiBaseUrl: trimTrailingSlash(apiBaseUrl) } : {},
744
- ...agentOsId ? { agentOsId } : {},
745
- ...defaultRepo ? { defaultRepo } : {},
746
- ...harnessRoot ? { harnessRoot } : {},
747
- ...typeof args.agentOsSlug === "string" ? { agentOsSlug: args.agentOsSlug } : existing.agentOsSlug ? { agentOsSlug: existing.agentOsSlug } : {}
887
+ ok,
888
+ path: path69,
889
+ freeBytes,
890
+ totalBytes,
891
+ usedPercent,
892
+ warnBelowBytes,
893
+ criticalBelowBytes,
894
+ maxUsedPercent,
895
+ hardMaxUsedPercent,
896
+ reason,
897
+ wslHost
748
898
  };
749
899
  }
750
- function loadCredentialsFile() {
751
- if (!existsSync5(CREDENTIALS_FILE)) return {};
752
- try {
753
- return JSON.parse(readFileSync5(CREDENTIALS_FILE, "utf8"));
754
- } catch {
755
- return {};
900
+
901
+ // src/run-store.ts
902
+ import { existsSync as existsSync7, readdirSync as readdirSync2, statSync as statSync2 } from "node:fs";
903
+ import path7 from "node:path";
904
+
905
+ // src/paths.ts
906
+ import { existsSync as existsSync6 } from "node:fs";
907
+ import { homedir as homedir4 } from "node:os";
908
+ import path6 from "node:path";
909
+ var LEGACY_ROOT = path6.join(homedir4(), ".openclaw", "harness");
910
+ var HARNESS_LAYOUT_DIR_NAMES = /* @__PURE__ */ new Set(["runs", "worktrees"]);
911
+ function normalizeHarnessRoot(root) {
912
+ let resolved = path6.resolve(resolveUserPath(root.trim()));
913
+ while (HARNESS_LAYOUT_DIR_NAMES.has(path6.basename(resolved))) {
914
+ resolved = path6.dirname(resolved);
756
915
  }
916
+ return resolved;
757
917
  }
758
- function saveCredentialsFile(parsed) {
759
- mkdirSync2(CONFIG_DIR, { recursive: true });
760
- writeFileSync2(CREDENTIALS_FILE, `${JSON.stringify(parsed, null, 2)}
761
- `, { mode: 384 });
918
+ function resolveHarnessRoot() {
919
+ const env = process.env.KYNVER_HARNESS_ROOT || process.env.OPUS_HARNESS_ROOT;
920
+ if (env) return normalizeHarnessRoot(env);
921
+ const configured = loadUserConfig().harnessRoot?.trim();
922
+ if (configured) return normalizeHarnessRoot(configured);
923
+ const kynverRoot = path6.join(homedir4(), ".kynver", "harness");
924
+ if (existsSync6(kynverRoot)) return kynverRoot;
925
+ if (existsSync6(LEGACY_ROOT)) return LEGACY_ROOT;
926
+ return kynverRoot;
762
927
  }
763
- function loadApiKey() {
764
- if (process.env.KYNVER_API_KEY) return process.env.KYNVER_API_KEY;
765
- return loadCredentialsFile().apiKey;
928
+ function harnessRunsDir(harnessRoot) {
929
+ return path6.join(normalizeHarnessRoot(harnessRoot), "runs");
766
930
  }
767
- function saveApiKey(apiKey) {
768
- saveCredentialsFile({ ...loadCredentialsFile(), apiKey });
931
+ function harnessWorktreesDir(harnessRoot) {
932
+ return path6.join(normalizeHarnessRoot(harnessRoot), "worktrees");
769
933
  }
770
- function loadRunnerToken(agentOsId) {
771
- const envToken = process.env.KYNVER_RUNNER_TOKEN?.trim();
772
- if (envToken) return envToken;
773
- const creds = loadCredentialsFile();
774
- if (!creds.runnerToken) return void 0;
775
- if (agentOsId && creds.runnerTokenAgentOsId && creds.runnerTokenAgentOsId !== agentOsId) {
776
- return void 0;
777
- }
778
- return creds.runnerToken;
934
+ function getHarnessPaths() {
935
+ const harnessRoot = resolveHarnessRoot();
936
+ return {
937
+ harnessRoot,
938
+ runsDir: harnessRunsDir(harnessRoot),
939
+ worktreesDir: harnessWorktreesDir(harnessRoot)
940
+ };
779
941
  }
780
- function saveRunnerToken(agentOsId, token) {
781
- saveCredentialsFile({
782
- ...loadCredentialsFile(),
783
- runnerToken: token,
784
- runnerTokenAgentOsId: agentOsId
785
- });
942
+ function runDir(runsDir, id) {
943
+ return path6.join(runsDir, safeSlug(id));
786
944
  }
787
- function resolveBaseUrl(argsBaseUrl) {
788
- const baseUrl = resolveConfiguredBaseUrl(argsBaseUrl);
789
- if (!baseUrl) failConfig("requires --base-url, KYNVER_API_URL, KYNVER_CRON_FIRE_BASE_URL, or ~/.kynver/config.json apiBaseUrl");
790
- return baseUrl;
945
+
946
+ // src/run-store.ts
947
+ function getPaths() {
948
+ return getHarnessPaths();
791
949
  }
792
- function resolveConfiguredBaseUrl(argsBaseUrl) {
793
- const baseUrl = argsBaseUrl || process.env.KYNVER_API_URL || process.env.KYNVER_CRON_FIRE_BASE_URL || process.env.OPENCLAW_CRON_FIRE_BASE_URL || loadUserConfig().apiBaseUrl;
794
- return baseUrl ? trimTrailingSlash(String(baseUrl)) : void 0;
950
+ function loadRun(id) {
951
+ const { runsDir } = getPaths();
952
+ return readJson(path7.join(runDir(runsDir, safeSlug(id)), "run.json"));
795
953
  }
796
- function resolveConfiguredCallbackSecret(argsSecret, agentOsId) {
797
- const scoped = argsSecret || loadRunnerToken(agentOsId) || (agentOsId ? void 0 : loadRunnerToken(loadUserConfig().agentOsId));
798
- if (scoped) return String(scoped);
799
- const globalSecret = process.env.KYNVER_RUNTIME_SECRET || process.env.KYNVER_CRON_SECRET || process.env.OPENCLAW_CRON_SECRET;
800
- if (globalSecret) {
801
- console.warn(
802
- "[kynver] using deployment-level callback secret; run `kynver runner credential --agent-os-id <id>` for a scoped token"
803
- );
804
- return String(globalSecret);
805
- }
806
- return void 0;
954
+ function listRunRecords() {
955
+ const { runsDir } = getPaths();
956
+ return listRunRecordsAt(runsDir);
807
957
  }
808
- function resolveCallbackSecret(argsSecret, agentOsId) {
809
- const configured = resolveConfiguredCallbackSecret(argsSecret, agentOsId);
810
- if (configured) return configured;
811
- failConfig(
812
- "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"
813
- );
814
- }
815
- async function resolveCallbackSecretWithMint(argsSecret, agentOsId, opts) {
816
- const configured = resolveConfiguredCallbackSecret(argsSecret, agentOsId);
817
- if (configured) return configured;
818
- const apiKey = loadApiKey();
819
- const baseUrl = resolveConfiguredBaseUrl(opts?.baseUrl);
820
- if (apiKey && agentOsId && baseUrl) {
821
- try {
822
- const token = await fetchRunnerCredential(agentOsId, { baseUrl, apiKey });
823
- saveRunnerToken(agentOsId, token);
824
- return token;
825
- } catch (error) {
826
- failConfig(`runner credential mint failed: ${error.message}`);
827
- }
828
- }
829
- failConfig(
830
- "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"
831
- );
958
+ function listRunRecordsForHarnessRoot(harnessRoot) {
959
+ return listRunRecordsAt(harnessRunsDir(harnessRoot));
832
960
  }
833
- async function refreshRunnerToken(agentOsId, opts) {
834
- const apiKey = loadApiKey();
835
- const baseUrl = resolveConfiguredBaseUrl(opts?.baseUrl);
836
- if (!apiKey || !agentOsId || !baseUrl) return null;
961
+ function isRunDirectoryEntry(runDirPath) {
837
962
  try {
838
- const token = await fetchRunnerCredential(agentOsId, { baseUrl, apiKey });
839
- saveRunnerToken(agentOsId, token);
840
- return token;
963
+ return statSync2(runDirPath).isDirectory();
841
964
  } catch {
842
- return null;
843
- }
844
- }
845
- async function refreshRunnerTokenForAuthFailure(rejectedSecret, agentOsId, opts) {
846
- const apiKey = loadApiKey();
847
- const baseUrl = resolveConfiguredBaseUrl(opts?.baseUrl);
848
- if (!apiKey) return { ok: false, reason: "KYNVER_API_KEY is required to refresh a rejected runner token" };
849
- if (!agentOsId) return { ok: false, reason: "agentOsId is required to refresh a rejected runner token" };
850
- if (!baseUrl) return { ok: false, reason: "KYNVER_API_URL or --base-url is required to refresh a rejected runner token" };
851
- try {
852
- const token = await fetchRunnerCredential(agentOsId, { baseUrl, apiKey });
853
- if (token && token !== rejectedSecret) {
854
- saveRunnerToken(agentOsId, token);
855
- return { ok: true, token };
856
- }
857
- return { ok: false, reason: "runner credential refresh returned the rejected token" };
858
- } catch (error) {
859
- return { ok: false, reason: error.message };
965
+ return false;
860
966
  }
861
967
  }
862
- async function fetchRunnerCredential(agentOsId, opts) {
863
- const apiKey = opts?.apiKey || loadApiKey();
864
- if (!apiKey) throw new Error("API key required \u2014 run `kynver login` first");
865
- const base = resolveBaseUrl(opts?.baseUrl);
866
- const url = `${base}/api/agent-os/by-id/${encodeURIComponent(agentOsId)}/runner-credentials`;
867
- const res = await fetch(url, {
868
- method: "POST",
869
- headers: {
870
- "Content-Type": "application/json",
871
- Authorization: `Bearer ${apiKey}`
872
- },
873
- body: JSON.stringify({})
874
- });
875
- const text = await res.text();
876
- let parsed = null;
877
- try {
878
- parsed = JSON.parse(text);
879
- } catch {
880
- parsed = null;
881
- }
882
- if (!res.ok || !parsed?.token) {
883
- throw new Error(
884
- `runner credential mint failed (${res.status}): ${parsed?.error ?? text.slice(0, 200)}`
968
+ function listRunRecordsAt(runsDir) {
969
+ if (!existsSync7(runsDir)) return [];
970
+ const runs = [];
971
+ for (const entry of readdirSync2(runsDir, { withFileTypes: true })) {
972
+ if (entry.name === "runs") continue;
973
+ const runDir2 = path7.join(runsDir, entry.name);
974
+ if (!isRunDirectoryEntry(runDir2)) continue;
975
+ const run = readJson(
976
+ path7.join(runDir2, "run.json"),
977
+ void 0
885
978
  );
979
+ if (run?.id) runs.push(run);
886
980
  }
887
- return parsed.token;
981
+ return runs;
888
982
  }
889
- async function mintRunnerCredential(args) {
890
- const agentOsId = (args.agentOsId ? String(args.agentOsId) : loadUserConfig().agentOsId) || "";
891
- if (!agentOsId) failConfig("runner credential requires --agent-os-id or agentOsId in ~/.kynver/config.json");
892
- try {
893
- const token = await fetchRunnerCredential(agentOsId, {
894
- baseUrl: args.baseUrl ? String(args.baseUrl) : void 0
895
- });
896
- saveRunnerToken(agentOsId, token);
897
- console.log(
898
- JSON.stringify(
899
- {
900
- ok: true,
901
- agentOsId,
902
- credentialsPath: displayUserPath(CREDENTIALS_FILE),
903
- tokenPrefix: `${token.slice(0, 12)}\u2026`,
904
- note: "Scoped runner token saved; callbacks use X-Kynver-Runner-Token."
905
- },
906
- null,
907
- 2
908
- )
909
- );
910
- } catch (err) {
911
- console.error(err instanceof Error ? err.message : String(err));
912
- process.exit(1);
913
- }
983
+ function loadWorker(runId, name) {
984
+ const { runsDir } = getPaths();
985
+ return readJson(
986
+ path7.join(runDir(runsDir, safeSlug(runId)), "workers", safeSlug(name), "worker.json")
987
+ );
914
988
  }
915
- function failConfig(message) {
916
- console.error(message);
917
- process.exit(1);
989
+ function saveRun(run) {
990
+ const { runsDir } = getPaths();
991
+ writeJson(path7.join(runDir(runsDir, run.id), "run.json"), run);
918
992
  }
919
- function parseArgs(argv) {
920
- const args = {};
921
- for (let i = 0; i < argv.length; i++) {
922
- const item = argv[i];
923
- if (!item.startsWith("--")) continue;
924
- const key = item.slice(2).replace(/-([a-z])/g, (_, c) => c.toUpperCase());
925
- const next = argv[i + 1];
926
- if (!next || next.startsWith("--")) args[key] = true;
927
- else {
928
- args[key] = next;
929
- i++;
930
- }
931
- }
932
- return args;
993
+ function saveWorker(runId, worker) {
994
+ const { runsDir } = getPaths();
995
+ writeJson(path7.join(runDir(runsDir, runId), "workers", worker.name, "worker.json"), worker);
933
996
  }
934
- async function runSetup(args) {
935
- const existing = loadUserConfig();
936
- const maxWorkersRaw = typeof args.maxWorkers === "string" ? args.maxWorkers : typeof args.maxConcurrentWorkers === "string" ? args.maxConcurrentWorkers : void 0;
937
- const config = normalizeConfigPaths({
938
- ...existing,
939
- ...inferSetupFields(existing, args),
940
- ...maxWorkersRaw ? { maxConcurrentWorkers: Math.max(1, Math.floor(Number(maxWorkersRaw))) } : {},
941
- workerProvider: typeof args.provider === "string" ? args.provider : existing.workerProvider || "cursor"
942
- });
943
- saveUserConfig(config);
944
- let runnerCredentialNote;
945
- const apiKey = loadApiKey();
946
- const agentOsId = config.agentOsId;
947
- if (apiKey && agentOsId) {
948
- try {
949
- const token = await fetchRunnerCredential(agentOsId, {
950
- baseUrl: typeof args.apiBaseUrl === "string" ? args.apiBaseUrl : config.apiBaseUrl,
951
- apiKey
952
- });
953
- saveRunnerToken(agentOsId, token);
954
- runnerCredentialNote = "Scoped runner token minted and saved to ~/.kynver/credentials.";
955
- } catch {
956
- runnerCredentialNote = "Runner token not minted (server offline or master secret unset). Run `kynver runner credential` after deploy.";
957
- }
958
- }
959
- console.log(
960
- JSON.stringify(
961
- {
962
- ok: true,
963
- configPath: displayUserPath(CONFIG_FILE),
964
- config: presentUserConfig(config),
965
- note: runnerCredentialNote ?? "Set worker limit once with --max-workers N (or omit to auto-size from RAM). Run `kynver login` + `kynver runner credential` for scoped callbacks."
966
- },
967
- null,
968
- 2
969
- )
970
- );
997
+ function runDirectory(id) {
998
+ const { harnessRoot } = getPaths();
999
+ return runDirectoryAt(harnessRoot, id);
971
1000
  }
972
- async function runLogin(args) {
973
- const apiKey = typeof args.apiKey === "string" ? args.apiKey : process.env.KYNVER_API_KEY;
974
- if (!apiKey) failConfig("kynver login requires --api-key or KYNVER_API_KEY");
975
- saveApiKey(apiKey);
976
- console.log(JSON.stringify({ ok: true, credentialsPath: displayUserPath(CREDENTIALS_FILE) }, null, 2));
1001
+ function runDirectoryAt(harnessRoot, id) {
1002
+ return runDir(harnessRunsDir(harnessRoot), safeSlug(id));
977
1003
  }
978
1004
 
979
- // src/callback-headers.ts
980
- function buildHarnessCallbackHeaders(secret) {
981
- const trimmed = String(secret).trim();
982
- if (trimmed.startsWith("krc1.")) {
983
- return {
984
- "Content-Type": "application/json",
985
- "X-Kynver-Runner-Token": trimmed
986
- };
1005
+ // src/run-worker-index.ts
1006
+ import { existsSync as existsSync8, readdirSync as readdirSync3 } from "node:fs";
1007
+ import path8 from "node:path";
1008
+ function listRunWorkerNames(run) {
1009
+ const names = /* @__PURE__ */ new Set();
1010
+ for (const name of Object.keys(run.workers || {})) {
1011
+ names.add(safeSlug(name));
987
1012
  }
988
- return {
989
- "Content-Type": "application/json",
990
- // Canonical header. We keep sending the legacy `X-OpenClaw-Cron-Secret`
991
- // (and `X-Kynver-Runtime-Secret`) so an un-upgraded Kynver server that
992
- // only reads the old header still authenticates this runner during the
993
- // rename compat window.
994
- "X-Kynver-Cron-Secret": trimmed,
995
- "X-OpenClaw-Cron-Secret": trimmed,
996
- "X-Kynver-Runtime-Secret": trimmed
997
- };
998
- }
999
-
1000
- // src/callbacks.ts
1001
- function callbackTimeoutMs() {
1002
- const parsed = Number(process.env.KYNVER_CALLBACK_TIMEOUT_MS);
1003
- if (Number.isFinite(parsed) && parsed > 0) return Math.floor(parsed);
1004
- return 3e4;
1005
- }
1006
- async function withTimeout(fn) {
1007
- const controller = new AbortController();
1008
- const timeout = setTimeout(() => controller.abort(), callbackTimeoutMs());
1009
- try {
1010
- return await fn(controller.signal);
1011
- } finally {
1012
- clearTimeout(timeout);
1013
+ const workersDir = path8.join(runDirectory(run.id), "workers");
1014
+ if (!existsSync8(workersDir)) return [...names];
1015
+ for (const entry of readdirSync3(workersDir, { withFileTypes: true })) {
1016
+ if (!entry.isDirectory()) continue;
1017
+ names.add(safeSlug(entry.name));
1013
1018
  }
1019
+ return [...names];
1014
1020
  }
1015
- async function postJson(url, secret, body) {
1016
- const res = await withTimeout(
1017
- (signal) => fetch(url, {
1018
- method: "POST",
1019
- headers: buildHarnessCallbackHeaders(secret),
1020
- body: JSON.stringify(body),
1021
- signal
1022
- })
1023
- );
1024
- let response = null;
1021
+
1022
+ // src/heartbeat.ts
1023
+ import { existsSync as existsSync9, readFileSync as readFileSync7 } from "node:fs";
1024
+
1025
+ // src/heartbeat-final-result.ts
1026
+ function tryParseJsonObject(text) {
1027
+ const trimmed = text.trim();
1028
+ if (!trimmed.startsWith("{")) return null;
1025
1029
  try {
1026
- response = await res.json();
1030
+ const parsed = JSON.parse(trimmed);
1031
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
1032
+ return parsed;
1033
+ }
1027
1034
  } catch {
1028
- response = null;
1035
+ return null;
1029
1036
  }
1030
- return { ok: res.ok, status: res.status, response };
1031
- }
1032
- async function postJsonWithCredentialRefresh(url, secret, body, opts) {
1033
- const first = await postJson(url, secret, body);
1034
- if (first.ok || first.status !== 401) return first;
1035
- const refreshed = await refreshRunnerTokenForAuthFailure(secret, opts.agentOsId, { baseUrl: opts.baseUrl });
1036
- if (!refreshed.ok) return { ...first, authRefreshFailure: refreshed.reason };
1037
- const retry = await postJson(url, refreshed.token, body);
1038
- return { ...retry, refreshedAuth: true };
1037
+ return null;
1039
1038
  }
1040
- async function getJson(url, secret) {
1041
- const res = await withTimeout(
1042
- (signal) => fetch(url, {
1043
- method: "GET",
1044
- headers: buildHarnessCallbackHeaders(secret),
1045
- signal
1046
- })
1047
- );
1048
- let response = null;
1049
- try {
1050
- response = await res.json();
1051
- } catch {
1052
- response = null;
1039
+ function embeddedRecordFromProse(text) {
1040
+ const trimmed = text.trim();
1041
+ if (!trimmed) return null;
1042
+ const direct = tryParseJsonObject(trimmed);
1043
+ if (direct) return direct;
1044
+ const candidates = [];
1045
+ const fenceRe = /```(?:json)?\s*([\s\S]*?)```/gi;
1046
+ let fenceMatch;
1047
+ while ((fenceMatch = fenceRe.exec(trimmed)) !== null) {
1048
+ const fromFence = tryParseJsonObject(fenceMatch[1] ?? "");
1049
+ if (fromFence) candidates.push(fromFence);
1053
1050
  }
1054
- return { ok: res.ok, status: res.status, response };
1055
- }
1056
-
1057
- // src/dispatch-lane-normalization.ts
1058
- function trimLower(value) {
1059
- return (value ?? "").trim().toLowerCase();
1060
- }
1061
- function roleLaneToDispatchLane(roleLane) {
1062
- switch (trimLower(roleLane)) {
1063
- case "implementer":
1064
- case "repair_implementer":
1065
- case "plan_author":
1066
- case "runtime_verifier":
1067
- return "implementation";
1068
- case "plan_reviewer":
1069
- case "report_reviewer":
1070
- case "deep_reviewer":
1071
- return "review";
1072
- default:
1073
- return null;
1051
+ const firstBrace = trimmed.indexOf("{");
1052
+ const lastBrace = trimmed.lastIndexOf("}");
1053
+ if (firstBrace >= 0 && lastBrace > firstBrace) {
1054
+ const slice = tryParseJsonObject(trimmed.slice(firstBrace, lastBrace + 1));
1055
+ if (slice) candidates.push(slice);
1074
1056
  }
1057
+ return candidates.length > 0 ? candidates[candidates.length - 1] : null;
1075
1058
  }
1076
- function normalizeDispatchNextLaneFilter(raw) {
1077
- const key = trimLower(raw);
1078
- if (!key) return void 0;
1079
- if (key === "implementation" || key === "review" || key === "landing" || key === "any") {
1080
- return key;
1059
+ function terminalFinalResultFromHeartbeatRow(row) {
1060
+ const explicit = row.finalResult ?? row.final_result;
1061
+ if (explicit !== void 0 && explicit !== null) {
1062
+ if (typeof explicit === "string") {
1063
+ const embedded2 = embeddedRecordFromProse(explicit);
1064
+ return embedded2 ?? (explicit.trim() || null);
1065
+ }
1066
+ return explicit;
1081
1067
  }
1082
- const mapped = roleLaneToDispatchLane(key);
1083
- if (mapped) return mapped;
1084
- if (key === "implement" || key === "repair" || key === "coding") return "implementation";
1085
- if (key === "land" || key === "merge") return "landing";
1086
- return void 0;
1087
- }
1088
- function resolveDispatchNextLaneFilter(raw) {
1089
- return normalizeDispatchNextLaneFilter(raw) ?? "any";
1068
+ const summary = typeof row.summary === "string" ? row.summary.trim() : "";
1069
+ if (!summary) return null;
1070
+ const embedded = embeddedRecordFromProse(summary);
1071
+ if (embedded) return embedded;
1072
+ return summary;
1090
1073
  }
1091
1074
 
1092
- // src/disk-gate.ts
1093
- import { statfsSync as statfsSync2 } from "node:fs";
1094
-
1095
- // src/wsl-host.ts
1096
- import { existsSync as existsSync6, readFileSync as readFileSync6, statfsSync } from "node:fs";
1097
- var DEFAULT_WSL_HOST_WARN_FREE_BYTES = 25 * 1024 * 1024 * 1024;
1098
- var DEFAULT_WSL_HOST_CRITICAL_FREE_BYTES = 12 * 1024 * 1024 * 1024;
1099
- var DEFAULT_WSL_HOST_MOUNT = "/mnt/c";
1100
- function isWslHost() {
1101
- if (process.platform !== "linux") return false;
1102
- for (const probe of ["/proc/sys/kernel/osrelease", "/proc/version"]) {
1103
- try {
1104
- if (!existsSync6(probe)) continue;
1105
- const text = readFileSync6(probe, "utf8");
1106
- if (/microsoft|wsl/i.test(text)) return true;
1107
- } catch {
1108
- }
1109
- }
1110
- return false;
1075
+ // src/heartbeat.ts
1076
+ var HEARTBEAT_FUTURE_SKEW_MS = 6e4;
1077
+ function isTerminalHeartbeatPhase(phase) {
1078
+ return phase === "complete";
1111
1079
  }
1112
- function observeWslHostDisk(options = {}) {
1113
- const wsl = options.forceWsl === void 0 ? isWslHost() : options.forceWsl;
1114
- if (!wsl) return null;
1115
- const path67 = options.wslHostMount?.trim() || process.env.KYNVER_WSL_HOST_MOUNT?.trim() || DEFAULT_WSL_HOST_MOUNT;
1116
- const warnBelowBytes = options.wslHostFreeWarnBytes ?? DEFAULT_WSL_HOST_WARN_FREE_BYTES;
1117
- const criticalBelowBytes = options.wslHostFreeCriticalBytes ?? DEFAULT_WSL_HOST_CRITICAL_FREE_BYTES;
1118
- const statfs = options.statfs ?? statfsSync;
1119
- let stats;
1120
- try {
1121
- stats = statfs(path67);
1122
- } catch (error) {
1123
- return {
1124
- ok: false,
1125
- path: path67,
1126
- freeBytes: 0,
1127
- totalBytes: 0,
1128
- usedPercent: 100,
1129
- warnBelowBytes,
1130
- criticalBelowBytes,
1131
- reason: `Windows host disk probe failed at ${path67}: ${error.message}`,
1132
- probeError: error.message
1133
- };
1134
- }
1135
- const freeBytes = Number(stats.bavail) * Number(stats.bsize);
1136
- const totalBytes = Number(stats.blocks) * Number(stats.bsize);
1137
- const usedPercent = totalBytes > 0 ? (totalBytes - freeBytes) / totalBytes * 100 : 100;
1138
- const lowFree = freeBytes < warnBelowBytes;
1139
- const criticalFree = freeBytes < criticalBelowBytes;
1140
- const ok = !lowFree && !criticalFree;
1141
- const freeGiB = (freeBytes / (1024 * 1024 * 1024)).toFixed(1);
1142
- let reason = null;
1143
- if (!ok) {
1144
- const tag = criticalFree ? "critical" : "warning";
1145
- reason = `Windows host disk ${path67} at ${tag}: ${freeGiB} GiB free (<${(criticalFree ? criticalBelowBytes : warnBelowBytes) / 1024 / 1024 / 1024} GiB); WSL VHDX cannot grow safely. ${summarizeWslRecoverySteps()}`;
1080
+ function terminalFinalResultFromHeartbeat(heartbeat) {
1081
+ if (!isTerminalHeartbeatPhase(heartbeat.lastHeartbeatPhase)) return null;
1082
+ if (heartbeat.terminalFinalResult !== void 0 && heartbeat.terminalFinalResult !== null) {
1083
+ return heartbeat.terminalFinalResult;
1146
1084
  }
1147
- return {
1148
- ok,
1149
- path: path67,
1150
- freeBytes,
1151
- totalBytes,
1152
- usedPercent,
1153
- warnBelowBytes,
1154
- criticalBelowBytes,
1155
- reason,
1156
- probeError: null
1157
- };
1158
- }
1159
- function summarizeWslRecoverySteps() {
1160
- 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.";
1085
+ const summary = heartbeat.lastHeartbeatSummary?.trim();
1086
+ return summary || "completed";
1161
1087
  }
1162
-
1163
- // src/disk-gate.ts
1164
- var DEFAULT_WARN_FREE_BYTES = 30 * 1024 * 1024 * 1024;
1165
- var DEFAULT_CRITICAL_FREE_BYTES = 15 * 1024 * 1024 * 1024;
1166
- var DEFAULT_MAX_USED_PERCENT = 80;
1167
- var DEFAULT_HARD_MAX_USED_PERCENT = 90;
1168
- function observeRunnerDiskGate(input = {}) {
1169
- const path67 = input.diskPath?.trim() || "/";
1170
- const warnBelowBytes = input.diskFreeWarnBytes ?? DEFAULT_WARN_FREE_BYTES;
1171
- const criticalBelowBytes = input.diskFreeCriticalBytes ?? DEFAULT_CRITICAL_FREE_BYTES;
1172
- const maxUsedPercent = input.diskMaxUsedPercent ?? DEFAULT_MAX_USED_PERCENT;
1173
- const hardMaxUsedPercent = input.diskHardMaxUsedPercent ?? DEFAULT_HARD_MAX_USED_PERCENT;
1174
- const stats = statfsSync2(path67);
1175
- const freeBytes = Number(stats.bavail) * Number(stats.bsize);
1176
- const totalBytes = Number(stats.blocks) * Number(stats.bsize);
1177
- const usedPercent = totalBytes > 0 ? (totalBytes - freeBytes) / totalBytes * 100 : 100;
1178
- const lowFree = freeBytes < warnBelowBytes;
1179
- const criticalFree = freeBytes < criticalBelowBytes;
1180
- const highUse = usedPercent > maxUsedPercent;
1181
- const hardHighUse = usedPercent > hardMaxUsedPercent;
1182
- const localOk = !lowFree && !criticalFree && !highUse && !hardHighUse;
1183
- const wslHost = input.skipWslHostCheck ? null : observeWslHostDisk(input.wslHost);
1184
- const ok = localOk && (wslHost ? wslHost.ok : true);
1185
- let reason = null;
1186
- if (!ok) {
1187
- reason = [
1188
- criticalFree ? `free space below critical ${criticalBelowBytes} bytes` : null,
1189
- lowFree ? `free space below warning ${warnBelowBytes} bytes` : null,
1190
- hardHighUse ? `used percent above hard cap ${hardMaxUsedPercent}%` : null,
1191
- highUse ? `used percent above cap ${maxUsedPercent}%` : null,
1192
- wslHost && !wslHost.ok ? wslHost.reason : null
1193
- ].filter(Boolean).join("; ");
1194
- }
1195
- return {
1196
- ok,
1197
- path: path67,
1198
- freeBytes,
1199
- totalBytes,
1200
- usedPercent,
1201
- warnBelowBytes,
1202
- criticalBelowBytes,
1203
- maxUsedPercent,
1204
- hardMaxUsedPercent,
1205
- reason,
1206
- wslHost
1088
+ function parseHeartbeat(file) {
1089
+ const result = {
1090
+ heartbeatCount: 0,
1091
+ lastHeartbeatAt: null,
1092
+ lastHeartbeatPhase: null,
1093
+ lastHeartbeatSummary: null,
1094
+ terminalFinalResult: null,
1095
+ heartbeatBlocker: null,
1096
+ timestampAnomalies: [],
1097
+ lastBoxResourceSnapshot: null,
1098
+ lastPrEvidence: []
1207
1099
  };
1208
- }
1209
-
1210
- // src/resource-gate.ts
1211
- import os2 from "node:os";
1212
-
1213
- // src/bounded-build/meminfo.ts
1214
- import { readFileSync as readFileSync7 } from "node:fs";
1215
- import os from "node:os";
1216
- function readMemAvailableBytes(meminfoText) {
1217
- if (meminfoText !== void 0) {
1218
- const match = meminfoText.match(/^MemAvailable:\s+(\d+)\s*kB/m);
1219
- if (match) return Number(match[1]) * 1024;
1220
- return os.freemem();
1221
- }
1222
- if (process.platform === "linux") {
1223
- try {
1224
- const meminfo = readFileSync7("/proc/meminfo", "utf8");
1225
- const match = meminfo.match(/^MemAvailable:\s+(\d+)\s*kB/m);
1226
- if (match) return Number(match[1]) * 1024;
1227
- } catch {
1100
+ if (!existsSync9(file)) return result;
1101
+ const maxFutureMs = Date.now() + HEARTBEAT_FUTURE_SKEW_MS;
1102
+ const clampedTo = new Date(maxFutureMs).toISOString();
1103
+ const lines = readFileSync7(file, "utf8").split("\n").filter(Boolean);
1104
+ for (const line of lines) {
1105
+ const entry = safeJson(line);
1106
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) continue;
1107
+ const row = entry;
1108
+ result.heartbeatCount++;
1109
+ if (row.ts) {
1110
+ const ts = String(row.ts);
1111
+ const tsMs = Date.parse(ts);
1112
+ if (Number.isFinite(tsMs) && tsMs > maxFutureMs) {
1113
+ result.timestampAnomalies.push({
1114
+ kind: "future_heartbeat_timestamp",
1115
+ observedAt: ts,
1116
+ clampedTo
1117
+ });
1118
+ } else {
1119
+ result.lastHeartbeatAt = ts;
1120
+ }
1121
+ }
1122
+ if (row.phase !== void 0 && row.phase !== null) result.lastHeartbeatPhase = String(row.phase);
1123
+ if (row.summary !== void 0 && row.summary !== null) result.lastHeartbeatSummary = String(row.summary);
1124
+ if (isTerminalHeartbeatPhase(result.lastHeartbeatPhase)) {
1125
+ result.terminalFinalResult = terminalFinalResultFromHeartbeatRow(row);
1126
+ }
1127
+ result.heartbeatBlocker = row.blocker ? String(row.blocker) : null;
1128
+ if (row.boxResourceSnapshot && typeof row.boxResourceSnapshot === "object" && !Array.isArray(row.boxResourceSnapshot)) {
1129
+ result.lastBoxResourceSnapshot = row.boxResourceSnapshot;
1130
+ }
1131
+ if (Array.isArray(row.prEvidence)) {
1132
+ result.lastPrEvidence = row.prEvidence.filter(
1133
+ (entry2) => !!entry2 && typeof entry2 === "object" && typeof entry2.prUrl === "string"
1134
+ );
1228
1135
  }
1229
1136
  }
1230
- return os.freemem();
1137
+ return result;
1231
1138
  }
1232
1139
 
1233
- // src/resource-gate.ts
1234
- import path10 from "node:path";
1235
-
1236
- // src/run-store.ts
1237
- import { existsSync as existsSync8, readdirSync as readdirSync2, statSync as statSync2 } from "node:fs";
1238
- import path8 from "node:path";
1239
-
1240
- // src/paths.ts
1241
- import { existsSync as existsSync7 } from "node:fs";
1242
- import { homedir as homedir5 } from "node:os";
1243
- import path7 from "node:path";
1244
- var LEGACY_ROOT = path7.join(homedir5(), ".openclaw", "harness");
1245
- var HARNESS_LAYOUT_DIR_NAMES = /* @__PURE__ */ new Set(["runs", "worktrees"]);
1246
- function normalizeHarnessRoot(root) {
1247
- let resolved = path7.resolve(resolveUserPath(root.trim()));
1248
- while (HARNESS_LAYOUT_DIR_NAMES.has(path7.basename(resolved))) {
1249
- resolved = path7.dirname(resolved);
1250
- }
1251
- return resolved;
1252
- }
1253
- function resolveHarnessRoot() {
1254
- const env = process.env.KYNVER_HARNESS_ROOT || process.env.OPUS_HARNESS_ROOT;
1255
- if (env) return normalizeHarnessRoot(env);
1256
- const configured = loadUserConfig().harnessRoot?.trim();
1257
- if (configured) return normalizeHarnessRoot(configured);
1258
- const kynverRoot = path7.join(homedir5(), ".kynver", "harness");
1259
- if (existsSync7(kynverRoot)) return kynverRoot;
1260
- if (existsSync7(LEGACY_ROOT)) return LEGACY_ROOT;
1261
- return kynverRoot;
1262
- }
1263
- function harnessRunsDir(harnessRoot) {
1264
- return path7.join(normalizeHarnessRoot(harnessRoot), "runs");
1265
- }
1266
- function harnessWorktreesDir(harnessRoot) {
1267
- return path7.join(normalizeHarnessRoot(harnessRoot), "worktrees");
1268
- }
1269
- function getHarnessPaths() {
1270
- const harnessRoot = resolveHarnessRoot();
1271
- return {
1272
- harnessRoot,
1273
- runsDir: harnessRunsDir(harnessRoot),
1274
- worktreesDir: harnessWorktreesDir(harnessRoot)
1275
- };
1276
- }
1277
- function runDir(runsDir, id) {
1278
- return path7.join(runsDir, safeSlug(id));
1279
- }
1280
-
1281
- // src/run-store.ts
1282
- function getPaths() {
1283
- return getHarnessPaths();
1284
- }
1285
- function loadRun(id) {
1286
- const { runsDir } = getPaths();
1287
- return readJson(path8.join(runDir(runsDir, safeSlug(id)), "run.json"));
1288
- }
1289
- function listRunRecords() {
1290
- const { runsDir } = getPaths();
1291
- return listRunRecordsAt(runsDir);
1292
- }
1293
- function listRunRecordsForHarnessRoot(harnessRoot) {
1294
- return listRunRecordsAt(harnessRunsDir(harnessRoot));
1295
- }
1296
- function isRunDirectoryEntry(runDirPath) {
1297
- try {
1298
- return statSync2(runDirPath).isDirectory();
1299
- } catch {
1300
- return false;
1301
- }
1302
- }
1303
- function listRunRecordsAt(runsDir) {
1304
- if (!existsSync8(runsDir)) return [];
1305
- const runs = [];
1306
- for (const entry of readdirSync2(runsDir, { withFileTypes: true })) {
1307
- if (entry.name === "runs") continue;
1308
- const runDir2 = path8.join(runsDir, entry.name);
1309
- if (!isRunDirectoryEntry(runDir2)) continue;
1310
- const run = readJson(
1311
- path8.join(runDir2, "run.json"),
1312
- void 0
1313
- );
1314
- if (run?.id) runs.push(run);
1315
- }
1316
- return runs;
1317
- }
1318
- function loadWorker(runId, name) {
1319
- const { runsDir } = getPaths();
1320
- return readJson(
1321
- path8.join(runDir(runsDir, safeSlug(runId)), "workers", safeSlug(name), "worker.json")
1322
- );
1323
- }
1324
- function saveRun(run) {
1325
- const { runsDir } = getPaths();
1326
- writeJson(path8.join(runDir(runsDir, run.id), "run.json"), run);
1327
- }
1328
- function saveWorker(runId, worker) {
1329
- const { runsDir } = getPaths();
1330
- writeJson(path8.join(runDir(runsDir, runId), "workers", worker.name, "worker.json"), worker);
1331
- }
1332
- function runDirectory(id) {
1333
- const { harnessRoot } = getPaths();
1334
- return runDirectoryAt(harnessRoot, id);
1335
- }
1336
- function runDirectoryAt(harnessRoot, id) {
1337
- return runDir(harnessRunsDir(harnessRoot), safeSlug(id));
1338
- }
1339
-
1340
- // src/run-worker-index.ts
1341
- import { existsSync as existsSync9, readdirSync as readdirSync3 } from "node:fs";
1342
- import path9 from "node:path";
1343
- function listRunWorkerNames(run) {
1344
- const names = /* @__PURE__ */ new Set();
1345
- for (const name of Object.keys(run.workers || {})) {
1346
- names.add(safeSlug(name));
1347
- }
1348
- const workersDir = path9.join(runDirectory(run.id), "workers");
1349
- if (!existsSync9(workersDir)) return [...names];
1350
- for (const entry of readdirSync3(workersDir, { withFileTypes: true })) {
1351
- if (!entry.isDirectory()) continue;
1352
- names.add(safeSlug(entry.name));
1353
- }
1354
- return [...names];
1355
- }
1356
-
1357
- // src/heartbeat.ts
1358
- import { existsSync as existsSync10, readFileSync as readFileSync8 } from "node:fs";
1359
-
1360
- // src/heartbeat-final-result.ts
1361
- function tryParseJsonObject(text) {
1362
- const trimmed = text.trim();
1363
- if (!trimmed.startsWith("{")) return null;
1364
- try {
1365
- const parsed = JSON.parse(trimmed);
1366
- if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
1367
- return parsed;
1368
- }
1369
- } catch {
1370
- return null;
1371
- }
1372
- return null;
1373
- }
1374
- function embeddedRecordFromProse(text) {
1375
- const trimmed = text.trim();
1376
- if (!trimmed) return null;
1377
- const direct = tryParseJsonObject(trimmed);
1378
- if (direct) return direct;
1379
- const candidates = [];
1380
- const fenceRe = /```(?:json)?\s*([\s\S]*?)```/gi;
1381
- let fenceMatch;
1382
- while ((fenceMatch = fenceRe.exec(trimmed)) !== null) {
1383
- const fromFence = tryParseJsonObject(fenceMatch[1] ?? "");
1384
- if (fromFence) candidates.push(fromFence);
1385
- }
1386
- const firstBrace = trimmed.indexOf("{");
1387
- const lastBrace = trimmed.lastIndexOf("}");
1388
- if (firstBrace >= 0 && lastBrace > firstBrace) {
1389
- const slice = tryParseJsonObject(trimmed.slice(firstBrace, lastBrace + 1));
1390
- if (slice) candidates.push(slice);
1391
- }
1392
- return candidates.length > 0 ? candidates[candidates.length - 1] : null;
1393
- }
1394
- function terminalFinalResultFromHeartbeatRow(row) {
1395
- const explicit = row.finalResult ?? row.final_result;
1396
- if (explicit !== void 0 && explicit !== null) {
1397
- if (typeof explicit === "string") {
1398
- const embedded2 = embeddedRecordFromProse(explicit);
1399
- return embedded2 ?? (explicit.trim() || null);
1400
- }
1401
- return explicit;
1402
- }
1403
- const summary = typeof row.summary === "string" ? row.summary.trim() : "";
1404
- if (!summary) return null;
1405
- const embedded = embeddedRecordFromProse(summary);
1406
- if (embedded) return embedded;
1407
- return summary;
1408
- }
1409
-
1410
- // src/heartbeat.ts
1411
- var HEARTBEAT_FUTURE_SKEW_MS = 6e4;
1412
- function isTerminalHeartbeatPhase(phase) {
1413
- return phase === "complete";
1414
- }
1415
- function terminalFinalResultFromHeartbeat(heartbeat) {
1416
- if (!isTerminalHeartbeatPhase(heartbeat.lastHeartbeatPhase)) return null;
1417
- if (heartbeat.terminalFinalResult !== void 0 && heartbeat.terminalFinalResult !== null) {
1418
- return heartbeat.terminalFinalResult;
1419
- }
1420
- const summary = heartbeat.lastHeartbeatSummary?.trim();
1421
- return summary || "completed";
1422
- }
1423
- function parseHeartbeat(file) {
1424
- const result = {
1425
- heartbeatCount: 0,
1426
- lastHeartbeatAt: null,
1427
- lastHeartbeatPhase: null,
1428
- lastHeartbeatSummary: null,
1429
- terminalFinalResult: null,
1430
- heartbeatBlocker: null,
1431
- timestampAnomalies: [],
1432
- lastBoxResourceSnapshot: null,
1433
- lastPrEvidence: []
1434
- };
1435
- if (!existsSync10(file)) return result;
1436
- const maxFutureMs = Date.now() + HEARTBEAT_FUTURE_SKEW_MS;
1437
- const clampedTo = new Date(maxFutureMs).toISOString();
1438
- const lines = readFileSync8(file, "utf8").split("\n").filter(Boolean);
1439
- for (const line of lines) {
1440
- const entry = safeJson(line);
1441
- if (!entry || typeof entry !== "object" || Array.isArray(entry)) continue;
1442
- const row = entry;
1443
- result.heartbeatCount++;
1444
- if (row.ts) {
1445
- const ts = String(row.ts);
1446
- const tsMs = Date.parse(ts);
1447
- if (Number.isFinite(tsMs) && tsMs > maxFutureMs) {
1448
- result.timestampAnomalies.push({
1449
- kind: "future_heartbeat_timestamp",
1450
- observedAt: ts,
1451
- clampedTo
1452
- });
1453
- } else {
1454
- result.lastHeartbeatAt = ts;
1455
- }
1456
- }
1457
- if (row.phase !== void 0 && row.phase !== null) result.lastHeartbeatPhase = String(row.phase);
1458
- if (row.summary !== void 0 && row.summary !== null) result.lastHeartbeatSummary = String(row.summary);
1459
- if (isTerminalHeartbeatPhase(result.lastHeartbeatPhase)) {
1460
- result.terminalFinalResult = terminalFinalResultFromHeartbeatRow(row);
1461
- }
1462
- result.heartbeatBlocker = row.blocker ? String(row.blocker) : null;
1463
- if (row.boxResourceSnapshot && typeof row.boxResourceSnapshot === "object" && !Array.isArray(row.boxResourceSnapshot)) {
1464
- result.lastBoxResourceSnapshot = row.boxResourceSnapshot;
1465
- }
1466
- if (Array.isArray(row.prEvidence)) {
1467
- result.lastPrEvidence = row.prEvidence.filter(
1468
- (entry2) => !!entry2 && typeof entry2 === "object" && typeof entry2.prUrl === "string"
1469
- );
1470
- }
1471
- }
1472
- return result;
1473
- }
1474
-
1475
- // src/stream.ts
1476
- import { existsSync as existsSync11, readFileSync as readFileSync9 } from "node:fs";
1140
+ // src/stream.ts
1141
+ import { existsSync as existsSync10, readFileSync as readFileSync8 } from "node:fs";
1477
1142
 
1478
1143
  // src/repo-search.ts
1479
1144
  var RG_BINARIES = /* @__PURE__ */ new Set(["rg", "ripgrep", "grep"]);
@@ -1957,8 +1622,8 @@ function parseHarnessStream(file) {
1957
1622
  error: null,
1958
1623
  lastShellOutcome: null
1959
1624
  };
1960
- if (!existsSync11(file)) return result;
1961
- const lines = readFileSync9(file, "utf8").split("\n").filter(Boolean);
1625
+ if (!existsSync10(file)) return result;
1626
+ const lines = readFileSync8(file, "utf8").split("\n").filter(Boolean);
1962
1627
  for (const line of lines) {
1963
1628
  const event = safeJson(line);
1964
1629
  if (!event) continue;
@@ -2414,302 +2079,823 @@ function assessWorkerLandingContract(input) {
2414
2079
  missing.push(key);
2415
2080
  }
2416
2081
  }
2417
- if (missing.length > 0) {
2418
- return {
2419
- blocked: true,
2420
- reason: missing.every((u) => byUrl.has(u)) ? "incomplete_target_pr_landing" : "missing_target_pr_reconciliation",
2421
- detail: `Target PR reconciliation incomplete: ${missing.join(", ")}`
2422
- };
2423
- }
2424
- return { blocked: false };
2082
+ if (missing.length > 0) {
2083
+ return {
2084
+ blocked: true,
2085
+ reason: missing.every((u) => byUrl.has(u)) ? "incomplete_target_pr_landing" : "missing_target_pr_reconciliation",
2086
+ detail: `Target PR reconciliation incomplete: ${missing.join(", ")}`
2087
+ };
2088
+ }
2089
+ return { blocked: false };
2090
+ }
2091
+ function landingContractAttentionReason(verdict) {
2092
+ if (!verdict.blocked) return void 0;
2093
+ return verdict.detail ?? verdict.reason;
2094
+ }
2095
+
2096
+ // src/status.ts
2097
+ var NO_START_MS = 18e4;
2098
+ var STALE_MS = 6e5;
2099
+ function computeAttention(input) {
2100
+ const now = Date.now();
2101
+ if (input.completionBlocker) {
2102
+ return { state: "blocked", reason: input.completionBlocker };
2103
+ }
2104
+ if (input.finalResult) {
2105
+ const landingSnapshot = {
2106
+ finalResult: input.finalResult,
2107
+ changedFiles: input.changedFiles ?? [],
2108
+ gitAncestry: input.gitAncestry ?? null,
2109
+ prUrl: input.prUrl ?? null
2110
+ };
2111
+ const landing = assessWorkerLanding(landingSnapshot);
2112
+ if (landing.blocked) {
2113
+ const detail = landingAttentionReason(landing);
2114
+ return {
2115
+ state: "needs_attention",
2116
+ reason: landing.reason ? `landing blocked (${landing.reason}): ${detail}` : `landing blocked: ${detail}`
2117
+ };
2118
+ }
2119
+ if (input.landingContract) {
2120
+ const contractVerdict = assessWorkerLandingContract({
2121
+ contract: input.landingContract,
2122
+ snapshot: landingSnapshot,
2123
+ finalResult: input.finalResult
2124
+ });
2125
+ const contractDetail = landingContractAttentionReason(contractVerdict);
2126
+ if (contractDetail) {
2127
+ return {
2128
+ state: "needs_attention",
2129
+ reason: contractVerdict.reason ? `landing contract (${contractVerdict.reason}): ${contractDetail}` : `landing contract: ${contractDetail}`
2130
+ };
2131
+ }
2132
+ }
2133
+ return { state: "done", reason: "final result recorded" };
2134
+ }
2135
+ if (!input.alive) {
2136
+ const classified = classifyExitFailure(input.error);
2137
+ if (classified) return { state: "blocked", reason: classified.reason };
2138
+ const salvage = assessExitedWorkerSalvage({
2139
+ alive: false,
2140
+ finalResult: null,
2141
+ changedFiles: input.changedFiles,
2142
+ gitAncestry: input.gitAncestry
2143
+ });
2144
+ if (salvage?.salvageable) {
2145
+ const tail2 = input.error?.trim();
2146
+ return {
2147
+ state: "needs_attention",
2148
+ reason: tail2 ? `${salvage.attentionReason} (${tail2})` : salvage.attentionReason
2149
+ };
2150
+ }
2151
+ const tail = input.error?.trim();
2152
+ return {
2153
+ state: "needs_attention",
2154
+ reason: tail ? `process exited without a final result: ${tail}` : salvage?.attentionReason ?? "process exited without a final result"
2155
+ };
2156
+ }
2157
+ if (input.heartbeatBlocker) {
2158
+ return { state: "blocked", reason: `worker heartbeat reported blocker: ${input.heartbeatBlocker}` };
2159
+ }
2160
+ const startMs = input.startedAt ? Date.parse(input.startedAt) : NaN;
2161
+ if (!input.firstEventAt && input.stdoutBytes === 0 && input.heartbeatBytes === 0 && Number.isFinite(startMs) && now - startMs > NO_START_MS) {
2162
+ return { state: "needs_attention", reason: `no first stream event ${secsAgo(startMs)}s after start` };
2163
+ }
2164
+ const actMs = input.lastActivityAt ? Date.parse(input.lastActivityAt) : NaN;
2165
+ if (Number.isFinite(actMs) && now - actMs > STALE_MS) {
2166
+ return { state: "stale", reason: `no log/event/heartbeat activity for ${secsAgo(actMs)}s` };
2167
+ }
2168
+ return { state: "ok", reason: "recent activity" };
2169
+ }
2170
+ function resolveFinalResult(worker, parsedFinalResult, heartbeat) {
2171
+ if (parsedFinalResult) return parsedFinalResult;
2172
+ const ackSnapshot = worker.completionSnapshot?.finalResult;
2173
+ if (ackSnapshot !== void 0 && ackSnapshot !== null) return ackSnapshot;
2174
+ return terminalFinalResultFromHeartbeat(heartbeat);
2175
+ }
2176
+ function computeWorkerStatus(worker, options = {}) {
2177
+ const parsed = parseHarnessStream(worker.stdoutPath);
2178
+ const heartbeat = parseHeartbeat(worker.heartbeatPath);
2179
+ const completionAcknowledged = typeof worker.completionReportedAt === "string" && worker.completionReportedAt.trim().length > 0;
2180
+ const finalResult = resolveFinalResult(worker, parsed.finalResult, heartbeat);
2181
+ const alive = completionAcknowledged ? false : isPidAlive(worker.pid);
2182
+ const stdoutBytes = fileSize(worker.stdoutPath);
2183
+ const stderrBytes = fileSize(worker.stderrPath);
2184
+ const heartbeatBytes = fileSize(worker.heartbeatPath);
2185
+ const changedFiles = gitStatusShort(worker.worktreePath);
2186
+ const gitAncestry = computeGitAncestry(worker.worktreePath, {
2187
+ base: options.base,
2188
+ baseCommit: options.baseCommit
2189
+ });
2190
+ const lastActivityAt = latestIso([
2191
+ parsed.lastEventAt,
2192
+ heartbeat.lastHeartbeatAt,
2193
+ fileMtime(worker.stdoutPath),
2194
+ fileMtime(worker.stderrPath),
2195
+ fileMtime(worker.heartbeatPath)
2196
+ ]);
2197
+ const error = parsed.error || (!alive && !finalResult ? tailFile(worker.stderrPath, 10).trim() || void 0 : void 0);
2198
+ const completionBlocker = typeof worker.completionBlocker === "string" && worker.completionBlocker.trim() ? worker.completionBlocker.trim() : null;
2199
+ const landingContract = worker.repairTargetPrUrl ? {
2200
+ landingOnly: false,
2201
+ targetPrUrls: [worker.repairTargetPrUrl],
2202
+ targetPrUrl: worker.repairTargetPrUrl,
2203
+ repairEnforceOriginalPr: true
2204
+ } : null;
2205
+ const attention = computeAttention({
2206
+ alive,
2207
+ finalResult,
2208
+ firstEventAt: parsed.firstEventAt,
2209
+ stdoutBytes,
2210
+ heartbeatBytes,
2211
+ lastActivityAt,
2212
+ heartbeatBlocker: heartbeat.heartbeatBlocker,
2213
+ startedAt: worker.startedAt,
2214
+ error,
2215
+ changedFiles,
2216
+ gitAncestry,
2217
+ completionBlocker,
2218
+ landingContract,
2219
+ prUrl: worker.repairTargetPrUrl ?? worker.taskPrUrl ?? null
2220
+ });
2221
+ const workerStatusLabel = completionBlocker || attention.state === "blocked" ? "blocked" : completionAcknowledged || attention.state === "done" ? "done" : finalResult ? "exited" : alive ? "running" : "exited";
2222
+ return {
2223
+ runId: worker.runId,
2224
+ worker: worker.name,
2225
+ pid: worker.pid,
2226
+ alive,
2227
+ status: workerStatusLabel,
2228
+ attention,
2229
+ branch: worker.branch,
2230
+ worktreePath: worker.worktreePath,
2231
+ ownedPaths: worker.ownedPaths,
2232
+ stdoutBytes,
2233
+ stderrBytes,
2234
+ heartbeatBytes,
2235
+ firstEventAt: parsed.firstEventAt,
2236
+ lastEventAt: parsed.lastEventAt,
2237
+ lastActivityAt,
2238
+ currentTool: completionAcknowledged ? null : parsed.currentTool,
2239
+ heartbeatCount: heartbeat.heartbeatCount,
2240
+ lastHeartbeatAt: heartbeat.lastHeartbeatAt,
2241
+ lastHeartbeatPhase: heartbeat.lastHeartbeatPhase,
2242
+ lastHeartbeatSummary: heartbeat.lastHeartbeatSummary,
2243
+ heartbeatBlocker: heartbeat.heartbeatBlocker,
2244
+ timestampAnomalies: heartbeat.timestampAnomalies,
2245
+ finalResult,
2246
+ error,
2247
+ changedFiles,
2248
+ gitAncestry,
2249
+ instructionPolicyFingerprint: worker.instructionPolicyFingerprint ?? null,
2250
+ instructionPolicyEvidence: worker.instructionPolicyEvidence ?? null
2251
+ };
2252
+ }
2253
+ function isFinishedWorkerStatus(status) {
2254
+ if (status.finalResult) return true;
2255
+ if (status.alive === false) return true;
2256
+ if (status.status === "exited" || status.status === "done") return true;
2257
+ return false;
2258
+ }
2259
+ function isLandingBlockedWorkerStatus(status) {
2260
+ if (!status.finalResult) return false;
2261
+ return status.attention.state === "needs_attention" || status.attention.state === "blocked";
2262
+ }
2263
+ function deriveRunStatus(fallback, workers) {
2264
+ if (workers.length === 0) return fallback;
2265
+ if (workers.some((w) => w.attention === "needs_attention" || w.attention === "stale" || w.attention === "blocked")) {
2266
+ return "needs_attention";
2267
+ }
2268
+ if (workers.every((w) => w.status === "done")) return "done";
2269
+ if (workers.some((w) => w.status === "running")) return "running";
2270
+ return fallback;
2271
+ }
2272
+
2273
+ // src/resource-gate.ts
2274
+ var DEFAULT_PER_WORKER_MEM_BYTES = 500 * 1024 * 1024;
2275
+ var DEFAULT_MEM_RESERVE_BYTES = 4 * 1024 * 1024 * 1024;
2276
+ var DEFAULT_MEM_UTILIZATION = 0.85;
2277
+ var AUTO_MAX_WORKERS_CEILING = 64;
2278
+ function positiveInt(value, fallback) {
2279
+ const n = Number(value);
2280
+ if (!Number.isFinite(n) || n <= 0) return fallback;
2281
+ return Math.floor(n);
2282
+ }
2283
+ function resolveResourceConfig(config = loadUserConfig(), configuredMaxWorkersOverride, totalMemBytes) {
2284
+ const perWorkerMemBytes = positiveInt(config.perWorkerMemBytes, DEFAULT_PER_WORKER_MEM_BYTES);
2285
+ const memReserveBytes = positiveInt(config.memReserveBytes, DEFAULT_MEM_RESERVE_BYTES);
2286
+ const memUtilization = Math.min(
2287
+ 1,
2288
+ Math.max(0.1, Number(config.memUtilization) > 0 ? Number(config.memUtilization) : DEFAULT_MEM_UTILIZATION)
2289
+ );
2290
+ const cap = resolveWorkerCap({
2291
+ config,
2292
+ configuredMaxWorkersOverride,
2293
+ totalMemBytes: totalMemBytes ?? os2.totalmem()
2294
+ });
2295
+ return {
2296
+ perWorkerMemBytes,
2297
+ memReserveBytes,
2298
+ memUtilization,
2299
+ configuredMaxWorkers: cap.configuredMaxWorkers,
2300
+ autoCap: cap.autoCap,
2301
+ workerCapSource: cap.workerCapSource
2302
+ };
2303
+ }
2304
+ function computeAutoMaxWorkers(totalMemBytes, opts = {}) {
2305
+ const perWorkerMemBytes = opts.perWorkerMemBytes ?? DEFAULT_PER_WORKER_MEM_BYTES;
2306
+ const memReserveBytes = opts.memReserveBytes ?? DEFAULT_MEM_RESERVE_BYTES;
2307
+ const memUtilization = opts.memUtilization ?? DEFAULT_MEM_UTILIZATION;
2308
+ const budgetBytes = Math.max(0, Math.floor(totalMemBytes * memUtilization) - memReserveBytes);
2309
+ const raw = Math.max(1, Math.floor(budgetBytes / perWorkerMemBytes));
2310
+ return Math.min(raw, AUTO_MAX_WORKERS_CEILING);
2311
+ }
2312
+ function readAvailableMemBytes() {
2313
+ return readMemAvailableBytes();
2314
+ }
2315
+ function isActiveHarnessWorker(worker) {
2316
+ if (typeof worker.completionBlocker === "string" && worker.completionBlocker.trim()) {
2317
+ return false;
2318
+ }
2319
+ const status = computeWorkerStatus(worker);
2320
+ return status.alive && !status.finalResult && status.attention.state !== "done";
2321
+ }
2322
+ function countActiveWorkersForRun(run) {
2323
+ let active = 0;
2324
+ for (const name of listRunWorkerNames(run)) {
2325
+ const worker = readJson(
2326
+ path9.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
2327
+ void 0
2328
+ );
2329
+ if (!worker || !isActiveHarnessWorker(worker)) continue;
2330
+ active++;
2331
+ }
2332
+ return active;
2333
+ }
2334
+ function countActiveWorkersGlobal() {
2335
+ let active = 0;
2336
+ for (const run of listRunRecords()) active += countActiveWorkersForRun(run);
2337
+ return active;
2338
+ }
2339
+ function observeRunnerResourceGate(input) {
2340
+ const config = input.config ?? loadUserConfig();
2341
+ const totalMemBytes = input.totalMemBytes ?? os2.totalmem();
2342
+ const { perWorkerMemBytes, memReserveBytes, memUtilization, configuredMaxWorkers, autoCap: resolvedAutoCap, workerCapSource } = resolveResourceConfig(config, input.configuredMaxWorkersOverride, totalMemBytes);
2343
+ const boxKind = resolveBoxKindFromConfig(config);
2344
+ const freeMemBytes = input.freeMemBytes ?? readAvailableMemBytes();
2345
+ const activeWorkers = input.activeWorkers ?? countActiveWorkersGlobal();
2346
+ const budgetBytes = Math.max(0, Math.floor(totalMemBytes * memUtilization) - memReserveBytes);
2347
+ const capacityFromTotal = Math.max(0, Math.floor(budgetBytes / perWorkerMemBytes));
2348
+ const capacityFromFree = Math.max(0, Math.floor(Math.max(0, freeMemBytes - memReserveBytes) / perWorkerMemBytes));
2349
+ const autoCap = resolvedAutoCap;
2350
+ const targetCap = configuredMaxWorkers ?? autoCap;
2351
+ const maxConcurrentWorkers = Math.max(0, Math.min(targetCap, capacityFromTotal));
2352
+ const slotsByCapacity = Math.max(0, maxConcurrentWorkers - activeWorkers);
2353
+ const slotsByFreeMem = capacityFromFree;
2354
+ let slotsAvailable = Math.min(slotsByCapacity, slotsByFreeMem);
2355
+ const skipDisk = input.skipDiskGate || process.env.KYNVER_RESOURCE_GATE_SKIP_DISK === "1";
2356
+ const diskGate = skipDisk ? void 0 : observeRunnerDiskGate({
2357
+ diskPath: input.diskPath?.trim() || process.env.KYNVER_DISK_GUARD_PATH?.trim() || "/"
2358
+ });
2359
+ if (diskGate && !diskGate.ok) slotsAvailable = 0;
2360
+ let reason = null;
2361
+ if (slotsAvailable <= 0) {
2362
+ if (diskGate && !diskGate.ok) {
2363
+ reason = diskGate.reason ?? "disk gate blocked worker admission";
2364
+ } else if (activeWorkers >= maxConcurrentWorkers) {
2365
+ reason = `at worker limit (${activeWorkers}/${maxConcurrentWorkers} running)`;
2366
+ } else if (capacityFromFree <= 0) {
2367
+ reason = "insufficient free memory \u2014 waiting for workers to finish";
2368
+ } else {
2369
+ reason = "no worker slots available";
2370
+ }
2371
+ }
2372
+ return {
2373
+ ok: slotsAvailable > 0,
2374
+ totalMemBytes,
2375
+ freeMemBytes,
2376
+ memReserveBytes,
2377
+ perWorkerMemBytes,
2378
+ configuredMaxWorkers,
2379
+ workerCapSource,
2380
+ boxKind,
2381
+ autoCap,
2382
+ capacityWorkers: capacityFromTotal,
2383
+ maxConcurrentWorkers,
2384
+ activeWorkers,
2385
+ slotsAvailable,
2386
+ reason,
2387
+ ...diskGate ? { diskGate } : {}
2388
+ };
2389
+ }
2390
+
2391
+ // src/worker-cap-source.ts
2392
+ function positiveInt2(value, fallback) {
2393
+ const n = Number(value);
2394
+ if (!Number.isFinite(n) || n <= 0) return fallback;
2395
+ return Math.floor(n);
2396
+ }
2397
+ function resolveWorkerCap(input) {
2398
+ const config = input.config ?? {};
2399
+ const env = input.env ?? process.env;
2400
+ const perWorkerMemBytes = positiveInt2(config.perWorkerMemBytes, DEFAULT_PER_WORKER_MEM_BYTES);
2401
+ const memReserveBytes = positiveInt2(config.memReserveBytes, DEFAULT_MEM_RESERVE_BYTES);
2402
+ const memUtilization = Math.min(
2403
+ 1,
2404
+ Math.max(0.1, Number(config.memUtilization) > 0 ? Number(config.memUtilization) : DEFAULT_MEM_UTILIZATION)
2405
+ );
2406
+ const autoCap = computeAutoMaxWorkers(input.totalMemBytes, {
2407
+ perWorkerMemBytes,
2408
+ memReserveBytes,
2409
+ memUtilization
2410
+ });
2411
+ if (input.configuredMaxWorkersOverride !== void 0 && input.configuredMaxWorkersOverride !== null) {
2412
+ return {
2413
+ configuredMaxWorkers: positiveInt2(input.configuredMaxWorkersOverride, autoCap),
2414
+ autoCap,
2415
+ workerCapSource: "workspace_override"
2416
+ };
2417
+ }
2418
+ if (config.maxConcurrentWorkers !== void 0 && config.maxConcurrentWorkers !== null) {
2419
+ const configured = positiveInt2(config.maxConcurrentWorkers, 0) || null;
2420
+ if (configured) {
2421
+ return {
2422
+ configuredMaxWorkers: Math.min(configured, AUTO_MAX_WORKERS_CEILING),
2423
+ autoCap,
2424
+ workerCapSource: "config"
2425
+ };
2426
+ }
2427
+ }
2428
+ const envIntentional = env.KYNVER_MAX_WORKERS_INTENTIONAL === "1" || env.KYNVER_MAX_WORKERS_INTENTIONAL === "true";
2429
+ const envCap = envIntentional && env.KYNVER_MAX_WORKERS ? positiveInt2(env.KYNVER_MAX_WORKERS, 0) || null : null;
2430
+ if (envCap) {
2431
+ return {
2432
+ configuredMaxWorkers: Math.min(envCap, AUTO_MAX_WORKERS_CEILING),
2433
+ autoCap,
2434
+ workerCapSource: "env"
2435
+ };
2436
+ }
2437
+ return {
2438
+ configuredMaxWorkers: null,
2439
+ autoCap,
2440
+ workerCapSource: "auto"
2441
+ };
2442
+ }
2443
+ function recommendSetupWorkerCap(input = {}) {
2444
+ const totalMemBytes = input.totalMemBytes ?? 0;
2445
+ const autoCap = computeAutoMaxWorkers(totalMemBytes, {
2446
+ perWorkerMemBytes: positiveInt2(input.config?.perWorkerMemBytes, DEFAULT_PER_WORKER_MEM_BYTES),
2447
+ memReserveBytes: positiveInt2(input.config?.memReserveBytes, DEFAULT_MEM_RESERVE_BYTES),
2448
+ memUtilization: input.config?.memUtilization && Number(input.config.memUtilization) > 0 ? Number(input.config.memUtilization) : DEFAULT_MEM_UTILIZATION
2449
+ });
2450
+ const diskGateOk = input.diskGateOk ?? true;
2451
+ const recommendedMaxWorkers = diskGateOk ? autoCap : Math.max(1, Math.min(autoCap, 4));
2452
+ return {
2453
+ totalMemBytes,
2454
+ autoCap,
2455
+ recommendedMaxWorkers,
2456
+ diskPath: input.diskPath ?? "/",
2457
+ diskGateOk,
2458
+ diskFreeBytes: input.diskFreeBytes ?? null
2459
+ };
2460
+ }
2461
+
2462
+ // src/config.ts
2463
+ import os3 from "node:os";
2464
+ var CONFIG_DIR = path10.join(homedir5(), ".kynver");
2465
+ var CONFIG_FILE = path10.join(CONFIG_DIR, "config.json");
2466
+ var CREDENTIALS_FILE = path10.join(CONFIG_DIR, "credentials");
2467
+ function loadUserConfig() {
2468
+ if (!existsSync11(CONFIG_FILE)) return {};
2469
+ try {
2470
+ return JSON.parse(readFileSync9(CONFIG_FILE, "utf8"));
2471
+ } catch {
2472
+ return {};
2473
+ }
2474
+ }
2475
+ function saveUserConfig(config) {
2476
+ mkdirSync2(CONFIG_DIR, { recursive: true });
2477
+ writeFileSync2(CONFIG_FILE, `${JSON.stringify(normalizeConfigPaths(config), null, 2)}
2478
+ `, { mode: 384 });
2479
+ }
2480
+ function normalizeConfigPaths(config) {
2481
+ return {
2482
+ ...config,
2483
+ ...config.harnessRoot?.trim() ? { harnessRoot: redactHomePath(config.harnessRoot.trim()) } : {},
2484
+ ...config.defaultRepo?.trim() ? { defaultRepo: redactHomePath(config.defaultRepo.trim()) } : {}
2485
+ };
2486
+ }
2487
+ function presentUserConfig(config) {
2488
+ return normalizeConfigPaths(config);
2489
+ }
2490
+ function inferSetupFields(existing, args) {
2491
+ const creds = loadCredentialsFile();
2492
+ 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();
2493
+ 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);
2494
+ const explicitRepo = typeof args.repo === "string" ? args.repo : args.discoverRepo === true || args.discoverRepo === "true" ? discoverDefaultRepo()?.repo : void 0;
2495
+ const defaultRepo = explicitRepo || existing.defaultRepo?.trim() || process.env.KYNVER_DEFAULT_REPO?.trim() || process.env.KYNVER_HARNESS_REPO?.trim() || discoverDefaultRepo()?.repo;
2496
+ const harnessRoot = (typeof args.harnessRoot === "string" ? args.harnessRoot : void 0) || existing.harnessRoot?.trim() || process.env.KYNVER_HARNESS_ROOT?.trim() || process.env.OPUS_HARNESS_ROOT?.trim();
2497
+ return {
2498
+ ...apiBaseUrl ? { apiBaseUrl: trimTrailingSlash(apiBaseUrl) } : {},
2499
+ ...agentOsId ? { agentOsId } : {},
2500
+ ...defaultRepo ? { defaultRepo } : {},
2501
+ ...harnessRoot ? { harnessRoot } : {},
2502
+ ...typeof args.agentOsSlug === "string" ? { agentOsSlug: args.agentOsSlug } : existing.agentOsSlug ? { agentOsSlug: existing.agentOsSlug } : {}
2503
+ };
2504
+ }
2505
+ var SETUP_PER_WORKER_MEM_BYTES = 500 * 1024 * 1024;
2506
+ var SETUP_MEM_RESERVE_BYTES = 4 * 1024 * 1024 * 1024;
2507
+ function resolveSetupWorkerConfig(existing, args, totalMemBytes = totalmem()) {
2508
+ const maxWorkersRaw = typeof args.maxWorkers === "string" ? args.maxWorkers : typeof args.maxConcurrentWorkers === "string" ? args.maxConcurrentWorkers : void 0;
2509
+ const explicitBoxKindArg = typeof args.boxKind === "string" ? args.boxKind : typeof args["box-kind"] === "string" ? String(args["box-kind"]) : void 0;
2510
+ const boxKind = resolveBoxIdentity(process.env, {
2511
+ ...existing,
2512
+ ...explicitBoxKindArg ? { boxKind: normalizeWorkerPoolBoxKind(explicitBoxKindArg) } : {}
2513
+ }).boxKind;
2514
+ const diskGate = observeRunnerDiskGate({
2515
+ diskPath: typeof args.diskPath === "string" ? args.diskPath : "/"
2516
+ });
2517
+ const capRecommendation = recommendSetupWorkerCap({
2518
+ totalMemBytes,
2519
+ diskPath: diskGate.path,
2520
+ diskGateOk: diskGate.ok,
2521
+ diskFreeBytes: diskGate.freeBytes,
2522
+ config: existing
2523
+ });
2524
+ if (maxWorkersRaw) {
2525
+ return {
2526
+ maxConcurrentWorkers: Math.max(1, Math.floor(Number(maxWorkersRaw))),
2527
+ maxConcurrentWorkersSource: "setup-flag",
2528
+ boxKind
2529
+ };
2530
+ }
2531
+ if (existing.maxConcurrentWorkers !== void 0 && existing.maxConcurrentWorkers !== null) {
2532
+ return {
2533
+ maxConcurrentWorkers: Math.max(1, Math.floor(Number(existing.maxConcurrentWorkers))),
2534
+ maxConcurrentWorkersSource: existing.maxConcurrentWorkersSource ?? "operator",
2535
+ boxKind
2536
+ };
2537
+ }
2538
+ return {
2539
+ maxConcurrentWorkers: capRecommendation.recommendedMaxWorkers,
2540
+ maxConcurrentWorkersSource: "setup-auto",
2541
+ boxKind
2542
+ };
2543
+ }
2544
+ function loadCredentialsFile() {
2545
+ if (!existsSync11(CREDENTIALS_FILE)) return {};
2546
+ try {
2547
+ return JSON.parse(readFileSync9(CREDENTIALS_FILE, "utf8"));
2548
+ } catch {
2549
+ return {};
2550
+ }
2551
+ }
2552
+ function saveCredentialsFile(parsed) {
2553
+ mkdirSync2(CONFIG_DIR, { recursive: true });
2554
+ writeFileSync2(CREDENTIALS_FILE, `${JSON.stringify(parsed, null, 2)}
2555
+ `, { mode: 384 });
2556
+ }
2557
+ function loadApiKey() {
2558
+ if (process.env.KYNVER_API_KEY) return process.env.KYNVER_API_KEY;
2559
+ return loadCredentialsFile().apiKey;
2560
+ }
2561
+ function saveApiKey(apiKey) {
2562
+ saveCredentialsFile({ ...loadCredentialsFile(), apiKey });
2563
+ }
2564
+ function loadRunnerToken(agentOsId) {
2565
+ const envToken = process.env.KYNVER_RUNNER_TOKEN?.trim();
2566
+ if (envToken) return envToken;
2567
+ const creds = loadCredentialsFile();
2568
+ if (!creds.runnerToken) return void 0;
2569
+ if (agentOsId && creds.runnerTokenAgentOsId && creds.runnerTokenAgentOsId !== agentOsId) {
2570
+ return void 0;
2571
+ }
2572
+ return creds.runnerToken;
2573
+ }
2574
+ function saveRunnerToken(agentOsId, token) {
2575
+ saveCredentialsFile({
2576
+ ...loadCredentialsFile(),
2577
+ runnerToken: token,
2578
+ runnerTokenAgentOsId: agentOsId
2579
+ });
2580
+ }
2581
+ function resolveBaseUrl(argsBaseUrl) {
2582
+ const baseUrl = resolveConfiguredBaseUrl(argsBaseUrl);
2583
+ if (!baseUrl) failConfig("requires --base-url, KYNVER_API_URL, KYNVER_CRON_FIRE_BASE_URL, or ~/.kynver/config.json apiBaseUrl");
2584
+ return baseUrl;
2585
+ }
2586
+ function resolveConfiguredBaseUrl(argsBaseUrl) {
2587
+ const baseUrl = argsBaseUrl || process.env.KYNVER_API_URL || process.env.KYNVER_CRON_FIRE_BASE_URL || process.env.OPENCLAW_CRON_FIRE_BASE_URL || loadUserConfig().apiBaseUrl;
2588
+ return baseUrl ? trimTrailingSlash(String(baseUrl)) : void 0;
2589
+ }
2590
+ function resolveConfiguredCallbackSecret(argsSecret, agentOsId) {
2591
+ const scoped = argsSecret || loadRunnerToken(agentOsId) || (agentOsId ? void 0 : loadRunnerToken(loadUserConfig().agentOsId));
2592
+ if (scoped) return String(scoped);
2593
+ const globalSecret = process.env.KYNVER_RUNTIME_SECRET || process.env.KYNVER_CRON_SECRET || process.env.OPENCLAW_CRON_SECRET;
2594
+ if (globalSecret) {
2595
+ console.warn(
2596
+ "[kynver] using deployment-level callback secret; run `kynver runner credential --agent-os-id <id>` for a scoped token"
2597
+ );
2598
+ return String(globalSecret);
2599
+ }
2600
+ return void 0;
2601
+ }
2602
+ function resolveCallbackSecret(argsSecret, agentOsId) {
2603
+ const configured = resolveConfiguredCallbackSecret(argsSecret, agentOsId);
2604
+ if (configured) return configured;
2605
+ failConfig(
2606
+ "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"
2607
+ );
2608
+ }
2609
+ async function resolveCallbackSecretWithMint(argsSecret, agentOsId, opts) {
2610
+ const configured = resolveConfiguredCallbackSecret(argsSecret, agentOsId);
2611
+ if (configured) return configured;
2612
+ const apiKey = loadApiKey();
2613
+ const baseUrl = resolveConfiguredBaseUrl(opts?.baseUrl);
2614
+ if (apiKey && agentOsId && baseUrl) {
2615
+ try {
2616
+ const token = await fetchRunnerCredential(agentOsId, { baseUrl, apiKey });
2617
+ saveRunnerToken(agentOsId, token);
2618
+ return token;
2619
+ } catch (error) {
2620
+ failConfig(`runner credential mint failed: ${error.message}`);
2621
+ }
2622
+ }
2623
+ failConfig(
2624
+ "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"
2625
+ );
2626
+ }
2627
+ async function refreshRunnerToken(agentOsId, opts) {
2628
+ const apiKey = loadApiKey();
2629
+ const baseUrl = resolveConfiguredBaseUrl(opts?.baseUrl);
2630
+ if (!apiKey || !agentOsId || !baseUrl) return null;
2631
+ try {
2632
+ const token = await fetchRunnerCredential(agentOsId, { baseUrl, apiKey });
2633
+ saveRunnerToken(agentOsId, token);
2634
+ return token;
2635
+ } catch {
2636
+ return null;
2637
+ }
2638
+ }
2639
+ async function refreshRunnerTokenForAuthFailure(rejectedSecret, agentOsId, opts) {
2640
+ const apiKey = loadApiKey();
2641
+ const baseUrl = resolveConfiguredBaseUrl(opts?.baseUrl);
2642
+ if (!apiKey) return { ok: false, reason: "KYNVER_API_KEY is required to refresh a rejected runner token" };
2643
+ if (!agentOsId) return { ok: false, reason: "agentOsId is required to refresh a rejected runner token" };
2644
+ if (!baseUrl) return { ok: false, reason: "KYNVER_API_URL or --base-url is required to refresh a rejected runner token" };
2645
+ try {
2646
+ const token = await fetchRunnerCredential(agentOsId, { baseUrl, apiKey });
2647
+ if (token && token !== rejectedSecret) {
2648
+ saveRunnerToken(agentOsId, token);
2649
+ return { ok: true, token };
2650
+ }
2651
+ return { ok: false, reason: "runner credential refresh returned the rejected token" };
2652
+ } catch (error) {
2653
+ return { ok: false, reason: error.message };
2654
+ }
2655
+ }
2656
+ async function fetchRunnerCredential(agentOsId, opts) {
2657
+ const apiKey = opts?.apiKey || loadApiKey();
2658
+ if (!apiKey) throw new Error("API key required \u2014 run `kynver login` first");
2659
+ const base = resolveBaseUrl(opts?.baseUrl);
2660
+ const url = `${base}/api/agent-os/by-id/${encodeURIComponent(agentOsId)}/runner-credentials`;
2661
+ const res = await fetch(url, {
2662
+ method: "POST",
2663
+ headers: {
2664
+ "Content-Type": "application/json",
2665
+ Authorization: `Bearer ${apiKey}`
2666
+ },
2667
+ body: JSON.stringify({})
2668
+ });
2669
+ const text = await res.text();
2670
+ let parsed = null;
2671
+ try {
2672
+ parsed = JSON.parse(text);
2673
+ } catch {
2674
+ parsed = null;
2675
+ }
2676
+ if (!res.ok || !parsed?.token) {
2677
+ throw new Error(
2678
+ `runner credential mint failed (${res.status}): ${parsed?.error ?? text.slice(0, 200)}`
2679
+ );
2680
+ }
2681
+ return parsed.token;
2682
+ }
2683
+ async function mintRunnerCredential(args) {
2684
+ const agentOsId = (args.agentOsId ? String(args.agentOsId) : loadUserConfig().agentOsId) || "";
2685
+ if (!agentOsId) failConfig("runner credential requires --agent-os-id or agentOsId in ~/.kynver/config.json");
2686
+ try {
2687
+ const token = await fetchRunnerCredential(agentOsId, {
2688
+ baseUrl: args.baseUrl ? String(args.baseUrl) : void 0
2689
+ });
2690
+ saveRunnerToken(agentOsId, token);
2691
+ console.log(
2692
+ JSON.stringify(
2693
+ {
2694
+ ok: true,
2695
+ agentOsId,
2696
+ credentialsPath: displayUserPath(CREDENTIALS_FILE),
2697
+ tokenPrefix: `${token.slice(0, 12)}\u2026`,
2698
+ note: "Scoped runner token saved; callbacks use X-Kynver-Runner-Token."
2699
+ },
2700
+ null,
2701
+ 2
2702
+ )
2703
+ );
2704
+ } catch (err) {
2705
+ console.error(err instanceof Error ? err.message : String(err));
2706
+ process.exit(1);
2707
+ }
2708
+ }
2709
+ function failConfig(message) {
2710
+ console.error(message);
2711
+ process.exit(1);
2712
+ }
2713
+ function parseArgs(argv) {
2714
+ const args = {};
2715
+ for (let i = 0; i < argv.length; i++) {
2716
+ const item = argv[i];
2717
+ if (!item.startsWith("--")) continue;
2718
+ const key = item.slice(2).replace(/-([a-z])/g, (_, c) => c.toUpperCase());
2719
+ const next = argv[i + 1];
2720
+ if (!next || next.startsWith("--")) args[key] = true;
2721
+ else {
2722
+ args[key] = next;
2723
+ i++;
2724
+ }
2725
+ }
2726
+ return args;
2727
+ }
2728
+ async function runSetup(args) {
2729
+ const existing = loadUserConfig();
2730
+ const diskGate = observeRunnerDiskGate({
2731
+ diskPath: typeof args.diskPath === "string" ? args.diskPath : "/"
2732
+ });
2733
+ const capRecommendation = recommendSetupWorkerCap({
2734
+ totalMemBytes: os3.totalmem(),
2735
+ diskPath: diskGate.path,
2736
+ diskGateOk: diskGate.ok,
2737
+ diskFreeBytes: diskGate.freeBytes,
2738
+ config: existing
2739
+ });
2740
+ const workerConfig = resolveSetupWorkerConfig(existing, args);
2741
+ const config = normalizeConfigPaths({
2742
+ ...existing,
2743
+ ...inferSetupFields(existing, args),
2744
+ ...workerConfig,
2745
+ workerProvider: typeof args.provider === "string" ? args.provider : existing.workerProvider || "cursor"
2746
+ });
2747
+ saveUserConfig(config);
2748
+ const boxIdentity = resolveBoxIdentity(process.env, config);
2749
+ let runnerCredentialNote;
2750
+ const apiKey = loadApiKey();
2751
+ const agentOsId = config.agentOsId;
2752
+ if (apiKey && agentOsId) {
2753
+ try {
2754
+ const token = await fetchRunnerCredential(agentOsId, {
2755
+ baseUrl: typeof args.apiBaseUrl === "string" ? args.apiBaseUrl : config.apiBaseUrl,
2756
+ apiKey
2757
+ });
2758
+ saveRunnerToken(agentOsId, token);
2759
+ runnerCredentialNote = "Scoped runner token minted and saved to ~/.kynver/credentials.";
2760
+ } catch {
2761
+ runnerCredentialNote = "Runner token not minted (server offline or master secret unset). Run `kynver runner credential` after deploy.";
2762
+ }
2763
+ }
2764
+ console.log(
2765
+ JSON.stringify(
2766
+ {
2767
+ ok: true,
2768
+ configPath: displayUserPath(CONFIG_FILE),
2769
+ config: presentUserConfig(config),
2770
+ boxKind: config.boxKind,
2771
+ boxKindSource: boxIdentity.source,
2772
+ workerCapRecommendation: capRecommendation,
2773
+ ...boxIdentity.warnings.length ? { boxIdentityWarnings: boxIdentity.warnings } : {},
2774
+ note: runnerCredentialNote ?? "boxKind and maxConcurrentWorkers persisted; override with --box-kind and --max-workers. Run `kynver login` + `kynver runner credential` for scoped callbacks."
2775
+ },
2776
+ null,
2777
+ 2
2778
+ )
2779
+ );
2425
2780
  }
2426
- function landingContractAttentionReason(verdict) {
2427
- if (!verdict.blocked) return void 0;
2428
- return verdict.detail ?? verdict.reason;
2781
+ async function runLogin(args) {
2782
+ const apiKey = typeof args.apiKey === "string" ? args.apiKey : process.env.KYNVER_API_KEY;
2783
+ if (!apiKey) failConfig("kynver login requires --api-key or KYNVER_API_KEY");
2784
+ saveApiKey(apiKey);
2785
+ console.log(JSON.stringify({ ok: true, credentialsPath: displayUserPath(CREDENTIALS_FILE) }, null, 2));
2429
2786
  }
2430
2787
 
2431
- // src/status.ts
2432
- var NO_START_MS = 18e4;
2433
- var STALE_MS = 6e5;
2434
- function computeAttention(input) {
2435
- const now = Date.now();
2436
- if (input.completionBlocker) {
2437
- return { state: "blocked", reason: input.completionBlocker };
2438
- }
2439
- if (input.finalResult) {
2440
- const landingSnapshot = {
2441
- finalResult: input.finalResult,
2442
- changedFiles: input.changedFiles ?? [],
2443
- gitAncestry: input.gitAncestry ?? null,
2444
- prUrl: input.prUrl ?? null
2445
- };
2446
- const landing = assessWorkerLanding(landingSnapshot);
2447
- if (landing.blocked) {
2448
- const detail = landingAttentionReason(landing);
2449
- return {
2450
- state: "needs_attention",
2451
- reason: landing.reason ? `landing blocked (${landing.reason}): ${detail}` : `landing blocked: ${detail}`
2452
- };
2453
- }
2454
- if (input.landingContract) {
2455
- const contractVerdict = assessWorkerLandingContract({
2456
- contract: input.landingContract,
2457
- snapshot: landingSnapshot,
2458
- finalResult: input.finalResult
2459
- });
2460
- const contractDetail = landingContractAttentionReason(contractVerdict);
2461
- if (contractDetail) {
2462
- return {
2463
- state: "needs_attention",
2464
- reason: contractVerdict.reason ? `landing contract (${contractVerdict.reason}): ${contractDetail}` : `landing contract: ${contractDetail}`
2465
- };
2466
- }
2467
- }
2468
- return { state: "done", reason: "final result recorded" };
2469
- }
2470
- if (!input.alive) {
2471
- const classified = classifyExitFailure(input.error);
2472
- if (classified) return { state: "blocked", reason: classified.reason };
2473
- const salvage = assessExitedWorkerSalvage({
2474
- alive: false,
2475
- finalResult: null,
2476
- changedFiles: input.changedFiles,
2477
- gitAncestry: input.gitAncestry
2478
- });
2479
- if (salvage?.salvageable) {
2480
- const tail2 = input.error?.trim();
2481
- return {
2482
- state: "needs_attention",
2483
- reason: tail2 ? `${salvage.attentionReason} (${tail2})` : salvage.attentionReason
2484
- };
2485
- }
2486
- const tail = input.error?.trim();
2788
+ // src/callback-headers.ts
2789
+ function buildHarnessCallbackHeaders(secret) {
2790
+ const trimmed = String(secret).trim();
2791
+ if (trimmed.startsWith("krc1.")) {
2487
2792
  return {
2488
- state: "needs_attention",
2489
- reason: tail ? `process exited without a final result: ${tail}` : salvage?.attentionReason ?? "process exited without a final result"
2793
+ "Content-Type": "application/json",
2794
+ "X-Kynver-Runner-Token": trimmed
2490
2795
  };
2491
2796
  }
2492
- if (input.heartbeatBlocker) {
2493
- return { state: "blocked", reason: `worker heartbeat reported blocker: ${input.heartbeatBlocker}` };
2494
- }
2495
- const startMs = input.startedAt ? Date.parse(input.startedAt) : NaN;
2496
- if (!input.firstEventAt && input.stdoutBytes === 0 && input.heartbeatBytes === 0 && Number.isFinite(startMs) && now - startMs > NO_START_MS) {
2497
- return { state: "needs_attention", reason: `no first stream event ${secsAgo(startMs)}s after start` };
2498
- }
2499
- const actMs = input.lastActivityAt ? Date.parse(input.lastActivityAt) : NaN;
2500
- if (Number.isFinite(actMs) && now - actMs > STALE_MS) {
2501
- return { state: "stale", reason: `no log/event/heartbeat activity for ${secsAgo(actMs)}s` };
2502
- }
2503
- return { state: "ok", reason: "recent activity" };
2504
- }
2505
- function resolveFinalResult(worker, parsedFinalResult, heartbeat) {
2506
- if (parsedFinalResult) return parsedFinalResult;
2507
- const ackSnapshot = worker.completionSnapshot?.finalResult;
2508
- if (ackSnapshot !== void 0 && ackSnapshot !== null) return ackSnapshot;
2509
- return terminalFinalResultFromHeartbeat(heartbeat);
2510
- }
2511
- function computeWorkerStatus(worker, options = {}) {
2512
- const parsed = parseHarnessStream(worker.stdoutPath);
2513
- const heartbeat = parseHeartbeat(worker.heartbeatPath);
2514
- const completionAcknowledged = typeof worker.completionReportedAt === "string" && worker.completionReportedAt.trim().length > 0;
2515
- const finalResult = resolveFinalResult(worker, parsed.finalResult, heartbeat);
2516
- const alive = completionAcknowledged ? false : isPidAlive(worker.pid);
2517
- const stdoutBytes = fileSize(worker.stdoutPath);
2518
- const stderrBytes = fileSize(worker.stderrPath);
2519
- const heartbeatBytes = fileSize(worker.heartbeatPath);
2520
- const changedFiles = gitStatusShort(worker.worktreePath);
2521
- const gitAncestry = computeGitAncestry(worker.worktreePath, {
2522
- base: options.base,
2523
- baseCommit: options.baseCommit
2524
- });
2525
- const lastActivityAt = latestIso([
2526
- parsed.lastEventAt,
2527
- heartbeat.lastHeartbeatAt,
2528
- fileMtime(worker.stdoutPath),
2529
- fileMtime(worker.stderrPath),
2530
- fileMtime(worker.heartbeatPath)
2531
- ]);
2532
- const error = parsed.error || (!alive && !finalResult ? tailFile(worker.stderrPath, 10).trim() || void 0 : void 0);
2533
- const completionBlocker = typeof worker.completionBlocker === "string" && worker.completionBlocker.trim() ? worker.completionBlocker.trim() : null;
2534
- const landingContract = worker.repairTargetPrUrl ? {
2535
- landingOnly: false,
2536
- targetPrUrls: [worker.repairTargetPrUrl],
2537
- targetPrUrl: worker.repairTargetPrUrl,
2538
- repairEnforceOriginalPr: true
2539
- } : null;
2540
- const attention = computeAttention({
2541
- alive,
2542
- finalResult,
2543
- firstEventAt: parsed.firstEventAt,
2544
- stdoutBytes,
2545
- heartbeatBytes,
2546
- lastActivityAt,
2547
- heartbeatBlocker: heartbeat.heartbeatBlocker,
2548
- startedAt: worker.startedAt,
2549
- error,
2550
- changedFiles,
2551
- gitAncestry,
2552
- completionBlocker,
2553
- landingContract,
2554
- prUrl: worker.repairTargetPrUrl ?? worker.taskPrUrl ?? null
2555
- });
2556
- const workerStatusLabel = completionBlocker || attention.state === "blocked" ? "blocked" : completionAcknowledged || attention.state === "done" ? "done" : finalResult ? "exited" : alive ? "running" : "exited";
2557
2797
  return {
2558
- runId: worker.runId,
2559
- worker: worker.name,
2560
- pid: worker.pid,
2561
- alive,
2562
- status: workerStatusLabel,
2563
- attention,
2564
- branch: worker.branch,
2565
- worktreePath: worker.worktreePath,
2566
- ownedPaths: worker.ownedPaths,
2567
- stdoutBytes,
2568
- stderrBytes,
2569
- heartbeatBytes,
2570
- firstEventAt: parsed.firstEventAt,
2571
- lastEventAt: parsed.lastEventAt,
2572
- lastActivityAt,
2573
- currentTool: completionAcknowledged ? null : parsed.currentTool,
2574
- heartbeatCount: heartbeat.heartbeatCount,
2575
- lastHeartbeatAt: heartbeat.lastHeartbeatAt,
2576
- lastHeartbeatPhase: heartbeat.lastHeartbeatPhase,
2577
- lastHeartbeatSummary: heartbeat.lastHeartbeatSummary,
2578
- heartbeatBlocker: heartbeat.heartbeatBlocker,
2579
- timestampAnomalies: heartbeat.timestampAnomalies,
2580
- finalResult,
2581
- error,
2582
- changedFiles,
2583
- gitAncestry,
2584
- instructionPolicyFingerprint: worker.instructionPolicyFingerprint ?? null,
2585
- instructionPolicyEvidence: worker.instructionPolicyEvidence ?? null
2798
+ "Content-Type": "application/json",
2799
+ // Canonical header. We keep sending the legacy `X-OpenClaw-Cron-Secret`
2800
+ // (and `X-Kynver-Runtime-Secret`) so an un-upgraded Kynver server that
2801
+ // only reads the old header still authenticates this runner during the
2802
+ // rename compat window.
2803
+ "X-Kynver-Cron-Secret": trimmed,
2804
+ "X-OpenClaw-Cron-Secret": trimmed,
2805
+ "X-Kynver-Runtime-Secret": trimmed
2586
2806
  };
2587
2807
  }
2588
- function isFinishedWorkerStatus(status) {
2589
- if (status.finalResult) return true;
2590
- if (status.alive === false) return true;
2591
- if (status.status === "exited" || status.status === "done") return true;
2592
- return false;
2808
+
2809
+ // src/callbacks.ts
2810
+ function callbackTimeoutMs() {
2811
+ const parsed = Number(process.env.KYNVER_CALLBACK_TIMEOUT_MS);
2812
+ if (Number.isFinite(parsed) && parsed > 0) return Math.floor(parsed);
2813
+ return 3e4;
2593
2814
  }
2594
- function isLandingBlockedWorkerStatus(status) {
2595
- if (!status.finalResult) return false;
2596
- return status.attention.state === "needs_attention" || status.attention.state === "blocked";
2815
+ async function withTimeout(fn) {
2816
+ const controller = new AbortController();
2817
+ const timeout = setTimeout(() => controller.abort(), callbackTimeoutMs());
2818
+ try {
2819
+ return await fn(controller.signal);
2820
+ } finally {
2821
+ clearTimeout(timeout);
2822
+ }
2597
2823
  }
2598
- function deriveRunStatus(fallback, workers) {
2599
- if (workers.length === 0) return fallback;
2600
- if (workers.some((w) => w.attention === "needs_attention" || w.attention === "stale" || w.attention === "blocked")) {
2601
- return "needs_attention";
2824
+ async function postJson(url, secret, body) {
2825
+ const res = await withTimeout(
2826
+ (signal) => fetch(url, {
2827
+ method: "POST",
2828
+ headers: buildHarnessCallbackHeaders(secret),
2829
+ body: JSON.stringify(body),
2830
+ signal
2831
+ })
2832
+ );
2833
+ let response = null;
2834
+ try {
2835
+ response = await res.json();
2836
+ } catch {
2837
+ response = null;
2602
2838
  }
2603
- if (workers.every((w) => w.status === "done")) return "done";
2604
- if (workers.some((w) => w.status === "running")) return "running";
2605
- return fallback;
2839
+ return { ok: res.ok, status: res.status, response };
2606
2840
  }
2607
-
2608
- // src/resource-gate.ts
2609
- var DEFAULT_PER_WORKER_MEM_BYTES = 500 * 1024 * 1024;
2610
- var DEFAULT_MEM_RESERVE_BYTES = 4 * 1024 * 1024 * 1024;
2611
- var DEFAULT_MEM_UTILIZATION = 0.85;
2612
- var AUTO_MAX_WORKERS_CEILING = 64;
2613
- function positiveInt(value, fallback) {
2614
- const n = Number(value);
2615
- if (!Number.isFinite(n) || n <= 0) return fallback;
2616
- return Math.floor(n);
2841
+ async function postJsonWithCredentialRefresh(url, secret, body, opts) {
2842
+ const first = await postJson(url, secret, body);
2843
+ if (first.ok || first.status !== 401) return first;
2844
+ const refreshed = await refreshRunnerTokenForAuthFailure(secret, opts.agentOsId, { baseUrl: opts.baseUrl });
2845
+ if (!refreshed.ok) return { ...first, authRefreshFailure: refreshed.reason };
2846
+ const retry = await postJson(url, refreshed.token, body);
2847
+ return { ...retry, refreshedAuth: true };
2617
2848
  }
2618
- function resolveResourceConfig(config = loadUserConfig(), configuredMaxWorkersOverride) {
2619
- const perWorkerMemBytes = positiveInt(config.perWorkerMemBytes, DEFAULT_PER_WORKER_MEM_BYTES);
2620
- const memReserveBytes = positiveInt(config.memReserveBytes, DEFAULT_MEM_RESERVE_BYTES);
2621
- const memUtilization = Math.min(
2622
- 1,
2623
- Math.max(0.1, Number(config.memUtilization) > 0 ? Number(config.memUtilization) : DEFAULT_MEM_UTILIZATION)
2849
+ async function getJson(url, secret) {
2850
+ const res = await withTimeout(
2851
+ (signal) => fetch(url, {
2852
+ method: "GET",
2853
+ headers: buildHarnessCallbackHeaders(secret),
2854
+ signal
2855
+ })
2624
2856
  );
2625
- const envCap = process.env.KYNVER_MAX_WORKERS ? positiveInt(process.env.KYNVER_MAX_WORKERS, 0) || null : null;
2626
- const configuredMaxWorkers = configuredMaxWorkersOverride !== void 0 ? configuredMaxWorkersOverride : envCap ?? (config.maxConcurrentWorkers !== void 0 && config.maxConcurrentWorkers !== null ? positiveInt(config.maxConcurrentWorkers, 0) || null : null);
2627
- return { perWorkerMemBytes, memReserveBytes, memUtilization, configuredMaxWorkers };
2628
- }
2629
- function computeAutoMaxWorkers(totalMemBytes, opts = {}) {
2630
- const perWorkerMemBytes = opts.perWorkerMemBytes ?? DEFAULT_PER_WORKER_MEM_BYTES;
2631
- const memReserveBytes = opts.memReserveBytes ?? DEFAULT_MEM_RESERVE_BYTES;
2632
- const memUtilization = opts.memUtilization ?? DEFAULT_MEM_UTILIZATION;
2633
- const budgetBytes = Math.max(0, Math.floor(totalMemBytes * memUtilization) - memReserveBytes);
2634
- const raw = Math.max(1, Math.floor(budgetBytes / perWorkerMemBytes));
2635
- return Math.min(raw, AUTO_MAX_WORKERS_CEILING);
2857
+ let response = null;
2858
+ try {
2859
+ response = await res.json();
2860
+ } catch {
2861
+ response = null;
2862
+ }
2863
+ return { ok: res.ok, status: res.status, response };
2636
2864
  }
2637
- function readAvailableMemBytes() {
2638
- return readMemAvailableBytes();
2865
+
2866
+ // src/dispatch-lane-normalization.ts
2867
+ function trimLower(value) {
2868
+ return (value ?? "").trim().toLowerCase();
2639
2869
  }
2640
- function isActiveHarnessWorker(worker) {
2641
- if (typeof worker.completionBlocker === "string" && worker.completionBlocker.trim()) {
2642
- return false;
2870
+ function roleLaneToDispatchLane(roleLane) {
2871
+ switch (trimLower(roleLane)) {
2872
+ case "implementer":
2873
+ case "repair_implementer":
2874
+ case "plan_author":
2875
+ case "runtime_verifier":
2876
+ return "implementation";
2877
+ case "plan_reviewer":
2878
+ case "report_reviewer":
2879
+ case "deep_reviewer":
2880
+ return "review";
2881
+ default:
2882
+ return null;
2643
2883
  }
2644
- const status = computeWorkerStatus(worker);
2645
- return status.alive && !status.finalResult && status.attention.state !== "done";
2646
2884
  }
2647
- function countActiveWorkersForRun(run) {
2648
- let active = 0;
2649
- for (const name of listRunWorkerNames(run)) {
2650
- const worker = readJson(
2651
- path10.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
2652
- void 0
2653
- );
2654
- if (!worker || !isActiveHarnessWorker(worker)) continue;
2655
- active++;
2885
+ function normalizeDispatchNextLaneFilter(raw) {
2886
+ const key = trimLower(raw);
2887
+ if (!key) return void 0;
2888
+ if (key === "implementation" || key === "review" || key === "landing" || key === "any") {
2889
+ return key;
2656
2890
  }
2657
- return active;
2658
- }
2659
- function countActiveWorkersGlobal() {
2660
- let active = 0;
2661
- for (const run of listRunRecords()) active += countActiveWorkersForRun(run);
2662
- return active;
2891
+ const mapped = roleLaneToDispatchLane(key);
2892
+ if (mapped) return mapped;
2893
+ if (key === "implement" || key === "repair" || key === "coding") return "implementation";
2894
+ if (key === "land" || key === "merge") return "landing";
2895
+ return void 0;
2663
2896
  }
2664
- function observeRunnerResourceGate(input) {
2665
- const { perWorkerMemBytes, memReserveBytes, memUtilization, configuredMaxWorkers } = resolveResourceConfig(
2666
- input.config,
2667
- input.configuredMaxWorkersOverride
2668
- );
2669
- const totalMemBytes = input.totalMemBytes ?? os2.totalmem();
2670
- const freeMemBytes = input.freeMemBytes ?? readAvailableMemBytes();
2671
- const activeWorkers = input.activeWorkers ?? countActiveWorkersGlobal();
2672
- const budgetBytes = Math.max(0, Math.floor(totalMemBytes * memUtilization) - memReserveBytes);
2673
- const capacityFromTotal = Math.max(0, Math.floor(budgetBytes / perWorkerMemBytes));
2674
- const capacityFromFree = Math.max(0, Math.floor(Math.max(0, freeMemBytes - memReserveBytes) / perWorkerMemBytes));
2675
- const autoCap = computeAutoMaxWorkers(totalMemBytes, { perWorkerMemBytes, memReserveBytes, memUtilization });
2676
- const targetCap = configuredMaxWorkers ?? autoCap;
2677
- const maxConcurrentWorkers = Math.max(0, Math.min(targetCap, capacityFromTotal));
2678
- const slotsByCapacity = Math.max(0, maxConcurrentWorkers - activeWorkers);
2679
- const slotsByFreeMem = capacityFromFree;
2680
- let slotsAvailable = Math.min(slotsByCapacity, slotsByFreeMem);
2681
- const skipDisk = input.skipDiskGate || process.env.KYNVER_RESOURCE_GATE_SKIP_DISK === "1";
2682
- const diskGate = skipDisk ? void 0 : observeRunnerDiskGate({
2683
- diskPath: input.diskPath?.trim() || process.env.KYNVER_DISK_GUARD_PATH?.trim() || "/"
2684
- });
2685
- if (diskGate && !diskGate.ok) slotsAvailable = 0;
2686
- let reason = null;
2687
- if (slotsAvailable <= 0) {
2688
- if (diskGate && !diskGate.ok) {
2689
- reason = diskGate.reason ?? "disk gate blocked worker admission";
2690
- } else if (activeWorkers >= maxConcurrentWorkers) {
2691
- reason = `at worker limit (${activeWorkers}/${maxConcurrentWorkers} running)`;
2692
- } else if (capacityFromFree <= 0) {
2693
- reason = "insufficient free memory \u2014 waiting for workers to finish";
2694
- } else {
2695
- reason = "no worker slots available";
2696
- }
2697
- }
2698
- return {
2699
- ok: slotsAvailable > 0,
2700
- totalMemBytes,
2701
- freeMemBytes,
2702
- memReserveBytes,
2703
- perWorkerMemBytes,
2704
- configuredMaxWorkers,
2705
- autoCap,
2706
- capacityWorkers: capacityFromTotal,
2707
- maxConcurrentWorkers,
2708
- activeWorkers,
2709
- slotsAvailable,
2710
- reason,
2711
- ...diskGate ? { diskGate } : {}
2712
- };
2897
+ function resolveDispatchNextLaneFilter(raw) {
2898
+ return normalizeDispatchNextLaneFilter(raw) ?? "any";
2713
2899
  }
2714
2900
 
2715
2901
  // src/worker-persona-catalog.ts
@@ -4005,15 +4191,15 @@ function resolveModelFallback(startedModel, launchModel, providerDefault) {
4005
4191
  }
4006
4192
 
4007
4193
  // src/retry-limits.ts
4008
- function positiveInt2(value, fallback) {
4194
+ function positiveInt3(value, fallback) {
4009
4195
  const n = Number(value);
4010
4196
  if (!Number.isFinite(n) || n <= 0) return fallback;
4011
4197
  return Math.floor(n);
4012
4198
  }
4013
4199
  function readHarnessRetryLimits() {
4014
4200
  return {
4015
- maxTaskAttempts: positiveInt2(process.env.KYNVER_MAX_TASK_ATTEMPTS, 4),
4016
- dispatchCooldownMs: positiveInt2(process.env.KYNVER_DISPATCH_COOLDOWN_MS, 5e3)
4201
+ maxTaskAttempts: positiveInt3(process.env.KYNVER_MAX_TASK_ATTEMPTS, 4),
4202
+ dispatchCooldownMs: positiveInt3(process.env.KYNVER_DISPATCH_COOLDOWN_MS, 5e3)
4017
4203
  };
4018
4204
  }
4019
4205
 
@@ -4054,22 +4240,15 @@ function resolveHarnessLeaseOwnerForRenewal(input) {
4054
4240
  }
4055
4241
 
4056
4242
  // src/runner-identity.ts
4057
- import os4 from "node:os";
4243
+ import os5 from "node:os";
4058
4244
 
4059
4245
  // src/box-resource-snapshot-shared.ts
4060
- import os3 from "node:os";
4061
- function normalizeWorkerPoolBoxKind(raw) {
4062
- const kind = (raw ?? "").trim().toLowerCase();
4063
- if (kind === "ghost" || kind === "forge") return kind;
4064
- if (kind.includes("forge")) return "forge";
4065
- if (kind.includes("ghost") || kind.includes("openclaw")) return "ghost";
4066
- return "forge";
4067
- }
4246
+ import os4 from "node:os";
4068
4247
  function resolveBoxKindFromEnv(env = process.env) {
4069
- return normalizeWorkerPoolBoxKind(env.KYNVER_BOX_KIND ?? env.KYNVER_AGENT_OS_SLUG ?? "forge");
4248
+ return resolveBoxIdentity(env).boxKind;
4070
4249
  }
4071
4250
  function defaultBoxId(boxKind, hostLabel) {
4072
- const host = (hostLabel ?? os3.hostname()).trim().toLowerCase().replace(/\s+/g, "-") || "unknown-host";
4251
+ const host = (hostLabel ?? os4.hostname()).trim().toLowerCase().replace(/\s+/g, "-") || "unknown-host";
4073
4252
  return `${boxKind}:${host}`;
4074
4253
  }
4075
4254
 
@@ -4080,10 +4259,10 @@ function trimOrNull5(value) {
4080
4259
  }
4081
4260
  function resolveRunnerPresencePayload(input = {}) {
4082
4261
  const env = input.env ?? process.env;
4083
- const runnerId = trimOrNull5(env.KYNVER_RUNTIME_ID) ?? trimOrNull5(env.OPENCLAW_RUNTIME_ID) ?? trimOrNull5(env.HOSTNAME) ?? os4.hostname();
4262
+ const runnerId = trimOrNull5(env.KYNVER_RUNTIME_ID) ?? trimOrNull5(env.OPENCLAW_RUNTIME_ID) ?? trimOrNull5(env.HOSTNAME) ?? os5.hostname();
4084
4263
  return {
4085
4264
  runnerId,
4086
- hostname: trimOrNull5(env.HOSTNAME) ?? os4.hostname(),
4265
+ hostname: trimOrNull5(env.HOSTNAME) ?? os5.hostname(),
4087
4266
  profile: trimOrNull5(env.KYNVER_RUNNER_PROFILE) ?? trimOrNull5(env.OPENCLAW_RUNNER_PROFILE),
4088
4267
  harnessRepo: trimOrNull5(env.KYNVER_HARNESS_REPO) ?? trimOrNull5(env.KYNVER_DEFAULT_REPO),
4089
4268
  runId: input.runId ?? null
@@ -4259,13 +4438,13 @@ function buildPrompt(input) {
4259
4438
  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."
4260
4439
  ];
4261
4440
  const mergeGateLines = compact ? [
4262
- "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)."
4441
+ "Merge-gate cost control: do not use Vercel previews/builds for PR verification. Run `node scripts/agent-os-pr-merge-gate.mjs --pr <url> --agent-os-id <id>` (or `verify-pr-local.mjs --from-pr` + 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)."
4263
4442
  ] : [
4264
4443
  "GitHub Actions merge-gate cost control (Kynver/Hermes PRs):",
4265
- "- Prefer local cached package verification (`node scripts/verify-pr-local.mjs --emit-json`) and Vercel preview evidence before GitHub Actions.",
4444
+ "- Prefer local cached package verification (`node scripts/verify-pr-local.mjs --emit-json`) before GitHub Actions. Do not use Vercel previews/builds as PR evidence.",
4266
4445
  "- Do not push empty commits to re-trigger CI. One budgeted merge-gate Actions run per PR candidate (head SHA) unless a human approves extra.",
4267
- "- Record evidence: POST `/api/agent-os/by-id/<agentOsId>/pr-merge-gate/refresh` with prUrl + local/vercel payloads.",
4268
- "- Request the final Actions run only when local + Vercel are green: POST `.../pr-merge-gate/request-run` (applies `merge-gate` label).",
4446
+ "- Record evidence: POST `/api/agent-os/by-id/<agentOsId>/pr-merge-gate/refresh` with prUrl + local payloads.",
4447
+ "- Request the final Actions run only when local verification is green: POST `.../pr-merge-gate/request-run` (applies `merge-gate` label).",
4269
4448
  "- Empty failed Actions jobs (no runner/steps/logs) are infra/quota \u2014 do not enter repair loops; escalate to operator."
4270
4449
  ];
4271
4450
  const planArtifactLines = compact ? [
@@ -4319,7 +4498,7 @@ function buildPrompt(input) {
4319
4498
  // src/providers/cursor.ts
4320
4499
  import { closeSync as closeSync4, existsSync as existsSync19, mkdirSync as mkdirSync3, openSync as openSync4, statSync as statSync4, unlinkSync } from "node:fs";
4321
4500
  import { spawn as spawn4 } from "node:child_process";
4322
- import os5 from "node:os";
4501
+ import os6 from "node:os";
4323
4502
  import path18 from "node:path";
4324
4503
 
4325
4504
  // src/providers/cursor-windows.ts
@@ -4430,7 +4609,7 @@ function positiveIntEnv(name, fallback) {
4430
4609
  return Number.isFinite(parsed) && parsed >= 0 ? Math.floor(parsed) : fallback;
4431
4610
  }
4432
4611
  function cursorStartLockPath() {
4433
- const root = process.env.KYNVER_CURSOR_START_LOCK_DIR?.trim() || path18.join(os5.homedir(), ".kynver", "locks");
4612
+ const root = process.env.KYNVER_CURSOR_START_LOCK_DIR?.trim() || path18.join(os6.homedir(), ".kynver", "locks");
4434
4613
  mkdirSync3(root, { recursive: true });
4435
4614
  return path18.join(root, "cursor-agent-start.lock");
4436
4615
  }
@@ -5165,8 +5344,8 @@ function dirtyPathsCoveredByDisposableRemoval(changedFiles, removed) {
5165
5344
  if (removed.length === 0) return false;
5166
5345
  const removedSet = new Set(removed.map((p) => normalizeRelativePath(p)));
5167
5346
  return material.every((line) => {
5168
- const path67 = normalizeRelativePath(pathFromGitStatusLine(line));
5169
- return removedSet.has(path67);
5347
+ const path69 = normalizeRelativePath(pathFromGitStatusLine(line));
5348
+ return removedSet.has(path69);
5170
5349
  });
5171
5350
  }
5172
5351
 
@@ -7505,10 +7684,10 @@ async function dispatchRun(args) {
7505
7684
  }
7506
7685
 
7507
7686
  // src/box-resource-snapshot.ts
7508
- import os6 from "node:os";
7687
+ import os7 from "node:os";
7509
7688
  function buildBoxResourceSnapshotFromGate(gate, input = {}) {
7510
- const boxKind = (input.boxKind ?? resolveBoxKindFromEnv()).trim().toLowerCase() || "forge";
7511
- const hostLabel = input.hostLabel ?? os6.hostname();
7689
+ const boxKind = (input.boxKind ?? resolveBoxKindFromConfig(loadUserConfig())).trim().toLowerCase() || "forge";
7690
+ const hostLabel = input.hostLabel ?? os7.hostname();
7512
7691
  const boxId = input.boxId ?? defaultBoxId(boxKind, hostLabel);
7513
7692
  return {
7514
7693
  boxId,
@@ -9169,7 +9348,7 @@ function harnessStorageSnapshot(opts = {}) {
9169
9348
  }
9170
9349
 
9171
9350
  // src/cleanup.ts
9172
- import path51 from "node:path";
9351
+ import path52 from "node:path";
9173
9352
 
9174
9353
  // src/cleanup-guards.ts
9175
9354
  import path41 from "node:path";
@@ -9340,11 +9519,11 @@ var LIVE_SKIP_REASONS = /* @__PURE__ */ new Set([
9340
9519
  function collectPreservedLivePaths(actions, skips) {
9341
9520
  const out = [];
9342
9521
  const seen = /* @__PURE__ */ new Set();
9343
- const push = (path67, reason, detail) => {
9344
- const key = `${path67}\0${reason}`;
9522
+ const push = (path69, reason, detail) => {
9523
+ const key = `${path69}\0${reason}`;
9345
9524
  if (seen.has(key) || out.length >= MAX_PRESERVED_LIVE_PATH_SAMPLES) return;
9346
9525
  seen.add(key);
9347
- out.push({ path: path67, reason, ...detail ? { detail } : {} });
9526
+ out.push({ path: path69, reason, ...detail ? { detail } : {} });
9348
9527
  };
9349
9528
  for (const skip2 of skips) {
9350
9529
  if (!LIVE_SKIP_REASONS.has(skip2.reason)) continue;
@@ -9421,60 +9600,263 @@ function pathAgeMs(target, now) {
9421
9600
  return 0;
9422
9601
  }
9423
9602
  }
9424
- function loadRunStatus(harnessRoot, runId) {
9425
- const runPath = path43.join(harnessRoot, "runs", runId, "run.json");
9426
- if (!existsSync33(runPath)) return null;
9427
- return readJson(runPath, null);
9603
+ function loadRunStatus(harnessRoot, runId) {
9604
+ const runPath = path43.join(harnessRoot, "runs", runId, "run.json");
9605
+ if (!existsSync33(runPath)) return null;
9606
+ return readJson(runPath, null);
9607
+ }
9608
+ function runDirectoryIsEmpty(runPath) {
9609
+ try {
9610
+ const entries = readdirSync11(runPath);
9611
+ return entries.length === 0;
9612
+ } catch {
9613
+ return false;
9614
+ }
9615
+ }
9616
+ function skipRunDirectoryRemoval(input) {
9617
+ const { harnessRoot, runId, runPath, ageMs, runDirectoriesAgeMs, activeGuards } = input;
9618
+ if (isWorktreeOnLiveRun(runPath, harnessRoot, runId, activeGuards.liveRunKeys)) {
9619
+ return "run_still_active";
9620
+ }
9621
+ if (!runDirectoryIsEmpty(runPath)) return "run_still_active";
9622
+ const run = loadRunStatus(harnessRoot, runId);
9623
+ if (run && !TERMINAL_RUN_STATUSES.has(run.status)) return "run_still_active";
9624
+ if (runDirectoriesAgeMs > 0 && ageMs < runDirectoriesAgeMs) return "below_age_threshold";
9625
+ return null;
9626
+ }
9627
+ function scanStaleRunDirectoryCandidates(opts) {
9628
+ if (!existsSync33(opts.worktreesDir)) return [];
9629
+ const candidates = [];
9630
+ let entries;
9631
+ try {
9632
+ entries = readdirSync11(opts.worktreesDir, { withFileTypes: true });
9633
+ } catch {
9634
+ return [];
9635
+ }
9636
+ for (const runEntry of entries) {
9637
+ if (!runEntry.isDirectory()) continue;
9638
+ const runId = runEntry.name;
9639
+ if (opts.runIdFilter && runId !== opts.runIdFilter) continue;
9640
+ const runPath = path43.join(opts.worktreesDir, runId);
9641
+ if (!runDirectoryIsEmpty(runPath)) continue;
9642
+ candidates.push({
9643
+ kind: "remove_run_directory",
9644
+ path: runPath,
9645
+ bytes: null,
9646
+ harnessRoot: opts.harnessRoot,
9647
+ runId,
9648
+ ageMs: pathAgeMs(runPath, opts.now)
9649
+ });
9650
+ }
9651
+ return candidates;
9652
+ }
9653
+
9654
+ // src/cleanup-execute.ts
9655
+ import { existsSync as existsSync35, rmSync as rmSync3 } from "node:fs";
9656
+
9657
+ // src/cleanup-remove-path.ts
9658
+ import { existsSync as existsSync34, rmSync as rmSync2 } from "node:fs";
9659
+
9660
+ // src/cleanup-path-ownership.ts
9661
+ import { lstatSync as lstatSync2, readdirSync as readdirSync12 } from "node:fs";
9662
+ function readPathOwnership(targetPath) {
9663
+ try {
9664
+ const st = lstatSync2(targetPath);
9665
+ const effectiveUid = typeof process.getuid === "function" ? process.getuid() : null;
9666
+ const effectiveGid = typeof process.getgid === "function" ? process.getgid() : null;
9667
+ const foreign = effectiveUid !== null && (st.uid !== effectiveUid || effectiveGid !== null && st.gid !== effectiveGid);
9668
+ return { uid: st.uid, gid: st.gid, foreign };
9669
+ } catch {
9670
+ return null;
9671
+ }
9672
+ }
9673
+ function pathHasForeignOwnedEntry(targetPath, maxEntries = 32) {
9674
+ const root = readPathOwnership(targetPath);
9675
+ if (!root) return false;
9676
+ if (root.foreign) return true;
9677
+ try {
9678
+ const names = readdirSync12(targetPath);
9679
+ let checked = 0;
9680
+ for (const name of names) {
9681
+ if (checked >= maxEntries) break;
9682
+ const child = `${targetPath.replace(/\/$/, "")}/${name}`;
9683
+ const info = readPathOwnership(child);
9684
+ checked += 1;
9685
+ if (info?.foreign) return true;
9686
+ }
9687
+ } catch {
9688
+ }
9689
+ return false;
9690
+ }
9691
+
9692
+ // src/cleanup-privileged-remove.ts
9693
+ import { spawnSync as spawnSync5 } from "node:child_process";
9694
+ import path45 from "node:path";
9695
+
9696
+ // src/cleanup-harness-path-validate.ts
9697
+ import path44 from "node:path";
9698
+ function isHarnessDependencyCachePath(targetPath, harnessRoot, worktreesDir, cacheDirName) {
9699
+ const resolved = path44.resolve(targetPath);
9700
+ const suffix = `${path44.sep}${cacheDirName}`;
9701
+ const cachePath = resolved.endsWith(suffix) ? resolved : null;
9702
+ if (!cachePath) return "path_outside_harness";
9703
+ const rel = path44.relative(worktreesDir, cachePath);
9704
+ if (rel.startsWith("..") || path44.isAbsolute(rel)) return "path_outside_harness";
9705
+ const parts = rel.split(path44.sep);
9706
+ if (parts.length < 3 || parts[parts.length - 1] !== cacheDirName) return "path_outside_harness";
9707
+ if (!resolved.startsWith(path44.resolve(harnessRoot))) return "path_outside_harness";
9708
+ return null;
9709
+ }
9710
+ function isHarnessNodeModulesPath(targetPath, harnessRoot, worktreesDir) {
9711
+ return isHarnessDependencyCachePath(targetPath, harnessRoot, worktreesDir, "node_modules");
9712
+ }
9713
+ function isHarnessNextCachePath(targetPath, harnessRoot, worktreesDir) {
9714
+ return isHarnessDependencyCachePath(targetPath, harnessRoot, worktreesDir, ".next");
9715
+ }
9716
+ function isHarnessBuildCachePath(targetPath, harnessRoot, worktreesDir) {
9717
+ const resolved = path44.resolve(targetPath);
9718
+ const relToWt = path44.relative(worktreesDir, resolved);
9719
+ if (relToWt.startsWith("..") || path44.isAbsolute(relToWt)) return "path_outside_harness";
9720
+ const parts = relToWt.split(path44.sep);
9721
+ if (parts.length < 3) return "path_outside_harness";
9722
+ if (!resolved.startsWith(path44.resolve(harnessRoot))) return "path_outside_harness";
9723
+ return null;
9724
+ }
9725
+ function isHarnessGeneratedCachePath(targetPath, harnessRoot, worktreesDir) {
9726
+ const resolved = path44.resolve(targetPath);
9727
+ return isHarnessNodeModulesPath(resolved, harnessRoot, worktreesDir) === null || isHarnessNextCachePath(resolved, harnessRoot, worktreesDir) === null || isHarnessBuildCachePath(resolved, harnessRoot, worktreesDir) === null;
9728
+ }
9729
+
9730
+ // src/cleanup-privileged-remove.ts
9731
+ function resolvePrivilegedCleanupMode() {
9732
+ const raw = (process.env.KYNVER_CLEANUP_PRIVILEGED ?? "auto").trim().toLowerCase();
9733
+ if (raw === "0" || raw === "false" || raw === "off" || raw === "no") return "off";
9734
+ if (raw === "1" || raw === "true" || raw === "force" || raw === "yes") return "force";
9735
+ return "auto";
9736
+ }
9737
+ function runSudoNonInteractive(argv) {
9738
+ const res = spawnSync5("sudo", ["-n", ...argv], {
9739
+ encoding: "utf8",
9740
+ stdio: ["ignore", "pipe", "pipe"]
9741
+ });
9742
+ return {
9743
+ ok: res.status === 0,
9744
+ stderr: `${res.stderr ?? ""}${res.stdout ?? ""}`.trim()
9745
+ };
9746
+ }
9747
+ function tryPrivilegedReclaimHarnessCache(targetPath, harnessRoot, worktreesDir) {
9748
+ if (!isHarnessGeneratedCachePath(targetPath, harnessRoot, worktreesDir)) {
9749
+ return { ok: false, error: "path is not an allowed harness generated cache" };
9750
+ }
9751
+ const effectiveUid = typeof process.getuid === "function" ? process.getuid() : null;
9752
+ const effectiveGid = typeof process.getgid === "function" ? process.getgid() : null;
9753
+ if (effectiveUid === null || effectiveGid === null) {
9754
+ return { ok: false, error: "privileged reclaim requires POSIX uid/gid" };
9755
+ }
9756
+ const chown = runSudoNonInteractive([
9757
+ "chown",
9758
+ "-R",
9759
+ `${effectiveUid}:${effectiveGid}`,
9760
+ path45.resolve(targetPath)
9761
+ ]);
9762
+ if (chown.ok) {
9763
+ return { ok: true, method: "chown_then_rm" };
9764
+ }
9765
+ const rm = runSudoNonInteractive(["rm", "-rf", path45.resolve(targetPath)]);
9766
+ if (rm.ok) {
9767
+ return { ok: true, method: "sudo_rm" };
9768
+ }
9769
+ const detail = chown.stderr || rm.stderr || "sudo -n failed (password required or not permitted)";
9770
+ return {
9771
+ ok: false,
9772
+ error: detail
9773
+ };
9774
+ }
9775
+ var HARNESS_ROOT_OWNED_CACHE_RUNBOOK = "Root-owned harness caches require operator reclaim: configure passwordless sudo for chown/rm under ~/.kynver/harness/worktrees, or run `node scripts/reclaim-harness-root-owned-cache.mjs --execute` as the harness owner with sudo available. See docs/runbooks/harness-root-owned-cache-reclaim.md.";
9776
+
9777
+ // src/cleanup-remove-path.ts
9778
+ function permissionFailure(error) {
9779
+ const code = error?.code;
9780
+ return code === "EACCES" || code === "EPERM";
9428
9781
  }
9429
- function runDirectoryIsEmpty(runPath) {
9430
- try {
9431
- const entries = readdirSync11(runPath);
9432
- return entries.length === 0;
9433
- } catch {
9434
- return false;
9782
+ function removeHarnessGeneratedPath(candidate, execute, deps = {}) {
9783
+ if (!existsSync34(candidate.path)) {
9784
+ return { executed: false, skipped: true, skipReason: "missing_worktree" };
9435
9785
  }
9436
- }
9437
- function skipRunDirectoryRemoval(input) {
9438
- const { harnessRoot, runId, runPath, ageMs, runDirectoriesAgeMs, activeGuards } = input;
9439
- if (isWorktreeOnLiveRun(runPath, harnessRoot, runId, activeGuards.liveRunKeys)) {
9440
- return "run_still_active";
9786
+ if (!execute) {
9787
+ return { executed: false, skipped: true, skipReason: "dry_run" };
9441
9788
  }
9442
- if (!runDirectoryIsEmpty(runPath)) return "run_still_active";
9443
- const run = loadRunStatus(harnessRoot, runId);
9444
- if (run && !TERMINAL_RUN_STATUSES.has(run.status)) return "run_still_active";
9445
- if (runDirectoriesAgeMs > 0 && ageMs < runDirectoriesAgeMs) return "below_age_threshold";
9446
- return null;
9447
- }
9448
- function scanStaleRunDirectoryCandidates(opts) {
9449
- if (!existsSync33(opts.worktreesDir)) return [];
9450
- const candidates = [];
9451
- let entries;
9789
+ const harnessRoot = candidate.harnessRoot;
9790
+ const worktreesDir = harnessRoot ? harnessWorktreesDir(harnessRoot) : null;
9791
+ const bytesBefore = candidate.bytes ?? directorySizeBytes(candidate.path);
9792
+ const bytesReported = bytesBefore ?? void 0;
9793
+ const removePath = deps.removePath ?? rmSync2;
9794
+ const hasForeignOwnedEntry = deps.hasForeignOwnedEntry ?? pathHasForeignOwnedEntry;
9452
9795
  try {
9453
- entries = readdirSync11(opts.worktreesDir, { withFileTypes: true });
9454
- } catch {
9455
- return [];
9456
- }
9457
- for (const runEntry of entries) {
9458
- if (!runEntry.isDirectory()) continue;
9459
- const runId = runEntry.name;
9460
- if (opts.runIdFilter && runId !== opts.runIdFilter) continue;
9461
- const runPath = path43.join(opts.worktreesDir, runId);
9462
- if (!runDirectoryIsEmpty(runPath)) continue;
9463
- candidates.push({
9464
- kind: "remove_run_directory",
9465
- path: runPath,
9466
- bytes: null,
9467
- harnessRoot: opts.harnessRoot,
9468
- runId,
9469
- ageMs: pathAgeMs(runPath, opts.now)
9470
- });
9796
+ removePath(candidate.path, { recursive: true, force: true });
9797
+ return { executed: true, skipped: false, bytes: bytesReported };
9798
+ } catch (error) {
9799
+ if (!permissionFailure(error) || !harnessRoot || !worktreesDir) {
9800
+ return {
9801
+ executed: false,
9802
+ skipped: true,
9803
+ skipReason: "remove_failed",
9804
+ error: error.message
9805
+ };
9806
+ }
9807
+ const foreign = hasForeignOwnedEntry(candidate.path);
9808
+ const mode = resolvePrivilegedCleanupMode();
9809
+ const shouldTryPrivileged = mode === "force" || mode === "auto" && foreign;
9810
+ if (!shouldTryPrivileged) {
9811
+ return foreign ? {
9812
+ executed: false,
9813
+ skipped: true,
9814
+ skipReason: "foreign_owner",
9815
+ error: `${error.message}; ${HARNESS_ROOT_OWNED_CACHE_RUNBOOK}`
9816
+ } : {
9817
+ executed: false,
9818
+ skipped: true,
9819
+ skipReason: "remove_failed",
9820
+ error: error.message
9821
+ };
9822
+ }
9823
+ const reclaim = tryPrivilegedReclaimHarnessCache(candidate.path, harnessRoot, worktreesDir);
9824
+ if (reclaim.ok && reclaim.method === "sudo_rm") {
9825
+ return {
9826
+ executed: true,
9827
+ skipped: false,
9828
+ bytes: bytesReported,
9829
+ privilegedReclaim: true
9830
+ };
9831
+ }
9832
+ if (reclaim.ok) {
9833
+ try {
9834
+ removePath(candidate.path, { recursive: true, force: true });
9835
+ return {
9836
+ executed: true,
9837
+ skipped: false,
9838
+ bytes: bytesReported,
9839
+ privilegedReclaim: true
9840
+ };
9841
+ } catch (retryError) {
9842
+ return {
9843
+ executed: false,
9844
+ skipped: true,
9845
+ skipReason: "foreign_owner",
9846
+ error: `${retryError.message}; privileged chown succeeded but rm still failed`
9847
+ };
9848
+ }
9849
+ }
9850
+ return {
9851
+ executed: false,
9852
+ skipped: true,
9853
+ skipReason: "foreign_owner",
9854
+ error: `${error.message}; privileged reclaim failed: ${reclaim.error}; ${HARNESS_ROOT_OWNED_CACHE_RUNBOOK}`
9855
+ };
9471
9856
  }
9472
- return candidates;
9473
9857
  }
9474
9858
 
9475
9859
  // src/cleanup-execute.ts
9476
- import { existsSync as existsSync34, rmSync as rmSync2 } from "node:fs";
9477
- import path44 from "node:path";
9478
9860
  function skipRunMetadataRemoval(candidate) {
9479
9861
  const harnessRoot = candidate.harnessRoot;
9480
9862
  if (!harnessRoot || !isHarnessRunMetadataPath(candidate.path, harnessRoot)) return null;
@@ -9488,35 +9870,15 @@ function skipRunMetadataRemoval(candidate) {
9488
9870
  function removeDependencyCache(candidate, execute) {
9489
9871
  const metadataSkip = skipRunMetadataRemoval(candidate);
9490
9872
  if (metadataSkip) return metadataSkip;
9491
- if (!existsSync34(candidate.path)) {
9492
- return {
9493
- ...candidate,
9494
- executed: false,
9495
- skipped: true,
9496
- skipReason: "missing_worktree"
9497
- };
9498
- }
9499
- if (!execute) {
9500
- return { ...candidate, executed: false, skipped: true, skipReason: "dry_run" };
9501
- }
9502
- try {
9503
- const bytesBefore = candidate.bytes ?? directorySizeBytes(candidate.path);
9504
- rmSync2(candidate.path, { recursive: true, force: true });
9505
- return {
9506
- ...candidate,
9507
- bytes: bytesBefore,
9508
- executed: true,
9509
- skipped: false
9510
- };
9511
- } catch (error) {
9512
- return {
9513
- ...candidate,
9514
- executed: false,
9515
- skipped: true,
9516
- skipReason: "remove_failed",
9517
- error: error.message
9518
- };
9519
- }
9873
+ const outcome = removeHarnessGeneratedPath(candidate, execute);
9874
+ return {
9875
+ ...candidate,
9876
+ bytes: outcome.bytes ?? candidate.bytes,
9877
+ executed: outcome.executed,
9878
+ skipped: outcome.skipped,
9879
+ skipReason: outcome.skipReason,
9880
+ error: outcome.error
9881
+ };
9520
9882
  }
9521
9883
  function removeNodeModules(candidate, execute) {
9522
9884
  return removeDependencyCache(candidate, execute);
@@ -9530,7 +9892,7 @@ function removeBuildCache(candidate, execute) {
9530
9892
  function removeRunDirectory(candidate, execute) {
9531
9893
  const metadataSkip = skipRunMetadataRemoval(candidate);
9532
9894
  if (metadataSkip) return metadataSkip;
9533
- if (!existsSync34(candidate.path)) {
9895
+ if (!existsSync35(candidate.path)) {
9534
9896
  return {
9535
9897
  ...candidate,
9536
9898
  executed: false,
@@ -9543,7 +9905,7 @@ function removeRunDirectory(candidate, execute) {
9543
9905
  }
9544
9906
  try {
9545
9907
  const bytesBefore = candidate.bytes ?? directorySizeBytes(candidate.path);
9546
- rmSync2(candidate.path, { recursive: true, force: true });
9908
+ rmSync3(candidate.path, { recursive: true, force: true });
9547
9909
  return {
9548
9910
  ...candidate,
9549
9911
  bytes: bytesBefore,
@@ -9563,7 +9925,7 @@ function removeRunDirectory(candidate, execute) {
9563
9925
  function removeWorktree(candidate, execute) {
9564
9926
  const metadataSkip = skipRunMetadataRemoval(candidate);
9565
9927
  if (metadataSkip) return metadataSkip;
9566
- if (!existsSync34(candidate.path)) {
9928
+ if (!existsSync35(candidate.path)) {
9567
9929
  return {
9568
9930
  ...candidate,
9569
9931
  executed: false,
@@ -9580,8 +9942,8 @@ function removeWorktree(candidate, execute) {
9580
9942
  if (repo) {
9581
9943
  git(repo, ["worktree", "remove", "--force", candidate.path], { allowFailure: true });
9582
9944
  }
9583
- if (existsSync34(candidate.path)) {
9584
- rmSync2(candidate.path, { recursive: true, force: true });
9945
+ if (existsSync35(candidate.path)) {
9946
+ rmSync3(candidate.path, { recursive: true, force: true });
9585
9947
  }
9586
9948
  return {
9587
9949
  ...candidate,
@@ -9599,37 +9961,10 @@ function removeWorktree(candidate, execute) {
9599
9961
  };
9600
9962
  }
9601
9963
  }
9602
- function isHarnessDependencyCachePath(targetPath, harnessRoot, worktreesDir, cacheDirName) {
9603
- const resolved = path44.resolve(targetPath);
9604
- const suffix = `${path44.sep}${cacheDirName}`;
9605
- const cachePath = resolved.endsWith(suffix) ? resolved : null;
9606
- if (!cachePath) return "path_outside_harness";
9607
- const rel = path44.relative(worktreesDir, cachePath);
9608
- if (rel.startsWith("..") || path44.isAbsolute(rel)) return "path_outside_harness";
9609
- const parts = rel.split(path44.sep);
9610
- if (parts.length < 3 || parts[parts.length - 1] !== cacheDirName) return "path_outside_harness";
9611
- if (!resolved.startsWith(path44.resolve(harnessRoot))) return "path_outside_harness";
9612
- return null;
9613
- }
9614
- function isHarnessNodeModulesPath(targetPath, harnessRoot, worktreesDir) {
9615
- return isHarnessDependencyCachePath(targetPath, harnessRoot, worktreesDir, "node_modules");
9616
- }
9617
- function isHarnessNextCachePath(targetPath, harnessRoot, worktreesDir) {
9618
- return isHarnessDependencyCachePath(targetPath, harnessRoot, worktreesDir, ".next");
9619
- }
9620
- function isHarnessBuildCachePath(targetPath, harnessRoot, worktreesDir) {
9621
- const resolved = path44.resolve(targetPath);
9622
- const relToWt = path44.relative(worktreesDir, resolved);
9623
- if (relToWt.startsWith("..") || path44.isAbsolute(relToWt)) return "path_outside_harness";
9624
- const parts = relToWt.split(path44.sep);
9625
- if (parts.length < 3) return "path_outside_harness";
9626
- if (!resolved.startsWith(path44.resolve(harnessRoot))) return "path_outside_harness";
9627
- return null;
9628
- }
9629
9964
 
9630
9965
  // src/cleanup-scan.ts
9631
- import { existsSync as existsSync35, readdirSync as readdirSync12, statSync as statSync10 } from "node:fs";
9632
- import path45 from "node:path";
9966
+ import { existsSync as existsSync36, readdirSync as readdirSync13, statSync as statSync10 } from "node:fs";
9967
+ import path46 from "node:path";
9633
9968
  function pathAgeMs2(target, now) {
9634
9969
  try {
9635
9970
  const mtime = statSync10(target).mtimeMs;
@@ -9639,16 +9974,16 @@ function pathAgeMs2(target, now) {
9639
9974
  }
9640
9975
  }
9641
9976
  function isPathInside(child, parent) {
9642
- const rel = path45.relative(parent, child);
9643
- return rel === "" || !rel.startsWith("..") && !path45.isAbsolute(rel);
9977
+ const rel = path46.relative(parent, child);
9978
+ return rel === "" || !rel.startsWith("..") && !path46.isAbsolute(rel);
9644
9979
  }
9645
9980
  function collectBuildCacheForWorktree(worktreePath, opts, seen, meta) {
9646
9981
  const out = [];
9647
9982
  for (const rel of HARNESS_BUILD_CACHE_RELATIVE_PATHS) {
9648
9983
  if (rel === ".next") continue;
9649
- const target = path45.join(worktreePath, rel);
9650
- if (!existsSync35(target)) continue;
9651
- const resolved = path45.resolve(target);
9984
+ const target = path46.join(worktreePath, rel);
9985
+ if (!existsSync36(target)) continue;
9986
+ const resolved = path46.resolve(target);
9652
9987
  if (seen.has(resolved)) continue;
9653
9988
  if (!isPathInside(resolved, opts.harnessRoot)) continue;
9654
9989
  seen.add(resolved);
@@ -9677,13 +10012,13 @@ function scanBuildCacheCandidates(opts) {
9677
10012
  })
9678
10013
  );
9679
10014
  }
9680
- if (!opts.includeOrphans || !existsSync35(opts.worktreesDir)) return candidates;
9681
- for (const runEntry of readdirSync12(opts.worktreesDir, { withFileTypes: true })) {
10015
+ if (!opts.includeOrphans || !existsSync36(opts.worktreesDir)) return candidates;
10016
+ for (const runEntry of readdirSync13(opts.worktreesDir, { withFileTypes: true })) {
9682
10017
  if (!runEntry.isDirectory()) continue;
9683
- const runPath = path45.join(opts.worktreesDir, runEntry.name);
9684
- for (const workerEntry of readdirSync12(runPath, { withFileTypes: true })) {
10018
+ const runPath = path46.join(opts.worktreesDir, runEntry.name);
10019
+ for (const workerEntry of readdirSync13(runPath, { withFileTypes: true })) {
9685
10020
  if (!workerEntry.isDirectory()) continue;
9686
- const worktreePath = path45.join(runPath, workerEntry.name);
10021
+ const worktreePath = path46.join(runPath, workerEntry.name);
9687
10022
  candidates.push(
9688
10023
  ...collectBuildCacheForWorktree(worktreePath, opts, seen, {
9689
10024
  runId: runEntry.name,
@@ -9704,7 +10039,7 @@ function scanWorktreeCandidates(opts) {
9704
10039
  for (const entry of opts.index.values()) {
9705
10040
  if (opts.runIdFilter && entry.runId !== opts.runIdFilter) continue;
9706
10041
  const resolved = entry.worktreePath;
9707
- if (!existsSync35(resolved)) continue;
10042
+ if (!existsSync36(resolved)) continue;
9708
10043
  if (seen.has(resolved)) continue;
9709
10044
  seen.add(resolved);
9710
10045
  candidates.push({
@@ -9718,24 +10053,24 @@ function scanWorktreeCandidates(opts) {
9718
10053
  });
9719
10054
  }
9720
10055
  }
9721
- if (!orphanEnabled || !existsSync35(opts.worktreesDir)) return candidates;
10056
+ if (!orphanEnabled || !existsSync36(opts.worktreesDir)) return candidates;
9722
10057
  const indexedPaths = /* @__PURE__ */ new Set();
9723
10058
  for (const entry of opts.index.values()) {
9724
- indexedPaths.add(path45.resolve(entry.worktreePath));
10059
+ indexedPaths.add(path46.resolve(entry.worktreePath));
9725
10060
  }
9726
- for (const runEntry of readdirSync12(opts.worktreesDir, { withFileTypes: true })) {
10061
+ for (const runEntry of readdirSync13(opts.worktreesDir, { withFileTypes: true })) {
9727
10062
  if (!runEntry.isDirectory()) continue;
9728
10063
  if (opts.runIdFilter && runEntry.name !== opts.runIdFilter) continue;
9729
- const runPath = path45.join(opts.worktreesDir, runEntry.name);
10064
+ const runPath = path46.join(opts.worktreesDir, runEntry.name);
9730
10065
  let workerEntries;
9731
10066
  try {
9732
- workerEntries = readdirSync12(runPath, { withFileTypes: true });
10067
+ workerEntries = readdirSync13(runPath, { withFileTypes: true });
9733
10068
  } catch {
9734
10069
  continue;
9735
10070
  }
9736
10071
  for (const workerEntry of workerEntries) {
9737
10072
  if (!workerEntry.isDirectory()) continue;
9738
- const worktreePath = path45.resolve(path45.join(runPath, workerEntry.name));
10073
+ const worktreePath = path46.resolve(path46.join(runPath, workerEntry.name));
9739
10074
  if (seen.has(worktreePath)) continue;
9740
10075
  if (indexedPaths.has(worktreePath)) continue;
9741
10076
  if (!isPathInside(worktreePath, opts.harnessRoot)) continue;
@@ -9754,8 +10089,8 @@ function scanWorktreeCandidates(opts) {
9754
10089
  }
9755
10090
 
9756
10091
  // src/cleanup-dependency-scan.ts
9757
- import { existsSync as existsSync36, readdirSync as readdirSync13, statSync as statSync11 } from "node:fs";
9758
- import path46 from "node:path";
10092
+ import { existsSync as existsSync37, readdirSync as readdirSync14, statSync as statSync11 } from "node:fs";
10093
+ import path47 from "node:path";
9759
10094
  var DEPENDENCY_CACHE_DIRS = [
9760
10095
  { dirName: "node_modules", kind: "remove_node_modules" },
9761
10096
  { dirName: ".next", kind: "remove_next_cache" }
@@ -9769,12 +10104,12 @@ function pathAgeMs3(target, now) {
9769
10104
  }
9770
10105
  }
9771
10106
  function isPathInside2(child, parent) {
9772
- const rel = path46.relative(parent, child);
9773
- return rel === "" || !rel.startsWith("..") && !path46.isAbsolute(rel);
10107
+ const rel = path47.relative(parent, child);
10108
+ return rel === "" || !rel.startsWith("..") && !path47.isAbsolute(rel);
9774
10109
  }
9775
10110
  function pushCandidate2(candidates, seen, opts, targetPath, kind, meta) {
9776
- if (!existsSync36(targetPath)) return;
9777
- const resolved = path46.resolve(targetPath);
10111
+ if (!existsSync37(targetPath)) return;
10112
+ const resolved = path47.resolve(targetPath);
9778
10113
  if (seen.has(resolved)) return;
9779
10114
  if (!isPathInside2(resolved, opts.harnessRoot)) return;
9780
10115
  seen.add(resolved);
@@ -9791,7 +10126,7 @@ function pushCandidate2(candidates, seen, opts, targetPath, kind, meta) {
9791
10126
  }
9792
10127
  function scanWorktreeDependencyCaches(candidates, seen, opts, worktreePath, meta) {
9793
10128
  for (const entry of DEPENDENCY_CACHE_DIRS) {
9794
- pushCandidate2(candidates, seen, opts, path46.join(worktreePath, entry.dirName), entry.kind, meta);
10129
+ pushCandidate2(candidates, seen, opts, path47.join(worktreePath, entry.dirName), entry.kind, meta);
9795
10130
  }
9796
10131
  }
9797
10132
  function scanDependencyCacheCandidates(opts) {
@@ -9805,20 +10140,20 @@ function scanDependencyCacheCandidates(opts) {
9805
10140
  repo: entry.run.repo
9806
10141
  });
9807
10142
  }
9808
- if (!opts.includeOrphans || !existsSync36(opts.worktreesDir)) return candidates;
9809
- for (const runEntry of readdirSync13(opts.worktreesDir, { withFileTypes: true })) {
10143
+ if (!opts.includeOrphans || !existsSync37(opts.worktreesDir)) return candidates;
10144
+ for (const runEntry of readdirSync14(opts.worktreesDir, { withFileTypes: true })) {
9810
10145
  if (!runEntry.isDirectory()) continue;
9811
10146
  if (opts.runIdFilter && runEntry.name !== opts.runIdFilter) continue;
9812
- const runPath = path46.join(opts.worktreesDir, runEntry.name);
10147
+ const runPath = path47.join(opts.worktreesDir, runEntry.name);
9813
10148
  let workerEntries;
9814
10149
  try {
9815
- workerEntries = readdirSync13(runPath, { withFileTypes: true });
10150
+ workerEntries = readdirSync14(runPath, { withFileTypes: true });
9816
10151
  } catch {
9817
10152
  continue;
9818
10153
  }
9819
10154
  for (const workerEntry of workerEntries) {
9820
10155
  if (!workerEntry.isDirectory()) continue;
9821
- const worktreePath = path46.join(runPath, workerEntry.name);
10156
+ const worktreePath = path47.join(runPath, workerEntry.name);
9822
10157
  scanWorktreeDependencyCaches(candidates, seen, opts, worktreePath, {
9823
10158
  runId: runEntry.name,
9824
10159
  worker: workerEntry.name
@@ -9829,8 +10164,8 @@ function scanDependencyCacheCandidates(opts) {
9829
10164
  }
9830
10165
 
9831
10166
  // src/cleanup-duplicate-worktrees.ts
9832
- import { existsSync as existsSync37, statSync as statSync12 } from "node:fs";
9833
- import path47 from "node:path";
10167
+ import { existsSync as existsSync38, statSync as statSync12 } from "node:fs";
10168
+ import path48 from "node:path";
9834
10169
  function pathAgeMs4(target, now) {
9835
10170
  try {
9836
10171
  const mtime = statSync12(target).mtimeMs;
@@ -9860,8 +10195,8 @@ function parseWorktreePorcelain(output) {
9860
10195
  return records;
9861
10196
  }
9862
10197
  function isUnderWorktreesDir(worktreePath, worktreesDir) {
9863
- const rel = path47.relative(path47.resolve(worktreesDir), path47.resolve(worktreePath));
9864
- return rel !== "" && !rel.startsWith("..") && !path47.isAbsolute(rel);
10198
+ const rel = path48.relative(path48.resolve(worktreesDir), path48.resolve(worktreePath));
10199
+ return rel !== "" && !rel.startsWith("..") && !path48.isAbsolute(rel);
9865
10200
  }
9866
10201
  function isCleanWorktree(worktreePath, repoRoot) {
9867
10202
  try {
@@ -9874,14 +10209,14 @@ function isCleanWorktree(worktreePath, repoRoot) {
9874
10209
  }
9875
10210
  }
9876
10211
  function scanDuplicateWorktreeCandidates(opts) {
9877
- if (!opts.includeOrphans || !existsSync37(opts.worktreesDir)) return [];
10212
+ if (!opts.includeOrphans || !existsSync38(opts.worktreesDir)) return [];
9878
10213
  const repos = /* @__PURE__ */ new Set();
9879
10214
  for (const entry of opts.index.values()) {
9880
- if (entry.run.repo) repos.add(path47.resolve(entry.run.repo));
10215
+ if (entry.run.repo) repos.add(path48.resolve(entry.run.repo));
9881
10216
  }
9882
10217
  const indexedPaths = /* @__PURE__ */ new Set();
9883
10218
  for (const entry of opts.index.values()) {
9884
- indexedPaths.add(path47.resolve(entry.worktreePath));
10219
+ indexedPaths.add(path48.resolve(entry.worktreePath));
9885
10220
  }
9886
10221
  const candidates = [];
9887
10222
  const seen = /* @__PURE__ */ new Set();
@@ -9894,15 +10229,15 @@ function scanDuplicateWorktreeCandidates(opts) {
9894
10229
  }
9895
10230
  const worktrees = parseWorktreePorcelain(porcelain);
9896
10231
  for (const wt of worktrees) {
9897
- const resolved = path47.resolve(wt.path);
9898
- if (resolved === path47.resolve(repoRoot)) continue;
10232
+ const resolved = path48.resolve(wt.path);
10233
+ if (resolved === path48.resolve(repoRoot)) continue;
9899
10234
  if (!isUnderWorktreesDir(resolved, opts.worktreesDir)) continue;
9900
10235
  if (indexedPaths.has(resolved)) continue;
9901
10236
  if (seen.has(resolved)) continue;
9902
- if (!existsSync37(resolved)) continue;
10237
+ if (!existsSync38(resolved)) continue;
9903
10238
  if (!isCleanWorktree(resolved, repoRoot)) continue;
9904
- const rel = path47.relative(opts.worktreesDir, resolved);
9905
- const parts = rel.split(path47.sep);
10239
+ const rel = path48.relative(opts.worktreesDir, resolved);
10240
+ const parts = rel.split(path48.sep);
9906
10241
  const runId = parts[0];
9907
10242
  const worker = parts[1] ?? "unknown";
9908
10243
  seen.add(resolved);
@@ -9921,12 +10256,12 @@ function scanDuplicateWorktreeCandidates(opts) {
9921
10256
  }
9922
10257
 
9923
10258
  // src/cleanup-worktree-index.ts
9924
- import path48 from "node:path";
10259
+ import path49 from "node:path";
9925
10260
  function buildWorktreeIndexAt(harnessRoot) {
9926
10261
  const index = /* @__PURE__ */ new Map();
9927
10262
  for (const run of listRunRecordsForHarnessRoot(harnessRoot)) {
9928
10263
  for (const name of Object.keys(run.workers || {})) {
9929
- const workerPath = path48.join(
10264
+ const workerPath = path49.join(
9930
10265
  runDirectoryAt(harnessRoot, run.id),
9931
10266
  "workers",
9932
10267
  safeSlug(name),
@@ -9934,9 +10269,9 @@ function buildWorktreeIndexAt(harnessRoot) {
9934
10269
  );
9935
10270
  const worker = readJson(workerPath, void 0);
9936
10271
  if (!worker?.worktreePath) continue;
9937
- index.set(path48.resolve(worker.worktreePath), {
10272
+ index.set(path49.resolve(worker.worktreePath), {
9938
10273
  harnessRoot,
9939
- worktreePath: path48.resolve(worker.worktreePath),
10274
+ worktreePath: path49.resolve(worker.worktreePath),
9940
10275
  runId: run.id,
9941
10276
  workerName: name,
9942
10277
  run,
@@ -10005,15 +10340,15 @@ function resolvePipelineHarnessRetention(runId) {
10005
10340
  }
10006
10341
 
10007
10342
  // src/cleanup-orphan-safety.ts
10008
- import { existsSync as existsSync38, statSync as statSync13 } from "node:fs";
10009
- import path49 from "node:path";
10343
+ import { existsSync as existsSync39, statSync as statSync13 } from "node:fs";
10344
+ import path50 from "node:path";
10010
10345
  var DEFAULT_HEARTBEAT_FRESH_MS = 30 * 60 * 1e3;
10011
10346
  function assessOrphanWorktreeSafety(input) {
10012
10347
  const now = input.now ?? Date.now();
10013
10348
  const heartbeatFreshMs = input.heartbeatFreshMs ?? DEFAULT_HEARTBEAT_FRESH_MS;
10014
- if (!existsSync38(input.worktreePath)) return null;
10349
+ if (!existsSync39(input.worktreePath)) return null;
10015
10350
  if (input.runId && input.workerName) {
10016
- const heartbeatPath = path49.join(
10351
+ const heartbeatPath = path50.join(
10017
10352
  input.harnessRoot,
10018
10353
  "runs",
10019
10354
  input.runId,
@@ -10027,8 +10362,8 @@ function assessOrphanWorktreeSafety(input) {
10027
10362
  } catch {
10028
10363
  }
10029
10364
  }
10030
- const gitDir = path49.join(input.worktreePath, ".git");
10031
- if (!existsSync38(gitDir)) return null;
10365
+ const gitDir = path50.join(input.worktreePath, ".git");
10366
+ if (!existsSync39(gitDir)) return null;
10032
10367
  const porcelain = gitCapture(input.worktreePath, ["status", "--porcelain"]);
10033
10368
  if (porcelain.status !== 0) return "pr_or_unmerged_commits";
10034
10369
  const dirtyLines = porcelain.stdout.split("\n").map((line) => line.trim()).filter((line) => line.length > 0);
@@ -10057,12 +10392,12 @@ function assessOrphanWorktreeSafety(input) {
10057
10392
  }
10058
10393
 
10059
10394
  // src/cleanup-harness-roots.ts
10060
- import { existsSync as existsSync39 } from "node:fs";
10395
+ import { existsSync as existsSync40 } from "node:fs";
10061
10396
  import { homedir as homedir13 } from "node:os";
10062
- import path50 from "node:path";
10397
+ import path51 from "node:path";
10063
10398
  var WELL_KNOWN_HARNESS_SCAN_ROOTS = [
10064
10399
  "/var/tmp/kynver-harness",
10065
- path50.join(homedir13(), ".openclaw", "harness")
10400
+ path51.join(homedir13(), ".openclaw", "harness")
10066
10401
  ];
10067
10402
  function addRoot(seen, roots, candidate) {
10068
10403
  if (!candidate?.trim()) return;
@@ -10084,8 +10419,8 @@ function resolveHarnessScanRoots(options = {}) {
10084
10419
  for (const candidate of extra ?? []) addRoot(seen, roots, candidate);
10085
10420
  if (shouldScanWellKnownRoots(options)) {
10086
10421
  for (const candidate of WELL_KNOWN_HARNESS_SCAN_ROOTS) {
10087
- const resolved = path50.resolve(candidate);
10088
- if (!seen.has(resolved) && existsSync39(resolved)) addRoot(seen, roots, resolved);
10422
+ const resolved = path51.resolve(candidate);
10423
+ if (!seen.has(resolved) && existsSync40(resolved)) addRoot(seen, roots, resolved);
10089
10424
  }
10090
10425
  }
10091
10426
  return roots;
@@ -10115,11 +10450,19 @@ function observeCleanupDiskPressure(input = {}) {
10115
10450
  }
10116
10451
  function applyDiskPressureToRetention(retention, pressure) {
10117
10452
  if (!pressure.pressured) return retention;
10118
- const executeOnPressure = retention.execute || envFlag2("KYNVER_CLEANUP_EXECUTE_ON_PRESSURE");
10453
+ const executeOnPressure = retention.execute || !envFlag2("KYNVER_CLEANUP_DRY_RUN_ON_PRESSURE");
10119
10454
  return {
10120
10455
  ...retention,
10121
10456
  execute: executeOnPressure,
10122
10457
  nodeModulesAgeMs: 0,
10458
+ // Disk pressure means the current-run-only cleanup scope has already fallen
10459
+ // behind. Expand the sweep to every known harness root/run, but keep the
10460
+ // worktree salvage/PR/dirty/live-worker guards in the cleanup pipeline.
10461
+ runIdFilter: void 0,
10462
+ includeOrphans: true,
10463
+ terminalWorktreesAgeMs: 0,
10464
+ runDirectoriesAgeMs: 0,
10465
+ worktreesAgeMs: retention.worktreesAgeMs > 0 ? retention.worktreesAgeMs : DEFAULT_WORKTREES_AGE_MS,
10123
10466
  diskPressure: true,
10124
10467
  diskGate: pressure.diskGate
10125
10468
  };
@@ -10231,9 +10574,9 @@ function mergeWorktreeIndexes(scanRoots) {
10231
10574
  }
10232
10575
  function worktreePathForCandidate(candidate, worktreesDir) {
10233
10576
  if (candidate.runId && candidate.worker) {
10234
- return path51.join(worktreesDir, candidate.runId, candidate.worker);
10577
+ return path52.join(worktreesDir, candidate.runId, candidate.worker);
10235
10578
  }
10236
- return path51.resolve(candidate.path, "..");
10579
+ return path52.resolve(candidate.path, "..");
10237
10580
  }
10238
10581
  function runHarnessCleanup(options = {}) {
10239
10582
  let retention = resolveHarnessRetention(options);
@@ -10258,7 +10601,7 @@ function runHarnessCleanup(options = {}) {
10258
10601
  for (const harnessRoot of paths.scanRoots) {
10259
10602
  if (atSweepCap()) break;
10260
10603
  emitCleanupProgress("root", harnessRoot);
10261
- const worktreesDir = path51.join(harnessRoot, "worktrees");
10604
+ const worktreesDir = path52.join(harnessRoot, "worktrees");
10262
10605
  const scanOpts = {
10263
10606
  harnessRoot,
10264
10607
  worktreesDir,
@@ -10271,7 +10614,7 @@ function runHarnessCleanup(options = {}) {
10271
10614
  };
10272
10615
  for (const raw of scanDependencyCacheCandidates(scanOpts)) {
10273
10616
  if (atSweepCap()) break;
10274
- const resolved = path51.resolve(raw.path);
10617
+ const resolved = path52.resolve(raw.path);
10275
10618
  if (processedPaths.has(resolved)) continue;
10276
10619
  processedPaths.add(resolved);
10277
10620
  const candidate = { ...raw, path: resolved };
@@ -10282,7 +10625,7 @@ function runHarnessCleanup(options = {}) {
10282
10625
  continue;
10283
10626
  }
10284
10627
  const worktreePath = worktreePathForCandidate(candidate, worktreesDir);
10285
- const indexed = index.get(path51.resolve(worktreePath)) ?? null;
10628
+ const indexed = index.get(path52.resolve(worktreePath)) ?? null;
10286
10629
  const guardReason = skipDependencyCacheRemoval({
10287
10630
  indexed,
10288
10631
  includeOrphans: true,
@@ -10306,7 +10649,7 @@ function runHarnessCleanup(options = {}) {
10306
10649
  }
10307
10650
  for (const raw of scanBuildCacheCandidates(scanOpts)) {
10308
10651
  if (atSweepCap()) break;
10309
- const resolved = path51.resolve(raw.path);
10652
+ const resolved = path52.resolve(raw.path);
10310
10653
  if (processedPaths.has(resolved)) continue;
10311
10654
  processedPaths.add(resolved);
10312
10655
  const candidate = { ...raw, path: resolved };
@@ -10317,7 +10660,7 @@ function runHarnessCleanup(options = {}) {
10317
10660
  continue;
10318
10661
  }
10319
10662
  const worktreePath = worktreePathForCandidate(candidate, worktreesDir);
10320
- const indexed = index.get(path51.resolve(worktreePath)) ?? null;
10663
+ const indexed = index.get(path52.resolve(worktreePath)) ?? null;
10321
10664
  const guardReason = skipBuildCacheRemoval({
10322
10665
  indexed,
10323
10666
  includeOrphans: true,
@@ -10347,11 +10690,11 @@ function runHarnessCleanup(options = {}) {
10347
10690
  const worktreeSeen = /* @__PURE__ */ new Set();
10348
10691
  for (const raw of worktreeCandidates) {
10349
10692
  if (atSweepCap()) break;
10350
- const resolved = path51.resolve(raw.path);
10693
+ const resolved = path52.resolve(raw.path);
10351
10694
  if (worktreeSeen.has(resolved)) continue;
10352
10695
  worktreeSeen.add(resolved);
10353
10696
  const candidate = { ...raw, path: resolved };
10354
- const indexed = index.get(path51.resolve(candidate.path)) ?? null;
10697
+ const indexed = index.get(path52.resolve(candidate.path)) ?? null;
10355
10698
  const orphanSafety = indexed ? null : assessOrphanWorktreeSafety({
10356
10699
  worktreePath: candidate.path,
10357
10700
  harnessRoot,
@@ -10361,7 +10704,7 @@ function runHarnessCleanup(options = {}) {
10361
10704
  });
10362
10705
  const guardSkip = skipWorktreeRemoval({
10363
10706
  indexed,
10364
- worktreePath: path51.resolve(candidate.path),
10707
+ worktreePath: path52.resolve(candidate.path),
10365
10708
  includeOrphans: retention.includeOrphans,
10366
10709
  worktreesAgeMs: retention.worktreesAgeMs,
10367
10710
  terminalWorktreesAgeMs: retention.terminalWorktreesAgeMs,
@@ -10393,11 +10736,11 @@ function runHarnessCleanup(options = {}) {
10393
10736
  now: paths.now
10394
10737
  })) {
10395
10738
  if (atSweepCap()) break;
10396
- const resolved = path51.resolve(raw.path);
10739
+ const resolved = path52.resolve(raw.path);
10397
10740
  if (processedPaths.has(resolved)) continue;
10398
10741
  processedPaths.add(resolved);
10399
10742
  const candidate = { ...raw, path: resolved };
10400
- const runId = candidate.runId ?? path51.basename(resolved);
10743
+ const runId = candidate.runId ?? path52.basename(resolved);
10401
10744
  const dirSkip = skipRunDirectoryRemoval({
10402
10745
  harnessRoot,
10403
10746
  runId,
@@ -10535,8 +10878,8 @@ import { mkdirSync as mkdirSync8, realpathSync } from "node:fs";
10535
10878
  import { fileURLToPath as fileURLToPath5 } from "node:url";
10536
10879
 
10537
10880
  // src/discard-disposable.ts
10538
- import { existsSync as existsSync40, rmSync as rmSync3 } from "node:fs";
10539
- import path52 from "node:path";
10881
+ import { existsSync as existsSync41, rmSync as rmSync4 } from "node:fs";
10882
+ import path53 from "node:path";
10540
10883
  function normalizeRelativePath2(value) {
10541
10884
  const normalized = value.replace(/\\/g, "/").replace(/^\.\//, "").trim();
10542
10885
  if (!normalized || normalized.startsWith("/") || normalized.includes("..")) {
@@ -10558,18 +10901,18 @@ function discardDisposableArtifacts(args) {
10558
10901
  if (paths.length === 0) {
10559
10902
  return { ok: false, removed: [], reason: "requires at least one --path" };
10560
10903
  }
10561
- const worktreeRoot = path52.resolve(worker.worktreePath);
10904
+ const worktreeRoot = path53.resolve(worker.worktreePath);
10562
10905
  const removed = [];
10563
10906
  for (const raw of paths) {
10564
10907
  const rel = normalizeRelativePath2(raw);
10565
- const abs = path52.resolve(worktreeRoot, rel);
10566
- if (!abs.startsWith(worktreeRoot + path52.sep) && abs !== worktreeRoot) {
10908
+ const abs = path53.resolve(worktreeRoot, rel);
10909
+ if (!abs.startsWith(worktreeRoot + path53.sep) && abs !== worktreeRoot) {
10567
10910
  return { ok: false, removed, reason: `path escapes worktree: ${raw}` };
10568
10911
  }
10569
- if (!existsSync40(abs)) {
10912
+ if (!existsSync41(abs)) {
10570
10913
  return { ok: false, removed, reason: `path not found: ${raw}` };
10571
10914
  }
10572
- rmSync3(abs, { recursive: true, force: true });
10915
+ rmSync4(abs, { recursive: true, force: true });
10573
10916
  removed.push(rel);
10574
10917
  }
10575
10918
  const prior = Array.isArray(worker.disposableArtifactsRemoved) ? worker.disposableArtifactsRemoved.filter((p) => typeof p === "string") : [];
@@ -10588,10 +10931,49 @@ function discardDisposableCli(args) {
10588
10931
  if (!result.ok) process.exit(1);
10589
10932
  }
10590
10933
 
10934
+ // src/daemon-box-identity.ts
10935
+ import os8 from "node:os";
10936
+ function emitDaemonIdentityMessage(level, message) {
10937
+ console.error(JSON.stringify({ event: "daemon_identity", level, message }));
10938
+ }
10939
+ function validateDaemonInstallIdentity(config = loadUserConfig(), env = process.env) {
10940
+ const box = resolveBoxIdentity(env, config);
10941
+ const cap = resolveWorkerCap({
10942
+ config,
10943
+ totalMemBytes: os8.totalmem(),
10944
+ env
10945
+ });
10946
+ const warnings = [...box.warnings];
10947
+ const errors = [];
10948
+ if (!config.boxKind?.trim() && !env.KYNVER_BOX_KIND?.trim()) {
10949
+ warnings.push(
10950
+ "boxKind is not persisted in ~/.kynver/config.json \u2014 run `kynver setup --box-kind forge|ghost` so Command Center attributes snapshots to the correct pool"
10951
+ );
10952
+ }
10953
+ if (box.slugInferenceBlocked) {
10954
+ const strict = env.KYNVER_DAEMON_STRICT_IDENTITY === "1" || env.KYNVER_DAEMON_STRICT_IDENTITY === "true";
10955
+ const msg = "ambiguous box identity: KYNVER_AGENT_OS_SLUG is set without KYNVER_BOX_KIND or config.boxKind; treating this host as forge";
10956
+ if (strict) errors.push(msg);
10957
+ else warnings.push(msg);
10958
+ }
10959
+ const ok = errors.length === 0;
10960
+ for (const warning of warnings) emitDaemonIdentityMessage("warn", warning);
10961
+ for (const error of errors) emitDaemonIdentityMessage("error", error);
10962
+ return {
10963
+ ok,
10964
+ box,
10965
+ workerCapSource: cap.workerCapSource,
10966
+ maxConcurrentWorkers: cap.configuredMaxWorkers ?? cap.autoCap,
10967
+ autoCap: cap.autoCap,
10968
+ warnings,
10969
+ errors
10970
+ };
10971
+ }
10972
+
10591
10973
  // src/cron/cron-env.ts
10592
- import { existsSync as existsSync41 } from "node:fs";
10974
+ import { existsSync as existsSync42 } from "node:fs";
10593
10975
  import { homedir as homedir14 } from "node:os";
10594
- import path53 from "node:path";
10976
+ import path54 from "node:path";
10595
10977
  function envFlag3(name, defaultValue) {
10596
10978
  const raw = process.env[name]?.trim().toLowerCase();
10597
10979
  if (!raw) return defaultValue;
@@ -10607,7 +10989,7 @@ function envInt(name, fallback, min = 1) {
10607
10989
  function defaultKynverCronStorePath() {
10608
10990
  const explicit = process.env.KYNVER_CRON_STORE_PATH?.trim() || process.env.OPENCLAW_CRON_STORE_PATH?.trim();
10609
10991
  if (explicit) return explicit;
10610
- return path53.join(homedir14(), ".kynver", "agent-os-cron.json");
10992
+ return path54.join(homedir14(), ".kynver", "agent-os-cron.json");
10611
10993
  }
10612
10994
  function defaultKynverCronStatePath(storePath = defaultKynverCronStorePath()) {
10613
10995
  const explicit = process.env.KYNVER_CRON_TICK_STATE_PATH?.trim();
@@ -10627,7 +11009,7 @@ function resolveKynverCronEnv() {
10627
11009
  const fireBaseUrl = resolveKynverCronFireBaseUrl();
10628
11010
  const secret = resolveKynverCronSecret();
10629
11011
  const credsReady = Boolean(fireBaseUrl && secret);
10630
- const storeExists = existsSync41(storePath);
11012
+ const storeExists = existsSync42(storePath);
10631
11013
  const defaultEnabled = credsReady && (storeExists || envFlag3("KYNVER_CRON_TICK_FORCE", false));
10632
11014
  return {
10633
11015
  storePath,
@@ -10680,10 +11062,10 @@ async function fireKynverCronJob(input) {
10680
11062
  }
10681
11063
 
10682
11064
  // src/cron/cron-lock.ts
10683
- import { closeSync as closeSync6, existsSync as existsSync42, openSync as openSync6, readFileSync as readFileSync15, unlinkSync as unlinkSync3, writeFileSync as writeFileSync4 } from "node:fs";
11065
+ import { closeSync as closeSync6, existsSync as existsSync43, openSync as openSync6, readFileSync as readFileSync15, unlinkSync as unlinkSync3, writeFileSync as writeFileSync4 } from "node:fs";
10684
11066
  var STALE_LOCK_MS = 10 * 6e4;
10685
11067
  function readLockInfo(lockPath) {
10686
- if (!existsSync42(lockPath)) return null;
11068
+ if (!existsSync43(lockPath)) return null;
10687
11069
  try {
10688
11070
  const parsed = JSON.parse(readFileSync15(lockPath, "utf8"));
10689
11071
  if (typeof parsed.pid === "number" && typeof parsed.at === "string") return parsed;
@@ -10701,14 +11083,14 @@ function lockIsStale(lockPath) {
10701
11083
  return Date.now() - atMs > STALE_LOCK_MS;
10702
11084
  }
10703
11085
  function tryAcquireCronTickLock(lockPath) {
10704
- if (existsSync42(lockPath) && !lockIsStale(lockPath)) {
11086
+ if (existsSync43(lockPath) && !lockIsStale(lockPath)) {
10705
11087
  const info = readLockInfo(lockPath);
10706
11088
  return {
10707
11089
  acquired: false,
10708
11090
  reason: info ? `held by pid ${info.pid}` : "held by another process"
10709
11091
  };
10710
11092
  }
10711
- if (existsSync42(lockPath)) {
11093
+ if (existsSync43(lockPath)) {
10712
11094
  try {
10713
11095
  unlinkSync3(lockPath);
10714
11096
  } catch {
@@ -10849,7 +11231,7 @@ async function loadCronJobs(storePath = defaultKynverCronStorePath()) {
10849
11231
  // src/cron/cron-tick-state.ts
10850
11232
  import { randomBytes } from "node:crypto";
10851
11233
  import { promises as fs4 } from "node:fs";
10852
- import path54 from "node:path";
11234
+ import path55 from "node:path";
10853
11235
  var EMPTY = { version: 1, jobs: {} };
10854
11236
  async function readFileIfExists2(filePath) {
10855
11237
  try {
@@ -10876,7 +11258,7 @@ async function loadCronTickState(statePath) {
10876
11258
  return parseCronTickState(raw);
10877
11259
  }
10878
11260
  async function writeStateAtomic(statePath, state) {
10879
- await fs4.mkdir(path54.dirname(statePath), { recursive: true });
11261
+ await fs4.mkdir(path55.dirname(statePath), { recursive: true });
10880
11262
  const suffix = randomBytes(6).toString("hex");
10881
11263
  const tmp = `${statePath}.tmp-${process.pid}-${Date.now()}-${suffix}`;
10882
11264
  await fs4.writeFile(tmp, `${JSON.stringify(state, null, 2)}
@@ -11053,7 +11435,7 @@ async function runKynverCronTick(opts = {}) {
11053
11435
  }
11054
11436
 
11055
11437
  // src/pipeline-tick.ts
11056
- import path56 from "node:path";
11438
+ import path57 from "node:path";
11057
11439
 
11058
11440
  // src/pipeline-dispatch.ts
11059
11441
  var RESERVED_REVIEW_STARTS = 1;
@@ -11184,7 +11566,7 @@ function resolvePipelineMaxStarts(resourceGate, operatorTick) {
11184
11566
  }
11185
11567
 
11186
11568
  // src/plan-progress-daemon-sync.ts
11187
- import path55 from "node:path";
11569
+ import path56 from "node:path";
11188
11570
 
11189
11571
  // src/plan-progress-sync.ts
11190
11572
  async function syncPlanProgress(args) {
@@ -11208,7 +11590,7 @@ async function syncActiveWorkerPlanProgress(runId, args) {
11208
11590
  const outcomes = [];
11209
11591
  for (const name of Object.keys(run.workers || {})) {
11210
11592
  const worker = readJson(
11211
- path55.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
11593
+ path56.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
11212
11594
  void 0
11213
11595
  );
11214
11596
  if (!worker?.dispatched || !worker.taskId) continue;
@@ -11237,7 +11619,8 @@ async function syncActiveWorkerPlanProgress(runId, args) {
11237
11619
 
11238
11620
  // src/workspace-runtime-config.ts
11239
11621
  function shouldApplyWorkspaceRuntimePreferences(env = process.env) {
11240
- return resolveBoxKindFromEnv(env) !== "forge";
11622
+ const config = loadUserConfig();
11623
+ return resolveBoxKindFromConfig(config, env) !== "forge";
11241
11624
  }
11242
11625
  function configuredMaxWorkersOverrideForBox(preferences, env = process.env) {
11243
11626
  if (!shouldApplyWorkspaceRuntimePreferences(env)) return void 0;
@@ -11269,7 +11652,7 @@ async function completeFinishedWorkers(runId, args) {
11269
11652
  const outcomes = [];
11270
11653
  for (const name of Object.keys(run.workers || {})) {
11271
11654
  const worker = readJson(
11272
- path56.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
11655
+ path57.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
11273
11656
  void 0
11274
11657
  );
11275
11658
  if (!worker?.taskId || worker.localOnly) continue;
@@ -11308,7 +11691,10 @@ async function postOperatorTick(agentOsId, runId, resourceGate, args, harnessCle
11308
11691
  ingestHarness: true,
11309
11692
  harnessBoardSnapshot: buildRunBoard(runId),
11310
11693
  resourceGate,
11311
- boxResourceSnapshot: buildBoxResourceSnapshotFromGate(resourceGate, { harnessRunId: runId }),
11694
+ boxResourceSnapshot: buildBoxResourceSnapshotFromGate(resourceGate, {
11695
+ harnessRunId: runId,
11696
+ boxKind: resolveBoxKindFromConfig(loadUserConfig())
11697
+ }),
11312
11698
  packageVersions,
11313
11699
  ...harnessCleanup ? { harnessCleanup } : {},
11314
11700
  runnerPresence: resolveRunnerPresencePayload({ runId }),
@@ -11426,7 +11812,30 @@ async function runDaemon(args) {
11426
11812
  process.on("SIGTERM", () => {
11427
11813
  stopping = true;
11428
11814
  });
11429
- console.error(JSON.stringify({ event: "daemon_start", runId, agentOsId, execute, intervalMs }));
11815
+ const identity = validateDaemonInstallIdentity(loadUserConfig());
11816
+ if (!identity.ok) {
11817
+ console.error(
11818
+ JSON.stringify({
11819
+ event: "daemon_start_blocked",
11820
+ runId,
11821
+ agentOsId,
11822
+ errors: identity.errors
11823
+ })
11824
+ );
11825
+ process.exit(1);
11826
+ }
11827
+ console.error(
11828
+ JSON.stringify({
11829
+ event: "daemon_start",
11830
+ runId,
11831
+ agentOsId,
11832
+ execute,
11833
+ intervalMs,
11834
+ boxKind: identity.box.boxKind,
11835
+ workerCapSource: identity.workerCapSource,
11836
+ maxConcurrentWorkers: identity.maxConcurrentWorkers
11837
+ })
11838
+ );
11430
11839
  const cronEnv = resolveKynverCronEnv();
11431
11840
  while (!stopping) {
11432
11841
  try {
@@ -11457,7 +11866,7 @@ async function runDaemon(args) {
11457
11866
  }
11458
11867
 
11459
11868
  // src/plan-progress.ts
11460
- import path57 from "node:path";
11869
+ import path59 from "node:path";
11461
11870
 
11462
11871
  // src/bounded-build/constants.ts
11463
11872
  var DEFAULT_BUILD_MEM_BUDGET_BYTES = 1536 * 1024 * 1024;
@@ -11495,7 +11904,7 @@ function formatNodeOptionsFlag(mb = resolveNodeOldSpaceSizeMb()) {
11495
11904
  }
11496
11905
 
11497
11906
  // src/bounded-build/systemd-wrap.ts
11498
- import { spawnSync as spawnSync5 } from "node:child_process";
11907
+ import { spawnSync as spawnSync6 } from "node:child_process";
11499
11908
  var systemdAvailableCache;
11500
11909
  function isSystemdRunAvailable() {
11501
11910
  if (process.env.KYNVER_BUILD_SKIP_SYSTEMD === "1" || process.env.KYNVER_BUILD_SKIP_SYSTEMD === "true") {
@@ -11506,7 +11915,7 @@ function isSystemdRunAvailable() {
11506
11915
  systemdAvailableCache = false;
11507
11916
  return false;
11508
11917
  }
11509
- const res = spawnSync5("systemd-run", ["--version"], { encoding: "utf8", stdio: ["ignore", "ignore", "pipe"] });
11918
+ const res = spawnSync6("systemd-run", ["--version"], { encoding: "utf8", stdio: ["ignore", "ignore", "pipe"] });
11510
11919
  systemdAvailableCache = res.status === 0;
11511
11920
  return systemdAvailableCache;
11512
11921
  }
@@ -11530,18 +11939,18 @@ function buildSystemdRunArgv(opts) {
11530
11939
  }
11531
11940
 
11532
11941
  // src/bounded-build/admission.ts
11533
- import { spawnSync as spawnSync6 } from "node:child_process";
11534
- function positiveInt3(value, fallback) {
11942
+ import { spawnSync as spawnSync7 } from "node:child_process";
11943
+ function positiveInt4(value, fallback) {
11535
11944
  const n = Number(value);
11536
11945
  if (!Number.isFinite(n) || n <= 0) return fallback;
11537
11946
  return Math.floor(n);
11538
11947
  }
11539
11948
  function resolveBuildAdmissionConfig(config = loadUserConfig()) {
11540
- const envBudget = process.env.KYNVER_BUILD_MEM_BUDGET_BYTES ? positiveInt3(process.env.KYNVER_BUILD_MEM_BUDGET_BYTES, DEFAULT_BUILD_MEM_BUDGET_BYTES) : void 0;
11541
- const envReserve = process.env.KYNVER_BUILD_MEM_RESERVE_BYTES ? positiveInt3(process.env.KYNVER_BUILD_MEM_RESERVE_BYTES, DEFAULT_BUILD_MEM_RESERVE_BYTES) : void 0;
11949
+ const envBudget = process.env.KYNVER_BUILD_MEM_BUDGET_BYTES ? positiveInt4(process.env.KYNVER_BUILD_MEM_BUDGET_BYTES, DEFAULT_BUILD_MEM_BUDGET_BYTES) : void 0;
11950
+ const envReserve = process.env.KYNVER_BUILD_MEM_RESERVE_BYTES ? positiveInt4(process.env.KYNVER_BUILD_MEM_RESERVE_BYTES, DEFAULT_BUILD_MEM_RESERVE_BYTES) : void 0;
11542
11951
  return {
11543
- perBuildBudgetBytes: envBudget ?? positiveInt3(config.perWorkerMemBytes, DEFAULT_BUILD_MEM_BUDGET_BYTES),
11544
- reserveBytes: envReserve ?? positiveInt3(config.memReserveBytes, DEFAULT_BUILD_MEM_RESERVE_BYTES)
11952
+ perBuildBudgetBytes: envBudget ?? positiveInt4(config.perWorkerMemBytes, DEFAULT_BUILD_MEM_BUDGET_BYTES),
11953
+ reserveBytes: envReserve ?? positiveInt4(config.memReserveBytes, DEFAULT_BUILD_MEM_RESERVE_BYTES)
11545
11954
  };
11546
11955
  }
11547
11956
  var activeBuilds = 0;
@@ -11566,7 +11975,7 @@ function assessBuildAdmission(opts = {}) {
11566
11975
  }
11567
11976
  function sleepMs2(ms) {
11568
11977
  if (ms <= 0) return;
11569
- spawnSync6(process.execPath, ["-e", `const d=Date.now()+${Math.floor(ms)};while(Date.now()<d);`], {
11978
+ spawnSync7(process.execPath, ["-e", `const d=Date.now()+${Math.floor(ms)};while(Date.now()<d);`], {
11570
11979
  stdio: "ignore"
11571
11980
  });
11572
11981
  }
@@ -11587,7 +11996,28 @@ function waitForBuildAdmission(timeoutMs, pollMs = 2e3, opts = {}) {
11587
11996
  }
11588
11997
 
11589
11998
  // src/bounded-build/exec.ts
11590
- import { spawnSync as spawnSync7 } from "node:child_process";
11999
+ import { spawnSync as spawnSync8 } from "node:child_process";
12000
+
12001
+ // src/harness-worktree-build-guard.ts
12002
+ import path58 from "node:path";
12003
+ function isPathUnderHarnessWorktree(cwd) {
12004
+ const worktreesDir = harnessWorktreesDir(resolveHarnessRoot());
12005
+ const rel = path58.relative(worktreesDir, path58.resolve(cwd));
12006
+ return rel.length > 0 && !rel.startsWith("..") && !path58.isAbsolute(rel);
12007
+ }
12008
+ function assessHarnessWorktreeBuildGuard(cwd) {
12009
+ if (!isPathUnderHarnessWorktree(cwd)) return { ok: true };
12010
+ const uid = typeof process.getuid === "function" ? process.getuid() : null;
12011
+ if (uid === 0) {
12012
+ return {
12013
+ ok: false,
12014
+ reason: "Refusing build/install as root inside a harness worktree \u2014 generated caches become root-owned and block daemon cleanup. Run kynver daemon and workers as the harness owner (never sudo kynver)."
12015
+ };
12016
+ }
12017
+ return { ok: true };
12018
+ }
12019
+
12020
+ // src/bounded-build/exec.ts
11591
12021
  function envArgv(env) {
11592
12022
  const out = [];
11593
12023
  for (const [key, value] of Object.entries(env)) {
@@ -11597,7 +12027,7 @@ function envArgv(env) {
11597
12027
  return out;
11598
12028
  }
11599
12029
  function runSpawn(argv, opts) {
11600
- const res = spawnSync7(argv[0], argv.slice(1), {
12030
+ const res = spawnSync8(argv[0], argv.slice(1), {
11601
12031
  cwd: opts.cwd,
11602
12032
  env: opts.env,
11603
12033
  encoding: "utf8",
@@ -11627,6 +12057,20 @@ function runBoundedBuildCheck(input) {
11627
12057
  command: input.command
11628
12058
  };
11629
12059
  }
12060
+ const worktreeGuard = assessHarnessWorktreeBuildGuard(input.cwd);
12061
+ if (!worktreeGuard.ok) {
12062
+ return {
12063
+ ok: false,
12064
+ exitCode: 1,
12065
+ stdout: "",
12066
+ stderr: worktreeGuard.reason,
12067
+ admitted: true,
12068
+ wrappedWithSystemd: false,
12069
+ nodeOptionsFlag: formatNodeOptionsFlag(),
12070
+ admission,
12071
+ command: input.command
12072
+ };
12073
+ }
11630
12074
  const env = mergeNodeOptionsForBuildCheck({ ...process.env, ...input.env });
11631
12075
  const nodeOptionsFlag = formatNodeOptionsFlag();
11632
12076
  const useSystemd = isSystemdRunAvailable();
@@ -11744,7 +12188,7 @@ async function emitPlanProgress(args) {
11744
12188
  }
11745
12189
  function verifyPlanLocal(args) {
11746
12190
  const worktree = required(args.worktree ? String(args.worktree) : void 0, "worktree");
11747
- const cwd = path57.resolve(worktree);
12191
+ const cwd = path59.resolve(worktree);
11748
12192
  const summary = runHarnessVerifyCommands(cwd);
11749
12193
  const emitJson = args.json === true || args.json === "true";
11750
12194
  const payload = { passed: summary.passed, worktree: cwd, steps: summary.steps };
@@ -11793,9 +12237,9 @@ async function verifyPlan(args) {
11793
12237
  }
11794
12238
 
11795
12239
  // src/harness-verify-cli.ts
11796
- import path58 from "node:path";
12240
+ import path60 from "node:path";
11797
12241
  function runHarnessVerifyCli(args) {
11798
- const cwd = path58.resolve(required(args.worktree ? String(args.worktree) : void 0, "worktree"));
12242
+ const cwd = path60.resolve(required(args.worktree ? String(args.worktree) : void 0, "worktree"));
11799
12243
  const emitJson = args.json === true || args.json === "true" || args.emitJson === true || args.emitJson === "true";
11800
12244
  const commands = [];
11801
12245
  const rawCmd = args.command;
@@ -12231,7 +12675,7 @@ ${text.slice(0, 800)}`,
12231
12675
  }
12232
12676
 
12233
12677
  // src/monitor/monitor.service.ts
12234
- import path60 from "node:path";
12678
+ import path62 from "node:path";
12235
12679
 
12236
12680
  // src/monitor/monitor.classify.ts
12237
12681
  function classifyWorkerHealth(input) {
@@ -12283,11 +12727,11 @@ function classifyWorkerHealth(input) {
12283
12727
  }
12284
12728
 
12285
12729
  // src/monitor/monitor.store.ts
12286
- import { existsSync as existsSync43, mkdirSync as mkdirSync7, readdirSync as readdirSync14, unlinkSync as unlinkSync4 } from "node:fs";
12287
- import path59 from "node:path";
12730
+ import { existsSync as existsSync44, mkdirSync as mkdirSync7, readdirSync as readdirSync15, unlinkSync as unlinkSync4 } from "node:fs";
12731
+ import path61 from "node:path";
12288
12732
  function monitorsDir() {
12289
12733
  const { harnessRoot } = getHarnessPaths();
12290
- const dir = path59.join(harnessRoot, "monitors");
12734
+ const dir = path61.join(harnessRoot, "monitors");
12291
12735
  mkdirSync7(dir, { recursive: true });
12292
12736
  return dir;
12293
12737
  }
@@ -12295,7 +12739,7 @@ function monitorIdFor(runId, workerName) {
12295
12739
  return workerName ? `${safeSlug(runId)}--${safeSlug(workerName)}` : safeSlug(runId);
12296
12740
  }
12297
12741
  function monitorPath(monitorId) {
12298
- return path59.join(monitorsDir(), `${monitorId}.json`);
12742
+ return path61.join(monitorsDir(), `${monitorId}.json`);
12299
12743
  }
12300
12744
  function loadMonitorSession(monitorId) {
12301
12745
  return readJson(monitorPath(monitorId), void 0);
@@ -12305,18 +12749,18 @@ function saveMonitorSession(session) {
12305
12749
  }
12306
12750
  function deleteMonitorSession(monitorId) {
12307
12751
  const file = monitorPath(monitorId);
12308
- if (!existsSync43(file)) return false;
12752
+ if (!existsSync44(file)) return false;
12309
12753
  unlinkSync4(file);
12310
12754
  return true;
12311
12755
  }
12312
12756
  function listMonitorSessions() {
12313
12757
  const dir = monitorsDir();
12314
- if (!existsSync43(dir)) return [];
12758
+ if (!existsSync44(dir)) return [];
12315
12759
  const entries = [];
12316
- for (const name of readdirSync14(dir)) {
12760
+ for (const name of readdirSync15(dir)) {
12317
12761
  if (!name.endsWith(".json")) continue;
12318
12762
  const session = readJson(
12319
- path59.join(dir, name),
12763
+ path61.join(dir, name),
12320
12764
  void 0
12321
12765
  );
12322
12766
  if (!session?.monitorId) continue;
@@ -12407,7 +12851,7 @@ async function fetchTaskLeasesForWorkers(input) {
12407
12851
  // src/monitor/monitor.service.ts
12408
12852
  function workerRecord2(runId, name) {
12409
12853
  return readJson(
12410
- path60.join(runDirectory(runId), "workers", safeSlug(name), "worker.json"),
12854
+ path62.join(runDirectory(runId), "workers", safeSlug(name), "worker.json"),
12411
12855
  void 0
12412
12856
  );
12413
12857
  }
@@ -12613,18 +13057,18 @@ async function runMonitorLoop(args) {
12613
13057
 
12614
13058
  // src/monitor/monitor-spawn.ts
12615
13059
  import { spawn as spawn6 } from "node:child_process";
12616
- import { closeSync as closeSync7, existsSync as existsSync44, openSync as openSync7 } from "node:fs";
12617
- import path61 from "node:path";
13060
+ import { closeSync as closeSync7, existsSync as existsSync45, openSync as openSync7 } from "node:fs";
13061
+ import path63 from "node:path";
12618
13062
  import { fileURLToPath as fileURLToPath4 } from "node:url";
12619
13063
  function resolveDefaultCliPath2() {
12620
- return path61.join(fileURLToPath4(new URL(".", import.meta.url)), "cli.js");
13064
+ return path63.join(fileURLToPath4(new URL(".", import.meta.url)), "cli.js");
12621
13065
  }
12622
13066
  function spawnMonitorSidecar(opts) {
12623
13067
  const cliPath = opts.cliPath ?? resolveDefaultCliPath2();
12624
- if (!existsSync44(cliPath)) return void 0;
13068
+ if (!existsSync45(cliPath)) return void 0;
12625
13069
  const monitorId = monitorIdFor(opts.runId, opts.workerName);
12626
13070
  const { harnessRoot } = getHarnessPaths();
12627
- const logPath = path61.join(harnessRoot, "monitors", `${monitorId}.log`);
13071
+ const logPath = path63.join(harnessRoot, "monitors", `${monitorId}.log`);
12628
13072
  let logFd;
12629
13073
  try {
12630
13074
  logFd = openSync7(logPath, "a");
@@ -12744,7 +13188,7 @@ async function monitorTickCli(args) {
12744
13188
  }
12745
13189
 
12746
13190
  // src/post-restart-unblock.ts
12747
- import path62 from "node:path";
13191
+ import path64 from "node:path";
12748
13192
  function skip(runId, worker, taskId, agentOsId, leaseOwner, reason) {
12749
13193
  return { runId, worker, taskId, agentOsId, leaseOwner, action: "skipped", reason };
12750
13194
  }
@@ -12757,7 +13201,7 @@ async function postRestartUnblock(args) {
12757
13201
  const errors = [];
12758
13202
  for (const run of listRunRecords()) {
12759
13203
  for (const name of Object.keys(run.workers ?? {})) {
12760
- const workerPath = path62.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json");
13204
+ const workerPath = path64.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json");
12761
13205
  const worker = readJson(workerPath, void 0);
12762
13206
  if (!worker) {
12763
13207
  skipped.push(skip(run.id, name, "", "", "", "worker.json missing"));
@@ -12869,9 +13313,9 @@ async function postRestartUnblockCli(args) {
12869
13313
  }
12870
13314
 
12871
13315
  // src/default-repo-cli.ts
12872
- import path63 from "node:path";
13316
+ import path65 from "node:path";
12873
13317
  import { homedir as homedir15 } from "node:os";
12874
- var CONFIG_FILE2 = path63.join(homedir15(), ".kynver", "config.json");
13318
+ var CONFIG_FILE2 = path65.join(homedir15(), ".kynver", "config.json");
12875
13319
  function ensureDefaultRepo(opts) {
12876
13320
  const existing = loadUserConfig();
12877
13321
  const resolved = resolveDefaultRepo({ ...opts, config: existing });
@@ -12952,16 +13396,16 @@ function summarizeResolvedDefaultRepo(resolved) {
12952
13396
  }
12953
13397
 
12954
13398
  // src/doctor/runtime-takeover.ts
12955
- import path65 from "node:path";
13399
+ import path67 from "node:path";
12956
13400
 
12957
13401
  // src/doctor/runtime-takeover.probes.ts
12958
- import { accessSync, constants, existsSync as existsSync45, readFileSync as readFileSync17 } from "node:fs";
13402
+ import { accessSync, constants, existsSync as existsSync46, readFileSync as readFileSync17 } from "node:fs";
12959
13403
  import { homedir as homedir16 } from "node:os";
12960
- import path64 from "node:path";
12961
- import { spawnSync as spawnSync8 } from "node:child_process";
13404
+ import path66 from "node:path";
13405
+ import { spawnSync as spawnSync9 } from "node:child_process";
12962
13406
  function captureCommand(bin, args) {
12963
13407
  try {
12964
- const res = spawnSync8(bin, args, { encoding: "utf8" });
13408
+ const res = spawnSync9(bin, args, { encoding: "utf8" });
12965
13409
  const stdout = (res.stdout || "").trim();
12966
13410
  const stderr = (res.stderr || "").trim();
12967
13411
  const ok = res.status === 0;
@@ -12986,7 +13430,7 @@ function tokenPrefix(token) {
12986
13430
  return trimmed.length <= 12 ? `${trimmed}\u2026` : `${trimmed.slice(0, 12)}\u2026`;
12987
13431
  }
12988
13432
  function isWritable(target) {
12989
- if (!existsSync45(target)) return false;
13433
+ if (!existsSync46(target)) return false;
12990
13434
  try {
12991
13435
  accessSync(target, constants.W_OK);
12992
13436
  return true;
@@ -12999,11 +13443,11 @@ var defaultRuntimeTakeoverProbes = {
12999
13443
  commandOnPath: (bin) => captureCommand(process.platform === "win32" ? "where" : "which", [bin]),
13000
13444
  kynverVersion: (bin) => captureCommand(bin, ["--version"]),
13001
13445
  loadConfig: () => loadUserConfig(),
13002
- configFilePath: () => path64.join(homedir16(), ".kynver", "config.json"),
13003
- credentialsFilePath: () => path64.join(homedir16(), ".kynver", "credentials"),
13446
+ configFilePath: () => path66.join(homedir16(), ".kynver", "config.json"),
13447
+ credentialsFilePath: () => path66.join(homedir16(), ".kynver", "credentials"),
13004
13448
  readCredentials: () => {
13005
- const credPath = path64.join(homedir16(), ".kynver", "credentials");
13006
- if (!existsSync45(credPath)) {
13449
+ const credPath = path66.join(homedir16(), ".kynver", "credentials");
13450
+ if (!existsSync46(credPath)) {
13007
13451
  return { hasApiKey: false };
13008
13452
  }
13009
13453
  try {
@@ -13037,8 +13481,8 @@ var defaultRuntimeTakeoverProbes = {
13037
13481
  })()
13038
13482
  }),
13039
13483
  harnessRoot: () => resolveHarnessRoot(),
13040
- legacyOpenclawHarnessRoot: () => path64.join(homedir16(), ".openclaw", "harness"),
13041
- pathExists: (target) => existsSync45(target),
13484
+ legacyOpenclawHarnessRoot: () => path66.join(homedir16(), ".openclaw", "harness"),
13485
+ pathExists: (target) => existsSync46(target),
13042
13486
  pathWritable: (target) => isWritable(target),
13043
13487
  vercelVersion: () => captureCommand("vercel", ["--version"]),
13044
13488
  vercelWhoami: () => captureCommand("vercel", ["whoami"])
@@ -13437,8 +13881,8 @@ function assessVercelCli(probes) {
13437
13881
  }
13438
13882
  function assessHarnessDirs(probes) {
13439
13883
  const harnessRoot = probes.harnessRoot();
13440
- const runsDir = path65.join(harnessRoot, "runs");
13441
- const worktreesDir = path65.join(harnessRoot, "worktrees");
13884
+ const runsDir = path67.join(harnessRoot, "runs");
13885
+ const worktreesDir = path67.join(harnessRoot, "worktrees");
13442
13886
  const displayHarnessRoot = redactHomePath(harnessRoot);
13443
13887
  const displayRunsDir = redactHomePath(runsDir);
13444
13888
  const displayWorktreesDir = redactHomePath(worktreesDir);
@@ -13702,9 +14146,9 @@ function applySchedulerCutoverAttestation(config) {
13702
14146
  }
13703
14147
 
13704
14148
  // src/scheduler-cutover-cli.ts
13705
- import path66 from "node:path";
14149
+ import path68 from "node:path";
13706
14150
  import { homedir as homedir17 } from "node:os";
13707
- var CONFIG_FILE3 = path66.join(homedir17(), ".kynver", "config.json");
14151
+ var CONFIG_FILE3 = path68.join(homedir17(), ".kynver", "config.json");
13708
14152
  function runSchedulerCutoverCheckCli(json = false) {
13709
14153
  const config = loadUserConfig();
13710
14154
  const report = assessSchedulerCutover(config);
@@ -13857,7 +14301,7 @@ function usage(code = 0) {
13857
14301
  "Usage:",
13858
14302
  " kynver login --api-key KEY",
13859
14303
  " kynver runner credential [--agent-os-id ID] [--base-url URL]",
13860
- " kynver setup [--api-base-url URL] [--agent-os-id ID] [--agent-os-slug SLUG] [--repo PATH] [--discover-repo] [--max-workers N] [--provider claude|cursor]",
14304
+ " kynver setup [--api-base-url URL] [--agent-os-id ID] [--agent-os-slug SLUG] [--box-kind forge|ghost] [--repo PATH] [--discover-repo] [--max-workers N] [--provider claude|cursor]",
13861
14305
  " kynver daemon --run RUN_ID --agent-os-id AOS_ID [--execute] [--interval-ms MS]",
13862
14306
  " kynver run create [--repo /path/repo] [--name name] [--base origin/main]",
13863
14307
  " kynver run list",
@@ -14121,7 +14565,7 @@ function pickVercelStatusContext(statuses) {
14121
14565
  }
14122
14566
 
14123
14567
  // src/vercel/vercel-evidence.ts
14124
- import { spawnSync as spawnSync9 } from "node:child_process";
14568
+ import { spawnSync as spawnSync10 } from "node:child_process";
14125
14569
  var DEFAULT_INSPECT_WAIT_SECONDS = 120;
14126
14570
  function mapGitHubStateToEvidence(state) {
14127
14571
  if (state === "success") return "ready";
@@ -14196,7 +14640,7 @@ function defaultRunVercelInspect(target, waitSeconds) {
14196
14640
  };
14197
14641
  }
14198
14642
  const args = ["inspect", target, "--wait", String(waitSeconds)];
14199
- const result = spawnSync9("vercel", args, {
14643
+ const result = spawnSync10("vercel", args, {
14200
14644
  encoding: "utf8",
14201
14645
  stdio: ["ignore", "pipe", "pipe"]
14202
14646
  });