@kynver-app/runtime 0.1.91 → 0.1.93

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -5,9 +5,9 @@ import { mkdirSync as mkdirSync8, realpathSync } from "node:fs";
5
5
  import { fileURLToPath as fileURLToPath5 } from "node:url";
6
6
 
7
7
  // src/config.ts
8
- import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "node:fs";
9
- import { homedir as homedir3 } from "node:os";
10
- import path4 from "node:path";
8
+ import { existsSync as existsSync9, mkdirSync as mkdirSync2, readFileSync as readFileSync7, writeFileSync as writeFileSync2 } from "node:fs";
9
+ import { homedir as homedir4, totalmem } from "node:os";
10
+ import path8 from "node:path";
11
11
 
12
12
  // src/default-repo-discovery.ts
13
13
  import { existsSync as existsSync2, readFileSync as readFileSync2 } from "node:fs";
@@ -381,768 +381,440 @@ function discoverDefaultRepo(opts) {
381
381
  return discoverDefaultRepoCandidates(opts)[0] ?? null;
382
382
  }
383
383
 
384
- // src/config.ts
385
- var CONFIG_DIR = path4.join(homedir3(), ".kynver");
386
- var CONFIG_FILE = path4.join(CONFIG_DIR, "config.json");
387
- var CREDENTIALS_FILE = path4.join(CONFIG_DIR, "credentials");
388
- function loadUserConfig() {
389
- if (!existsSync3(CONFIG_FILE)) return {};
390
- try {
391
- return JSON.parse(readFileSync3(CONFIG_FILE, "utf8"));
392
- } catch {
393
- return {};
384
+ // src/box-identity.ts
385
+ function normalizeWorkerPoolBoxKind(raw) {
386
+ const kind = (raw ?? "").trim().toLowerCase();
387
+ if (kind === "ghost" || kind === "forge") return kind;
388
+ if (kind.includes("forge")) return "forge";
389
+ if (kind.includes("ghost") || kind.includes("openclaw")) return "ghost";
390
+ return "forge";
391
+ }
392
+ function trimEnv(env, key) {
393
+ const value = env[key]?.trim();
394
+ return value || null;
395
+ }
396
+ function resolveBoxIdentity(env = process.env, config = {}) {
397
+ const warnings = [];
398
+ const configKind = config.boxKind?.trim();
399
+ if (configKind) {
400
+ return {
401
+ boxKind: normalizeWorkerPoolBoxKind(configKind),
402
+ source: "config",
403
+ slugInferenceBlocked: false,
404
+ warnings
405
+ };
406
+ }
407
+ const envKind = trimEnv(env, "KYNVER_BOX_KIND");
408
+ if (envKind) {
409
+ return {
410
+ boxKind: normalizeWorkerPoolBoxKind(envKind),
411
+ source: "env",
412
+ slugInferenceBlocked: false,
413
+ warnings
414
+ };
415
+ }
416
+ const agentOsSlug = trimEnv(env, "KYNVER_AGENT_OS_SLUG");
417
+ if (agentOsSlug) {
418
+ warnings.push(
419
+ `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)`
420
+ );
394
421
  }
422
+ return {
423
+ boxKind: "forge",
424
+ source: "default",
425
+ slugInferenceBlocked: Boolean(agentOsSlug),
426
+ warnings
427
+ };
395
428
  }
396
- function saveUserConfig(config) {
397
- mkdirSync2(CONFIG_DIR, { recursive: true });
398
- writeFileSync2(CONFIG_FILE, `${JSON.stringify(normalizeConfigPaths(config), null, 2)}
399
- `, { mode: 384 });
429
+ function resolveBoxKindFromConfig(config = {}, env = process.env) {
430
+ return resolveBoxIdentity(env, config).boxKind;
400
431
  }
401
- function normalizeConfigPaths(config) {
432
+
433
+ // src/resource-gate.ts
434
+ import os2 from "node:os";
435
+
436
+ // src/bounded-build/meminfo.ts
437
+ import { readFileSync as readFileSync3 } from "node:fs";
438
+ import os from "node:os";
439
+ function readMemAvailableBytes(meminfoText) {
440
+ if (meminfoText !== void 0) {
441
+ const match = meminfoText.match(/^MemAvailable:\s+(\d+)\s*kB/m);
442
+ if (match) return Number(match[1]) * 1024;
443
+ return os.freemem();
444
+ }
445
+ if (process.platform === "linux") {
446
+ try {
447
+ const meminfo = readFileSync3("/proc/meminfo", "utf8");
448
+ const match = meminfo.match(/^MemAvailable:\s+(\d+)\s*kB/m);
449
+ if (match) return Number(match[1]) * 1024;
450
+ } catch {
451
+ }
452
+ }
453
+ return os.freemem();
454
+ }
455
+
456
+ // src/resource-gate.ts
457
+ import path7 from "node:path";
458
+
459
+ // src/disk-gate.ts
460
+ import { statfsSync as statfsSync2 } from "node:fs";
461
+
462
+ // src/wsl-host.ts
463
+ import { existsSync as existsSync3, readFileSync as readFileSync4, statfsSync } from "node:fs";
464
+ var DEFAULT_WSL_HOST_WARN_FREE_BYTES = 25 * 1024 * 1024 * 1024;
465
+ var DEFAULT_WSL_HOST_CRITICAL_FREE_BYTES = 12 * 1024 * 1024 * 1024;
466
+ var DEFAULT_WSL_HOST_MOUNT = "/mnt/c";
467
+ function isWslHost() {
468
+ if (process.platform !== "linux") return false;
469
+ for (const probe of ["/proc/sys/kernel/osrelease", "/proc/version"]) {
470
+ try {
471
+ if (!existsSync3(probe)) continue;
472
+ const text = readFileSync4(probe, "utf8");
473
+ if (/microsoft|wsl/i.test(text)) return true;
474
+ } catch {
475
+ }
476
+ }
477
+ return false;
478
+ }
479
+ function observeWslHostDisk(options = {}) {
480
+ const wsl = options.forceWsl === void 0 ? isWslHost() : options.forceWsl;
481
+ if (!wsl) return null;
482
+ const path67 = options.wslHostMount?.trim() || process.env.KYNVER_WSL_HOST_MOUNT?.trim() || DEFAULT_WSL_HOST_MOUNT;
483
+ const warnBelowBytes = options.wslHostFreeWarnBytes ?? DEFAULT_WSL_HOST_WARN_FREE_BYTES;
484
+ const criticalBelowBytes = options.wslHostFreeCriticalBytes ?? DEFAULT_WSL_HOST_CRITICAL_FREE_BYTES;
485
+ const statfs = options.statfs ?? statfsSync;
486
+ let stats;
487
+ try {
488
+ stats = statfs(path67);
489
+ } catch (error) {
490
+ return {
491
+ ok: false,
492
+ path: path67,
493
+ freeBytes: 0,
494
+ totalBytes: 0,
495
+ usedPercent: 100,
496
+ warnBelowBytes,
497
+ criticalBelowBytes,
498
+ reason: `Windows host disk probe failed at ${path67}: ${error.message}`,
499
+ probeError: error.message
500
+ };
501
+ }
502
+ const freeBytes = Number(stats.bavail) * Number(stats.bsize);
503
+ const totalBytes = Number(stats.blocks) * Number(stats.bsize);
504
+ const usedPercent = totalBytes > 0 ? (totalBytes - freeBytes) / totalBytes * 100 : 100;
505
+ const lowFree = freeBytes < warnBelowBytes;
506
+ const criticalFree = freeBytes < criticalBelowBytes;
507
+ const ok = !lowFree && !criticalFree;
508
+ const freeGiB = (freeBytes / (1024 * 1024 * 1024)).toFixed(1);
509
+ let reason = null;
510
+ if (!ok) {
511
+ const tag = criticalFree ? "critical" : "warning";
512
+ reason = `Windows host disk ${path67} at ${tag}: ${freeGiB} GiB free (<${(criticalFree ? criticalBelowBytes : warnBelowBytes) / 1024 / 1024 / 1024} GiB); WSL VHDX cannot grow safely. ${summarizeWslRecoverySteps()}`;
513
+ }
402
514
  return {
403
- ...config,
404
- ...config.harnessRoot?.trim() ? { harnessRoot: redactHomePath(config.harnessRoot.trim()) } : {},
405
- ...config.defaultRepo?.trim() ? { defaultRepo: redactHomePath(config.defaultRepo.trim()) } : {}
515
+ ok,
516
+ path: path67,
517
+ freeBytes,
518
+ totalBytes,
519
+ usedPercent,
520
+ warnBelowBytes,
521
+ criticalBelowBytes,
522
+ reason,
523
+ probeError: null
406
524
  };
407
525
  }
408
- function presentUserConfig(config) {
409
- return normalizeConfigPaths(config);
526
+ function summarizeWslRecoverySteps() {
527
+ 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.";
410
528
  }
411
- function inferSetupFields(existing, args) {
412
- const creds = loadCredentialsFile();
413
- 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();
414
- 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);
415
- const explicitRepo = typeof args.repo === "string" ? args.repo : args.discoverRepo === true || args.discoverRepo === "true" ? discoverDefaultRepo()?.repo : void 0;
416
- const defaultRepo = explicitRepo || existing.defaultRepo?.trim() || process.env.KYNVER_DEFAULT_REPO?.trim() || process.env.KYNVER_HARNESS_REPO?.trim() || discoverDefaultRepo()?.repo;
417
- 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();
529
+
530
+ // src/disk-gate.ts
531
+ var DEFAULT_WARN_FREE_BYTES = 30 * 1024 * 1024 * 1024;
532
+ var DEFAULT_CRITICAL_FREE_BYTES = 15 * 1024 * 1024 * 1024;
533
+ var DEFAULT_MAX_USED_PERCENT = 80;
534
+ var DEFAULT_HARD_MAX_USED_PERCENT = 90;
535
+ function observeRunnerDiskGate(input = {}) {
536
+ const path67 = input.diskPath?.trim() || "/";
537
+ const warnBelowBytes = input.diskFreeWarnBytes ?? DEFAULT_WARN_FREE_BYTES;
538
+ const criticalBelowBytes = input.diskFreeCriticalBytes ?? DEFAULT_CRITICAL_FREE_BYTES;
539
+ const maxUsedPercent = input.diskMaxUsedPercent ?? DEFAULT_MAX_USED_PERCENT;
540
+ const hardMaxUsedPercent = input.diskHardMaxUsedPercent ?? DEFAULT_HARD_MAX_USED_PERCENT;
541
+ const stats = statfsSync2(path67);
542
+ const freeBytes = Number(stats.bavail) * Number(stats.bsize);
543
+ const totalBytes = Number(stats.blocks) * Number(stats.bsize);
544
+ const usedPercent = totalBytes > 0 ? (totalBytes - freeBytes) / totalBytes * 100 : 100;
545
+ const lowFree = freeBytes < warnBelowBytes;
546
+ const criticalFree = freeBytes < criticalBelowBytes;
547
+ const highUse = usedPercent > maxUsedPercent;
548
+ const hardHighUse = usedPercent > hardMaxUsedPercent;
549
+ const localOk = !lowFree && !criticalFree && !highUse && !hardHighUse;
550
+ const wslHost = input.skipWslHostCheck ? null : observeWslHostDisk(input.wslHost);
551
+ const ok = localOk && (wslHost ? wslHost.ok : true);
552
+ let reason = null;
553
+ if (!ok) {
554
+ reason = [
555
+ criticalFree ? `free space below critical ${criticalBelowBytes} bytes` : null,
556
+ lowFree ? `free space below warning ${warnBelowBytes} bytes` : null,
557
+ hardHighUse ? `used percent above hard cap ${hardMaxUsedPercent}%` : null,
558
+ highUse ? `used percent above cap ${maxUsedPercent}%` : null,
559
+ wslHost && !wslHost.ok ? wslHost.reason : null
560
+ ].filter(Boolean).join("; ");
561
+ }
418
562
  return {
419
- ...apiBaseUrl ? { apiBaseUrl: trimTrailingSlash(apiBaseUrl) } : {},
420
- ...agentOsId ? { agentOsId } : {},
421
- ...defaultRepo ? { defaultRepo } : {},
422
- ...harnessRoot ? { harnessRoot } : {},
423
- ...typeof args.agentOsSlug === "string" ? { agentOsSlug: args.agentOsSlug } : existing.agentOsSlug ? { agentOsSlug: existing.agentOsSlug } : {}
563
+ ok,
564
+ path: path67,
565
+ freeBytes,
566
+ totalBytes,
567
+ usedPercent,
568
+ warnBelowBytes,
569
+ criticalBelowBytes,
570
+ maxUsedPercent,
571
+ hardMaxUsedPercent,
572
+ reason,
573
+ wslHost
424
574
  };
425
575
  }
426
- function loadCredentialsFile() {
427
- if (!existsSync3(CREDENTIALS_FILE)) return {};
428
- try {
429
- return JSON.parse(readFileSync3(CREDENTIALS_FILE, "utf8"));
430
- } catch {
431
- return {};
576
+
577
+ // src/run-store.ts
578
+ import { existsSync as existsSync5, readdirSync as readdirSync2, statSync as statSync2 } from "node:fs";
579
+ import path5 from "node:path";
580
+
581
+ // src/paths.ts
582
+ import { existsSync as existsSync4 } from "node:fs";
583
+ import { homedir as homedir3 } from "node:os";
584
+ import path4 from "node:path";
585
+ var LEGACY_ROOT = path4.join(homedir3(), ".openclaw", "harness");
586
+ var HARNESS_LAYOUT_DIR_NAMES = /* @__PURE__ */ new Set(["runs", "worktrees"]);
587
+ function normalizeHarnessRoot(root) {
588
+ let resolved = path4.resolve(resolveUserPath(root.trim()));
589
+ while (HARNESS_LAYOUT_DIR_NAMES.has(path4.basename(resolved))) {
590
+ resolved = path4.dirname(resolved);
432
591
  }
592
+ return resolved;
433
593
  }
434
- function saveCredentialsFile(parsed) {
435
- mkdirSync2(CONFIG_DIR, { recursive: true });
436
- writeFileSync2(CREDENTIALS_FILE, `${JSON.stringify(parsed, null, 2)}
437
- `, { mode: 384 });
594
+ function resolveHarnessRoot() {
595
+ const env = process.env.KYNVER_HARNESS_ROOT || process.env.OPUS_HARNESS_ROOT;
596
+ if (env) return normalizeHarnessRoot(env);
597
+ const configured = loadUserConfig().harnessRoot?.trim();
598
+ if (configured) return normalizeHarnessRoot(configured);
599
+ const kynverRoot = path4.join(homedir3(), ".kynver", "harness");
600
+ if (existsSync4(kynverRoot)) return kynverRoot;
601
+ if (existsSync4(LEGACY_ROOT)) return LEGACY_ROOT;
602
+ return kynverRoot;
438
603
  }
439
- function loadApiKey() {
440
- if (process.env.KYNVER_API_KEY) return process.env.KYNVER_API_KEY;
441
- return loadCredentialsFile().apiKey;
604
+ function harnessRunsDir(harnessRoot) {
605
+ return path4.join(normalizeHarnessRoot(harnessRoot), "runs");
442
606
  }
443
- function saveApiKey(apiKey) {
444
- saveCredentialsFile({ ...loadCredentialsFile(), apiKey });
607
+ function harnessWorktreesDir(harnessRoot) {
608
+ return path4.join(normalizeHarnessRoot(harnessRoot), "worktrees");
445
609
  }
446
- function loadRunnerToken(agentOsId) {
447
- const envToken = process.env.KYNVER_RUNNER_TOKEN?.trim();
448
- if (envToken) return envToken;
449
- const creds = loadCredentialsFile();
450
- if (!creds.runnerToken) return void 0;
451
- if (agentOsId && creds.runnerTokenAgentOsId && creds.runnerTokenAgentOsId !== agentOsId) {
452
- return void 0;
453
- }
454
- return creds.runnerToken;
610
+ function getHarnessPaths() {
611
+ const harnessRoot = resolveHarnessRoot();
612
+ return {
613
+ harnessRoot,
614
+ runsDir: harnessRunsDir(harnessRoot),
615
+ worktreesDir: harnessWorktreesDir(harnessRoot)
616
+ };
455
617
  }
456
- function saveRunnerToken(agentOsId, token) {
457
- saveCredentialsFile({
458
- ...loadCredentialsFile(),
459
- runnerToken: token,
460
- runnerTokenAgentOsId: agentOsId
461
- });
618
+ function runDir(runsDir, id) {
619
+ return path4.join(runsDir, safeSlug(id));
462
620
  }
463
- function resolveBaseUrl(argsBaseUrl) {
464
- const baseUrl = resolveConfiguredBaseUrl(argsBaseUrl);
465
- if (!baseUrl) failConfig("requires --base-url, KYNVER_API_URL, KYNVER_CRON_FIRE_BASE_URL, or ~/.kynver/config.json apiBaseUrl");
466
- return baseUrl;
621
+
622
+ // src/run-store.ts
623
+ function getPaths() {
624
+ return getHarnessPaths();
467
625
  }
468
- function resolveConfiguredBaseUrl(argsBaseUrl) {
469
- const baseUrl = argsBaseUrl || process.env.KYNVER_API_URL || process.env.KYNVER_CRON_FIRE_BASE_URL || process.env.OPENCLAW_CRON_FIRE_BASE_URL || loadUserConfig().apiBaseUrl;
470
- return baseUrl ? trimTrailingSlash(String(baseUrl)) : void 0;
626
+ function loadRun(id) {
627
+ const { runsDir } = getPaths();
628
+ return readJson(path5.join(runDir(runsDir, safeSlug(id)), "run.json"));
471
629
  }
472
- function resolveConfiguredCallbackSecret(argsSecret, agentOsId) {
473
- const scoped = argsSecret || loadRunnerToken(agentOsId) || (agentOsId ? void 0 : loadRunnerToken(loadUserConfig().agentOsId));
474
- if (scoped) return String(scoped);
475
- const globalSecret = process.env.KYNVER_RUNTIME_SECRET || process.env.KYNVER_CRON_SECRET || process.env.OPENCLAW_CRON_SECRET;
476
- if (globalSecret) {
477
- console.warn(
478
- "[kynver] using deployment-level callback secret; run `kynver runner credential --agent-os-id <id>` for a scoped token"
479
- );
480
- return String(globalSecret);
481
- }
482
- return void 0;
630
+ function listRunRecords() {
631
+ const { runsDir } = getPaths();
632
+ return listRunRecordsAt(runsDir);
483
633
  }
484
- async function resolveCallbackSecretWithMint(argsSecret, agentOsId, opts) {
485
- const configured = resolveConfiguredCallbackSecret(argsSecret, agentOsId);
486
- if (configured) return configured;
487
- const apiKey = loadApiKey();
488
- const baseUrl = resolveConfiguredBaseUrl(opts?.baseUrl);
489
- if (apiKey && agentOsId && baseUrl) {
490
- try {
491
- const token = await fetchRunnerCredential(agentOsId, { baseUrl, apiKey });
492
- saveRunnerToken(agentOsId, token);
493
- return token;
494
- } catch (error) {
495
- failConfig(`runner credential mint failed: ${error.message}`);
496
- }
497
- }
498
- failConfig(
499
- "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"
500
- );
634
+ function listRunRecordsForHarnessRoot(harnessRoot) {
635
+ return listRunRecordsAt(harnessRunsDir(harnessRoot));
501
636
  }
502
- async function refreshRunnerToken(agentOsId, opts) {
503
- const apiKey = loadApiKey();
504
- const baseUrl = resolveConfiguredBaseUrl(opts?.baseUrl);
505
- if (!apiKey || !agentOsId || !baseUrl) return null;
637
+ function isRunDirectoryEntry(runDirPath) {
506
638
  try {
507
- const token = await fetchRunnerCredential(agentOsId, { baseUrl, apiKey });
508
- saveRunnerToken(agentOsId, token);
509
- return token;
639
+ return statSync2(runDirPath).isDirectory();
510
640
  } catch {
511
- return null;
512
- }
513
- }
514
- async function refreshRunnerTokenForAuthFailure(rejectedSecret, agentOsId, opts) {
515
- const apiKey = loadApiKey();
516
- const baseUrl = resolveConfiguredBaseUrl(opts?.baseUrl);
517
- if (!apiKey) return { ok: false, reason: "KYNVER_API_KEY is required to refresh a rejected runner token" };
518
- if (!agentOsId) return { ok: false, reason: "agentOsId is required to refresh a rejected runner token" };
519
- if (!baseUrl) return { ok: false, reason: "KYNVER_API_URL or --base-url is required to refresh a rejected runner token" };
520
- try {
521
- const token = await fetchRunnerCredential(agentOsId, { baseUrl, apiKey });
522
- if (token && token !== rejectedSecret) {
523
- saveRunnerToken(agentOsId, token);
524
- return { ok: true, token };
525
- }
526
- return { ok: false, reason: "runner credential refresh returned the rejected token" };
527
- } catch (error) {
528
- return { ok: false, reason: error.message };
641
+ return false;
529
642
  }
530
643
  }
531
- async function fetchRunnerCredential(agentOsId, opts) {
532
- const apiKey = opts?.apiKey || loadApiKey();
533
- if (!apiKey) throw new Error("API key required \u2014 run `kynver login` first");
534
- const base = resolveBaseUrl(opts?.baseUrl);
535
- const url = `${base}/api/agent-os/by-id/${encodeURIComponent(agentOsId)}/runner-credentials`;
536
- const res = await fetch(url, {
537
- method: "POST",
538
- headers: {
539
- "Content-Type": "application/json",
540
- Authorization: `Bearer ${apiKey}`
541
- },
542
- body: JSON.stringify({})
543
- });
544
- const text = await res.text();
545
- let parsed = null;
546
- try {
547
- parsed = JSON.parse(text);
548
- } catch {
549
- parsed = null;
550
- }
551
- if (!res.ok || !parsed?.token) {
552
- throw new Error(
553
- `runner credential mint failed (${res.status}): ${parsed?.error ?? text.slice(0, 200)}`
644
+ function listRunRecordsAt(runsDir) {
645
+ if (!existsSync5(runsDir)) return [];
646
+ const runs = [];
647
+ for (const entry of readdirSync2(runsDir, { withFileTypes: true })) {
648
+ if (entry.name === "runs") continue;
649
+ const runDir2 = path5.join(runsDir, entry.name);
650
+ if (!isRunDirectoryEntry(runDir2)) continue;
651
+ const run = readJson(
652
+ path5.join(runDir2, "run.json"),
653
+ void 0
554
654
  );
655
+ if (run?.id) runs.push(run);
555
656
  }
556
- return parsed.token;
657
+ return runs;
557
658
  }
558
- async function mintRunnerCredential(args) {
559
- const agentOsId = (args.agentOsId ? String(args.agentOsId) : loadUserConfig().agentOsId) || "";
560
- if (!agentOsId) failConfig("runner credential requires --agent-os-id or agentOsId in ~/.kynver/config.json");
561
- try {
562
- const token = await fetchRunnerCredential(agentOsId, {
563
- baseUrl: args.baseUrl ? String(args.baseUrl) : void 0
564
- });
565
- saveRunnerToken(agentOsId, token);
566
- console.log(
567
- JSON.stringify(
568
- {
569
- ok: true,
570
- agentOsId,
571
- credentialsPath: displayUserPath(CREDENTIALS_FILE),
572
- tokenPrefix: `${token.slice(0, 12)}\u2026`,
573
- note: "Scoped runner token saved; callbacks use X-Kynver-Runner-Token."
574
- },
575
- null,
576
- 2
577
- )
578
- );
579
- } catch (err) {
580
- console.error(err instanceof Error ? err.message : String(err));
581
- process.exit(1);
582
- }
659
+ function loadWorker(runId, name) {
660
+ const { runsDir } = getPaths();
661
+ return readJson(
662
+ path5.join(runDir(runsDir, safeSlug(runId)), "workers", safeSlug(name), "worker.json")
663
+ );
583
664
  }
584
- function failConfig(message) {
585
- console.error(message);
586
- process.exit(1);
665
+ function saveRun(run) {
666
+ const { runsDir } = getPaths();
667
+ writeJson(path5.join(runDir(runsDir, run.id), "run.json"), run);
587
668
  }
588
- function parseArgs(argv) {
589
- const args = {};
590
- for (let i = 0; i < argv.length; i++) {
591
- const item = argv[i];
592
- if (!item.startsWith("--")) continue;
593
- const key = item.slice(2).replace(/-([a-z])/g, (_, c) => c.toUpperCase());
594
- const next = argv[i + 1];
595
- if (!next || next.startsWith("--")) args[key] = true;
596
- else {
597
- args[key] = next;
598
- i++;
599
- }
600
- }
601
- return args;
669
+ function saveWorker(runId, worker) {
670
+ const { runsDir } = getPaths();
671
+ writeJson(path5.join(runDir(runsDir, runId), "workers", worker.name, "worker.json"), worker);
602
672
  }
603
- async function runSetup(args) {
604
- const existing = loadUserConfig();
605
- const maxWorkersRaw = typeof args.maxWorkers === "string" ? args.maxWorkers : typeof args.maxConcurrentWorkers === "string" ? args.maxConcurrentWorkers : void 0;
606
- const config = normalizeConfigPaths({
607
- ...existing,
608
- ...inferSetupFields(existing, args),
609
- ...maxWorkersRaw ? { maxConcurrentWorkers: Math.max(1, Math.floor(Number(maxWorkersRaw))) } : {},
610
- workerProvider: typeof args.provider === "string" ? args.provider : existing.workerProvider || "cursor"
611
- });
612
- saveUserConfig(config);
613
- let runnerCredentialNote;
614
- const apiKey = loadApiKey();
615
- const agentOsId = config.agentOsId;
616
- if (apiKey && agentOsId) {
617
- try {
618
- const token = await fetchRunnerCredential(agentOsId, {
619
- baseUrl: typeof args.apiBaseUrl === "string" ? args.apiBaseUrl : config.apiBaseUrl,
620
- apiKey
621
- });
622
- saveRunnerToken(agentOsId, token);
623
- runnerCredentialNote = "Scoped runner token minted and saved to ~/.kynver/credentials.";
624
- } catch {
625
- runnerCredentialNote = "Runner token not minted (server offline or master secret unset). Run `kynver runner credential` after deploy.";
626
- }
627
- }
628
- console.log(
629
- JSON.stringify(
630
- {
631
- ok: true,
632
- configPath: displayUserPath(CONFIG_FILE),
633
- config: presentUserConfig(config),
634
- 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."
635
- },
636
- null,
637
- 2
638
- )
639
- );
673
+ function runDirectory(id) {
674
+ const { harnessRoot } = getPaths();
675
+ return runDirectoryAt(harnessRoot, id);
640
676
  }
641
- async function runLogin(args) {
642
- const apiKey = typeof args.apiKey === "string" ? args.apiKey : process.env.KYNVER_API_KEY;
643
- if (!apiKey) failConfig("kynver login requires --api-key or KYNVER_API_KEY");
644
- saveApiKey(apiKey);
645
- console.log(JSON.stringify({ ok: true, credentialsPath: displayUserPath(CREDENTIALS_FILE) }, null, 2));
677
+ function runDirectoryAt(harnessRoot, id) {
678
+ return runDir(harnessRunsDir(harnessRoot), safeSlug(id));
646
679
  }
647
680
 
648
- // src/callback-headers.ts
649
- function buildHarnessCallbackHeaders(secret) {
650
- const trimmed = String(secret).trim();
651
- if (trimmed.startsWith("krc1.")) {
652
- return {
653
- "Content-Type": "application/json",
654
- "X-Kynver-Runner-Token": trimmed
655
- };
681
+ // src/run-worker-index.ts
682
+ import { existsSync as existsSync6, readdirSync as readdirSync3 } from "node:fs";
683
+ import path6 from "node:path";
684
+ function listRunWorkerNames(run) {
685
+ const names = /* @__PURE__ */ new Set();
686
+ for (const name of Object.keys(run.workers || {})) {
687
+ names.add(safeSlug(name));
656
688
  }
657
- return {
658
- "Content-Type": "application/json",
659
- // Canonical header. We keep sending the legacy `X-OpenClaw-Cron-Secret`
660
- // (and `X-Kynver-Runtime-Secret`) so an un-upgraded Kynver server that
661
- // only reads the old header still authenticates this runner during the
662
- // rename compat window.
663
- "X-Kynver-Cron-Secret": trimmed,
664
- "X-OpenClaw-Cron-Secret": trimmed,
665
- "X-Kynver-Runtime-Secret": trimmed
666
- };
667
- }
668
-
669
- // src/callbacks.ts
670
- function callbackTimeoutMs() {
671
- const parsed = Number(process.env.KYNVER_CALLBACK_TIMEOUT_MS);
672
- if (Number.isFinite(parsed) && parsed > 0) return Math.floor(parsed);
673
- return 3e4;
674
- }
675
- async function withTimeout(fn) {
676
- const controller = new AbortController();
677
- const timeout = setTimeout(() => controller.abort(), callbackTimeoutMs());
678
- try {
679
- return await fn(controller.signal);
680
- } finally {
681
- clearTimeout(timeout);
689
+ const workersDir = path6.join(runDirectory(run.id), "workers");
690
+ if (!existsSync6(workersDir)) return [...names];
691
+ for (const entry of readdirSync3(workersDir, { withFileTypes: true })) {
692
+ if (!entry.isDirectory()) continue;
693
+ names.add(safeSlug(entry.name));
682
694
  }
695
+ return [...names];
683
696
  }
684
- async function postJson(url, secret, body) {
685
- const res = await withTimeout(
686
- (signal) => fetch(url, {
687
- method: "POST",
688
- headers: buildHarnessCallbackHeaders(secret),
689
- body: JSON.stringify(body),
690
- signal
691
- })
692
- );
693
- let response = null;
697
+
698
+ // src/heartbeat.ts
699
+ import { existsSync as existsSync7, readFileSync as readFileSync5 } from "node:fs";
700
+
701
+ // src/heartbeat-final-result.ts
702
+ function tryParseJsonObject(text) {
703
+ const trimmed = text.trim();
704
+ if (!trimmed.startsWith("{")) return null;
694
705
  try {
695
- response = await res.json();
706
+ const parsed = JSON.parse(trimmed);
707
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
708
+ return parsed;
709
+ }
696
710
  } catch {
697
- response = null;
711
+ return null;
698
712
  }
699
- return { ok: res.ok, status: res.status, response };
700
- }
701
- async function postJsonWithCredentialRefresh(url, secret, body, opts) {
702
- const first = await postJson(url, secret, body);
703
- if (first.ok || first.status !== 401) return first;
704
- const refreshed = await refreshRunnerTokenForAuthFailure(secret, opts.agentOsId, { baseUrl: opts.baseUrl });
705
- if (!refreshed.ok) return { ...first, authRefreshFailure: refreshed.reason };
706
- const retry = await postJson(url, refreshed.token, body);
707
- return { ...retry, refreshedAuth: true };
713
+ return null;
708
714
  }
709
- async function getJson(url, secret) {
710
- const res = await withTimeout(
711
- (signal) => fetch(url, {
712
- method: "GET",
713
- headers: buildHarnessCallbackHeaders(secret),
714
- signal
715
- })
716
- );
717
- let response = null;
718
- try {
719
- response = await res.json();
720
- } catch {
721
- response = null;
715
+ function embeddedRecordFromProse(text) {
716
+ const trimmed = text.trim();
717
+ if (!trimmed) return null;
718
+ const direct = tryParseJsonObject(trimmed);
719
+ if (direct) return direct;
720
+ const candidates = [];
721
+ const fenceRe = /```(?:json)?\s*([\s\S]*?)```/gi;
722
+ let fenceMatch;
723
+ while ((fenceMatch = fenceRe.exec(trimmed)) !== null) {
724
+ const fromFence = tryParseJsonObject(fenceMatch[1] ?? "");
725
+ if (fromFence) candidates.push(fromFence);
722
726
  }
723
- return { ok: res.ok, status: res.status, response };
724
- }
725
-
726
- // src/dispatch-lane-normalization.ts
727
- function trimLower(value) {
728
- return (value ?? "").trim().toLowerCase();
729
- }
730
- function roleLaneToDispatchLane(roleLane) {
731
- switch (trimLower(roleLane)) {
732
- case "implementer":
733
- case "repair_implementer":
734
- case "plan_author":
735
- case "runtime_verifier":
736
- return "implementation";
737
- case "plan_reviewer":
738
- case "report_reviewer":
739
- case "deep_reviewer":
740
- return "review";
741
- default:
742
- return null;
727
+ const firstBrace = trimmed.indexOf("{");
728
+ const lastBrace = trimmed.lastIndexOf("}");
729
+ if (firstBrace >= 0 && lastBrace > firstBrace) {
730
+ const slice = tryParseJsonObject(trimmed.slice(firstBrace, lastBrace + 1));
731
+ if (slice) candidates.push(slice);
743
732
  }
733
+ return candidates.length > 0 ? candidates[candidates.length - 1] : null;
744
734
  }
745
- function normalizeDispatchNextLaneFilter(raw) {
746
- const key = trimLower(raw);
747
- if (!key) return void 0;
748
- if (key === "implementation" || key === "review" || key === "landing" || key === "any") {
749
- return key;
735
+ function terminalFinalResultFromHeartbeatRow(row) {
736
+ const explicit = row.finalResult ?? row.final_result;
737
+ if (explicit !== void 0 && explicit !== null) {
738
+ if (typeof explicit === "string") {
739
+ const embedded2 = embeddedRecordFromProse(explicit);
740
+ return embedded2 ?? (explicit.trim() || null);
741
+ }
742
+ return explicit;
750
743
  }
751
- const mapped = roleLaneToDispatchLane(key);
752
- if (mapped) return mapped;
753
- if (key === "implement" || key === "repair" || key === "coding") return "implementation";
754
- if (key === "land" || key === "merge") return "landing";
755
- return void 0;
756
- }
757
- function resolveDispatchNextLaneFilter(raw) {
758
- return normalizeDispatchNextLaneFilter(raw) ?? "any";
744
+ const summary = typeof row.summary === "string" ? row.summary.trim() : "";
745
+ if (!summary) return null;
746
+ const embedded = embeddedRecordFromProse(summary);
747
+ if (embedded) return embedded;
748
+ return summary;
759
749
  }
760
750
 
761
- // src/disk-gate.ts
762
- import { statfsSync as statfsSync2 } from "node:fs";
763
-
764
- // src/wsl-host.ts
765
- import { existsSync as existsSync4, readFileSync as readFileSync4, statfsSync } from "node:fs";
766
- var DEFAULT_WSL_HOST_WARN_FREE_BYTES = 25 * 1024 * 1024 * 1024;
767
- var DEFAULT_WSL_HOST_CRITICAL_FREE_BYTES = 12 * 1024 * 1024 * 1024;
768
- var DEFAULT_WSL_HOST_MOUNT = "/mnt/c";
769
- function isWslHost() {
770
- if (process.platform !== "linux") return false;
771
- for (const probe of ["/proc/sys/kernel/osrelease", "/proc/version"]) {
772
- try {
773
- if (!existsSync4(probe)) continue;
774
- const text = readFileSync4(probe, "utf8");
775
- if (/microsoft|wsl/i.test(text)) return true;
776
- } catch {
777
- }
778
- }
779
- return false;
751
+ // src/heartbeat.ts
752
+ var HEARTBEAT_FUTURE_SKEW_MS = 6e4;
753
+ function isTerminalHeartbeatPhase(phase) {
754
+ return phase === "complete";
780
755
  }
781
- function observeWslHostDisk(options = {}) {
782
- const wsl = options.forceWsl === void 0 ? isWslHost() : options.forceWsl;
783
- if (!wsl) return null;
784
- const path65 = options.wslHostMount?.trim() || process.env.KYNVER_WSL_HOST_MOUNT?.trim() || DEFAULT_WSL_HOST_MOUNT;
785
- const warnBelowBytes = options.wslHostFreeWarnBytes ?? DEFAULT_WSL_HOST_WARN_FREE_BYTES;
786
- const criticalBelowBytes = options.wslHostFreeCriticalBytes ?? DEFAULT_WSL_HOST_CRITICAL_FREE_BYTES;
787
- const statfs = options.statfs ?? statfsSync;
788
- let stats;
789
- try {
790
- stats = statfs(path65);
791
- } catch (error) {
792
- return {
793
- ok: false,
794
- path: path65,
795
- freeBytes: 0,
796
- totalBytes: 0,
797
- usedPercent: 100,
798
- warnBelowBytes,
799
- criticalBelowBytes,
800
- reason: `Windows host disk probe failed at ${path65}: ${error.message}`,
801
- probeError: error.message
802
- };
803
- }
804
- const freeBytes = Number(stats.bavail) * Number(stats.bsize);
805
- const totalBytes = Number(stats.blocks) * Number(stats.bsize);
806
- const usedPercent = totalBytes > 0 ? (totalBytes - freeBytes) / totalBytes * 100 : 100;
807
- const lowFree = freeBytes < warnBelowBytes;
808
- const criticalFree = freeBytes < criticalBelowBytes;
809
- const ok = !lowFree && !criticalFree;
810
- const freeGiB = (freeBytes / (1024 * 1024 * 1024)).toFixed(1);
811
- let reason = null;
812
- if (!ok) {
813
- const tag = criticalFree ? "critical" : "warning";
814
- reason = `Windows host disk ${path65} at ${tag}: ${freeGiB} GiB free (<${(criticalFree ? criticalBelowBytes : warnBelowBytes) / 1024 / 1024 / 1024} GiB); WSL VHDX cannot grow safely. ${summarizeWslRecoverySteps()}`;
756
+ function terminalFinalResultFromHeartbeat(heartbeat) {
757
+ if (!isTerminalHeartbeatPhase(heartbeat.lastHeartbeatPhase)) return null;
758
+ if (heartbeat.terminalFinalResult !== void 0 && heartbeat.terminalFinalResult !== null) {
759
+ return heartbeat.terminalFinalResult;
815
760
  }
816
- return {
817
- ok,
818
- path: path65,
819
- freeBytes,
820
- totalBytes,
821
- usedPercent,
822
- warnBelowBytes,
823
- criticalBelowBytes,
824
- reason,
825
- probeError: null
826
- };
827
- }
828
- function summarizeWslRecoverySteps() {
829
- 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.";
761
+ const summary = heartbeat.lastHeartbeatSummary?.trim();
762
+ return summary || "completed";
830
763
  }
831
-
832
- // src/disk-gate.ts
833
- var DEFAULT_WARN_FREE_BYTES = 30 * 1024 * 1024 * 1024;
834
- var DEFAULT_CRITICAL_FREE_BYTES = 15 * 1024 * 1024 * 1024;
835
- var DEFAULT_MAX_USED_PERCENT = 80;
836
- var DEFAULT_HARD_MAX_USED_PERCENT = 90;
837
- function observeRunnerDiskGate(input = {}) {
838
- const path65 = input.diskPath?.trim() || "/";
839
- const warnBelowBytes = input.diskFreeWarnBytes ?? DEFAULT_WARN_FREE_BYTES;
840
- const criticalBelowBytes = input.diskFreeCriticalBytes ?? DEFAULT_CRITICAL_FREE_BYTES;
841
- const maxUsedPercent = input.diskMaxUsedPercent ?? DEFAULT_MAX_USED_PERCENT;
842
- const hardMaxUsedPercent = input.diskHardMaxUsedPercent ?? DEFAULT_HARD_MAX_USED_PERCENT;
843
- const stats = statfsSync2(path65);
844
- const freeBytes = Number(stats.bavail) * Number(stats.bsize);
845
- const totalBytes = Number(stats.blocks) * Number(stats.bsize);
846
- const usedPercent = totalBytes > 0 ? (totalBytes - freeBytes) / totalBytes * 100 : 100;
847
- const lowFree = freeBytes < warnBelowBytes;
848
- const criticalFree = freeBytes < criticalBelowBytes;
849
- const highUse = usedPercent > maxUsedPercent;
850
- const hardHighUse = usedPercent > hardMaxUsedPercent;
851
- const localOk = !lowFree && !criticalFree && !highUse && !hardHighUse;
852
- const wslHost = input.skipWslHostCheck ? null : observeWslHostDisk(input.wslHost);
853
- const ok = localOk && (wslHost ? wslHost.ok : true);
854
- let reason = null;
855
- if (!ok) {
856
- reason = [
857
- criticalFree ? `free space below critical ${criticalBelowBytes} bytes` : null,
858
- lowFree ? `free space below warning ${warnBelowBytes} bytes` : null,
859
- hardHighUse ? `used percent above hard cap ${hardMaxUsedPercent}%` : null,
860
- highUse ? `used percent above cap ${maxUsedPercent}%` : null,
861
- wslHost && !wslHost.ok ? wslHost.reason : null
862
- ].filter(Boolean).join("; ");
863
- }
864
- return {
865
- ok,
866
- path: path65,
867
- freeBytes,
868
- totalBytes,
869
- usedPercent,
870
- warnBelowBytes,
871
- criticalBelowBytes,
872
- maxUsedPercent,
873
- hardMaxUsedPercent,
874
- reason,
875
- wslHost
764
+ function parseHeartbeat(file) {
765
+ const result = {
766
+ heartbeatCount: 0,
767
+ lastHeartbeatAt: null,
768
+ lastHeartbeatPhase: null,
769
+ lastHeartbeatSummary: null,
770
+ terminalFinalResult: null,
771
+ heartbeatBlocker: null,
772
+ timestampAnomalies: [],
773
+ lastBoxResourceSnapshot: null,
774
+ lastPrEvidence: []
876
775
  };
877
- }
878
-
879
- // src/resource-gate.ts
880
- import os2 from "node:os";
881
-
882
- // src/bounded-build/meminfo.ts
883
- import { readFileSync as readFileSync5 } from "node:fs";
884
- import os from "node:os";
885
- function readMemAvailableBytes(meminfoText) {
886
- if (meminfoText !== void 0) {
887
- const match = meminfoText.match(/^MemAvailable:\s+(\d+)\s*kB/m);
888
- if (match) return Number(match[1]) * 1024;
889
- return os.freemem();
890
- }
891
- if (process.platform === "linux") {
892
- try {
893
- const meminfo = readFileSync5("/proc/meminfo", "utf8");
894
- const match = meminfo.match(/^MemAvailable:\s+(\d+)\s*kB/m);
895
- if (match) return Number(match[1]) * 1024;
896
- } catch {
776
+ if (!existsSync7(file)) return result;
777
+ const maxFutureMs = Date.now() + HEARTBEAT_FUTURE_SKEW_MS;
778
+ const clampedTo = new Date(maxFutureMs).toISOString();
779
+ const lines = readFileSync5(file, "utf8").split("\n").filter(Boolean);
780
+ for (const line of lines) {
781
+ const entry = safeJson(line);
782
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) continue;
783
+ const row = entry;
784
+ result.heartbeatCount++;
785
+ if (row.ts) {
786
+ const ts = String(row.ts);
787
+ const tsMs = Date.parse(ts);
788
+ if (Number.isFinite(tsMs) && tsMs > maxFutureMs) {
789
+ result.timestampAnomalies.push({
790
+ kind: "future_heartbeat_timestamp",
791
+ observedAt: ts,
792
+ clampedTo
793
+ });
794
+ } else {
795
+ result.lastHeartbeatAt = ts;
796
+ }
797
+ }
798
+ if (row.phase !== void 0 && row.phase !== null) result.lastHeartbeatPhase = String(row.phase);
799
+ if (row.summary !== void 0 && row.summary !== null) result.lastHeartbeatSummary = String(row.summary);
800
+ if (isTerminalHeartbeatPhase(result.lastHeartbeatPhase)) {
801
+ result.terminalFinalResult = terminalFinalResultFromHeartbeatRow(row);
802
+ }
803
+ result.heartbeatBlocker = row.blocker ? String(row.blocker) : null;
804
+ if (row.boxResourceSnapshot && typeof row.boxResourceSnapshot === "object" && !Array.isArray(row.boxResourceSnapshot)) {
805
+ result.lastBoxResourceSnapshot = row.boxResourceSnapshot;
806
+ }
807
+ if (Array.isArray(row.prEvidence)) {
808
+ result.lastPrEvidence = row.prEvidence.filter(
809
+ (entry2) => !!entry2 && typeof entry2 === "object" && typeof entry2.prUrl === "string"
810
+ );
897
811
  }
898
812
  }
899
- return os.freemem();
813
+ return result;
900
814
  }
901
815
 
902
- // src/resource-gate.ts
903
- import path8 from "node:path";
904
-
905
- // src/run-store.ts
906
- import { existsSync as existsSync6, readdirSync as readdirSync2, statSync as statSync2 } from "node:fs";
907
- import path6 from "node:path";
908
-
909
- // src/paths.ts
910
- import { existsSync as existsSync5 } from "node:fs";
911
- import { homedir as homedir4 } from "node:os";
912
- import path5 from "node:path";
913
- var LEGACY_ROOT = path5.join(homedir4(), ".openclaw", "harness");
914
- var HARNESS_LAYOUT_DIR_NAMES = /* @__PURE__ */ new Set(["runs", "worktrees"]);
915
- function normalizeHarnessRoot(root) {
916
- let resolved = path5.resolve(resolveUserPath(root.trim()));
917
- while (HARNESS_LAYOUT_DIR_NAMES.has(path5.basename(resolved))) {
918
- resolved = path5.dirname(resolved);
919
- }
920
- return resolved;
921
- }
922
- function resolveHarnessRoot() {
923
- const env = process.env.KYNVER_HARNESS_ROOT || process.env.OPUS_HARNESS_ROOT;
924
- if (env) return normalizeHarnessRoot(env);
925
- const configured = loadUserConfig().harnessRoot?.trim();
926
- if (configured) return normalizeHarnessRoot(configured);
927
- const kynverRoot = path5.join(homedir4(), ".kynver", "harness");
928
- if (existsSync5(kynverRoot)) return kynverRoot;
929
- if (existsSync5(LEGACY_ROOT)) return LEGACY_ROOT;
930
- return kynverRoot;
931
- }
932
- function harnessRunsDir(harnessRoot) {
933
- return path5.join(normalizeHarnessRoot(harnessRoot), "runs");
934
- }
935
- function harnessWorktreesDir(harnessRoot) {
936
- return path5.join(normalizeHarnessRoot(harnessRoot), "worktrees");
937
- }
938
- function getHarnessPaths() {
939
- const harnessRoot = resolveHarnessRoot();
940
- return {
941
- harnessRoot,
942
- runsDir: harnessRunsDir(harnessRoot),
943
- worktreesDir: harnessWorktreesDir(harnessRoot)
944
- };
945
- }
946
- function runDir(runsDir, id) {
947
- return path5.join(runsDir, safeSlug(id));
948
- }
949
-
950
- // src/run-store.ts
951
- function getPaths() {
952
- return getHarnessPaths();
953
- }
954
- function loadRun(id) {
955
- const { runsDir } = getPaths();
956
- return readJson(path6.join(runDir(runsDir, safeSlug(id)), "run.json"));
957
- }
958
- function listRunRecords() {
959
- const { runsDir } = getPaths();
960
- return listRunRecordsAt(runsDir);
961
- }
962
- function listRunRecordsForHarnessRoot(harnessRoot) {
963
- return listRunRecordsAt(harnessRunsDir(harnessRoot));
964
- }
965
- function isRunDirectoryEntry(runDirPath) {
966
- try {
967
- return statSync2(runDirPath).isDirectory();
968
- } catch {
969
- return false;
970
- }
971
- }
972
- function listRunRecordsAt(runsDir) {
973
- if (!existsSync6(runsDir)) return [];
974
- const runs = [];
975
- for (const entry of readdirSync2(runsDir, { withFileTypes: true })) {
976
- if (entry.name === "runs") continue;
977
- const runDir2 = path6.join(runsDir, entry.name);
978
- if (!isRunDirectoryEntry(runDir2)) continue;
979
- const run = readJson(
980
- path6.join(runDir2, "run.json"),
981
- void 0
982
- );
983
- if (run?.id) runs.push(run);
984
- }
985
- return runs;
986
- }
987
- function loadWorker(runId, name) {
988
- const { runsDir } = getPaths();
989
- return readJson(
990
- path6.join(runDir(runsDir, safeSlug(runId)), "workers", safeSlug(name), "worker.json")
991
- );
992
- }
993
- function saveRun(run) {
994
- const { runsDir } = getPaths();
995
- writeJson(path6.join(runDir(runsDir, run.id), "run.json"), run);
996
- }
997
- function saveWorker(runId, worker) {
998
- const { runsDir } = getPaths();
999
- writeJson(path6.join(runDir(runsDir, runId), "workers", worker.name, "worker.json"), worker);
1000
- }
1001
- function runDirectory(id) {
1002
- const { harnessRoot } = getPaths();
1003
- return runDirectoryAt(harnessRoot, id);
1004
- }
1005
- function runDirectoryAt(harnessRoot, id) {
1006
- return runDir(harnessRunsDir(harnessRoot), safeSlug(id));
1007
- }
1008
-
1009
- // src/run-worker-index.ts
1010
- import { existsSync as existsSync7, readdirSync as readdirSync3 } from "node:fs";
1011
- import path7 from "node:path";
1012
- function listRunWorkerNames(run) {
1013
- const names = /* @__PURE__ */ new Set();
1014
- for (const name of Object.keys(run.workers || {})) {
1015
- names.add(safeSlug(name));
1016
- }
1017
- const workersDir = path7.join(runDirectory(run.id), "workers");
1018
- if (!existsSync7(workersDir)) return [...names];
1019
- for (const entry of readdirSync3(workersDir, { withFileTypes: true })) {
1020
- if (!entry.isDirectory()) continue;
1021
- names.add(safeSlug(entry.name));
1022
- }
1023
- return [...names];
1024
- }
1025
-
1026
- // src/heartbeat.ts
1027
- import { existsSync as existsSync8, readFileSync as readFileSync6 } from "node:fs";
1028
-
1029
- // src/heartbeat-final-result.ts
1030
- function tryParseJsonObject(text) {
1031
- const trimmed = text.trim();
1032
- if (!trimmed.startsWith("{")) return null;
1033
- try {
1034
- const parsed = JSON.parse(trimmed);
1035
- if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
1036
- return parsed;
1037
- }
1038
- } catch {
1039
- return null;
1040
- }
1041
- return null;
1042
- }
1043
- function embeddedRecordFromProse(text) {
1044
- const trimmed = text.trim();
1045
- if (!trimmed) return null;
1046
- const direct = tryParseJsonObject(trimmed);
1047
- if (direct) return direct;
1048
- const candidates = [];
1049
- const fenceRe = /```(?:json)?\s*([\s\S]*?)```/gi;
1050
- let fenceMatch;
1051
- while ((fenceMatch = fenceRe.exec(trimmed)) !== null) {
1052
- const fromFence = tryParseJsonObject(fenceMatch[1] ?? "");
1053
- if (fromFence) candidates.push(fromFence);
1054
- }
1055
- const firstBrace = trimmed.indexOf("{");
1056
- const lastBrace = trimmed.lastIndexOf("}");
1057
- if (firstBrace >= 0 && lastBrace > firstBrace) {
1058
- const slice = tryParseJsonObject(trimmed.slice(firstBrace, lastBrace + 1));
1059
- if (slice) candidates.push(slice);
1060
- }
1061
- return candidates.length > 0 ? candidates[candidates.length - 1] : null;
1062
- }
1063
- function terminalFinalResultFromHeartbeatRow(row) {
1064
- const explicit = row.finalResult ?? row.final_result;
1065
- if (explicit !== void 0 && explicit !== null) {
1066
- if (typeof explicit === "string") {
1067
- const embedded2 = embeddedRecordFromProse(explicit);
1068
- return embedded2 ?? (explicit.trim() || null);
1069
- }
1070
- return explicit;
1071
- }
1072
- const summary = typeof row.summary === "string" ? row.summary.trim() : "";
1073
- if (!summary) return null;
1074
- const embedded = embeddedRecordFromProse(summary);
1075
- if (embedded) return embedded;
1076
- return summary;
1077
- }
1078
-
1079
- // src/heartbeat.ts
1080
- var HEARTBEAT_FUTURE_SKEW_MS = 6e4;
1081
- function isTerminalHeartbeatPhase(phase) {
1082
- return phase === "complete";
1083
- }
1084
- function terminalFinalResultFromHeartbeat(heartbeat) {
1085
- if (!isTerminalHeartbeatPhase(heartbeat.lastHeartbeatPhase)) return null;
1086
- if (heartbeat.terminalFinalResult !== void 0 && heartbeat.terminalFinalResult !== null) {
1087
- return heartbeat.terminalFinalResult;
1088
- }
1089
- const summary = heartbeat.lastHeartbeatSummary?.trim();
1090
- return summary || "completed";
1091
- }
1092
- function parseHeartbeat(file) {
1093
- const result = {
1094
- heartbeatCount: 0,
1095
- lastHeartbeatAt: null,
1096
- lastHeartbeatPhase: null,
1097
- lastHeartbeatSummary: null,
1098
- terminalFinalResult: null,
1099
- heartbeatBlocker: null,
1100
- timestampAnomalies: [],
1101
- lastBoxResourceSnapshot: null,
1102
- lastPrEvidence: []
1103
- };
1104
- if (!existsSync8(file)) return result;
1105
- const maxFutureMs = Date.now() + HEARTBEAT_FUTURE_SKEW_MS;
1106
- const clampedTo = new Date(maxFutureMs).toISOString();
1107
- const lines = readFileSync6(file, "utf8").split("\n").filter(Boolean);
1108
- for (const line of lines) {
1109
- const entry = safeJson(line);
1110
- if (!entry || typeof entry !== "object" || Array.isArray(entry)) continue;
1111
- const row = entry;
1112
- result.heartbeatCount++;
1113
- if (row.ts) {
1114
- const ts = String(row.ts);
1115
- const tsMs = Date.parse(ts);
1116
- if (Number.isFinite(tsMs) && tsMs > maxFutureMs) {
1117
- result.timestampAnomalies.push({
1118
- kind: "future_heartbeat_timestamp",
1119
- observedAt: ts,
1120
- clampedTo
1121
- });
1122
- } else {
1123
- result.lastHeartbeatAt = ts;
1124
- }
1125
- }
1126
- if (row.phase !== void 0 && row.phase !== null) result.lastHeartbeatPhase = String(row.phase);
1127
- if (row.summary !== void 0 && row.summary !== null) result.lastHeartbeatSummary = String(row.summary);
1128
- if (isTerminalHeartbeatPhase(result.lastHeartbeatPhase)) {
1129
- result.terminalFinalResult = terminalFinalResultFromHeartbeatRow(row);
1130
- }
1131
- result.heartbeatBlocker = row.blocker ? String(row.blocker) : null;
1132
- if (row.boxResourceSnapshot && typeof row.boxResourceSnapshot === "object" && !Array.isArray(row.boxResourceSnapshot)) {
1133
- result.lastBoxResourceSnapshot = row.boxResourceSnapshot;
1134
- }
1135
- if (Array.isArray(row.prEvidence)) {
1136
- result.lastPrEvidence = row.prEvidence.filter(
1137
- (entry2) => !!entry2 && typeof entry2 === "object" && typeof entry2.prUrl === "string"
1138
- );
1139
- }
1140
- }
1141
- return result;
1142
- }
1143
-
1144
- // src/stream.ts
1145
- import { existsSync as existsSync9, readFileSync as readFileSync7 } from "node:fs";
816
+ // src/stream.ts
817
+ import { existsSync as existsSync8, readFileSync as readFileSync6 } from "node:fs";
1146
818
 
1147
819
  // src/repo-search.ts
1148
820
  var RG_BINARIES = /* @__PURE__ */ new Set(["rg", "ripgrep", "grep"]);
@@ -1626,8 +1298,8 @@ function parseHarnessStream(file) {
1626
1298
  error: null,
1627
1299
  lastShellOutcome: null
1628
1300
  };
1629
- if (!existsSync9(file)) return result;
1630
- const lines = readFileSync7(file, "utf8").split("\n").filter(Boolean);
1301
+ if (!existsSync8(file)) return result;
1302
+ const lines = readFileSync6(file, "utf8").split("\n").filter(Boolean);
1631
1303
  for (const line of lines) {
1632
1304
  const event = safeJson(line);
1633
1305
  if (!event) continue;
@@ -2221,161 +1893,675 @@ function computeWorkerStatus(worker, options = {}) {
2221
1893
  });
2222
1894
  const workerStatusLabel = completionBlocker || attention.state === "blocked" ? "blocked" : completionAcknowledged || attention.state === "done" ? "done" : finalResult ? "exited" : alive ? "running" : "exited";
2223
1895
  return {
2224
- runId: worker.runId,
2225
- worker: worker.name,
2226
- pid: worker.pid,
2227
- alive,
2228
- status: workerStatusLabel,
2229
- attention,
2230
- branch: worker.branch,
2231
- worktreePath: worker.worktreePath,
2232
- ownedPaths: worker.ownedPaths,
2233
- stdoutBytes,
2234
- stderrBytes,
2235
- heartbeatBytes,
2236
- firstEventAt: parsed.firstEventAt,
2237
- lastEventAt: parsed.lastEventAt,
2238
- lastActivityAt,
2239
- currentTool: completionAcknowledged ? null : parsed.currentTool,
2240
- heartbeatCount: heartbeat.heartbeatCount,
2241
- lastHeartbeatAt: heartbeat.lastHeartbeatAt,
2242
- lastHeartbeatPhase: heartbeat.lastHeartbeatPhase,
2243
- lastHeartbeatSummary: heartbeat.lastHeartbeatSummary,
2244
- heartbeatBlocker: heartbeat.heartbeatBlocker,
2245
- timestampAnomalies: heartbeat.timestampAnomalies,
2246
- finalResult,
2247
- error,
2248
- changedFiles,
2249
- gitAncestry,
2250
- instructionPolicyFingerprint: worker.instructionPolicyFingerprint ?? null,
2251
- instructionPolicyEvidence: worker.instructionPolicyEvidence ?? null
1896
+ runId: worker.runId,
1897
+ worker: worker.name,
1898
+ pid: worker.pid,
1899
+ alive,
1900
+ status: workerStatusLabel,
1901
+ attention,
1902
+ branch: worker.branch,
1903
+ worktreePath: worker.worktreePath,
1904
+ ownedPaths: worker.ownedPaths,
1905
+ stdoutBytes,
1906
+ stderrBytes,
1907
+ heartbeatBytes,
1908
+ firstEventAt: parsed.firstEventAt,
1909
+ lastEventAt: parsed.lastEventAt,
1910
+ lastActivityAt,
1911
+ currentTool: completionAcknowledged ? null : parsed.currentTool,
1912
+ heartbeatCount: heartbeat.heartbeatCount,
1913
+ lastHeartbeatAt: heartbeat.lastHeartbeatAt,
1914
+ lastHeartbeatPhase: heartbeat.lastHeartbeatPhase,
1915
+ lastHeartbeatSummary: heartbeat.lastHeartbeatSummary,
1916
+ heartbeatBlocker: heartbeat.heartbeatBlocker,
1917
+ timestampAnomalies: heartbeat.timestampAnomalies,
1918
+ finalResult,
1919
+ error,
1920
+ changedFiles,
1921
+ gitAncestry,
1922
+ instructionPolicyFingerprint: worker.instructionPolicyFingerprint ?? null,
1923
+ instructionPolicyEvidence: worker.instructionPolicyEvidence ?? null
1924
+ };
1925
+ }
1926
+ function isFinishedWorkerStatus(status) {
1927
+ if (status.finalResult) return true;
1928
+ if (status.alive === false) return true;
1929
+ if (status.status === "exited" || status.status === "done") return true;
1930
+ return false;
1931
+ }
1932
+ function isLandingBlockedWorkerStatus(status) {
1933
+ if (!status.finalResult) return false;
1934
+ return status.attention.state === "needs_attention" || status.attention.state === "blocked";
1935
+ }
1936
+ function deriveRunStatus(fallback, workers) {
1937
+ if (workers.length === 0) return fallback;
1938
+ if (workers.some((w) => w.attention === "needs_attention" || w.attention === "stale" || w.attention === "blocked")) {
1939
+ return "needs_attention";
1940
+ }
1941
+ if (workers.every((w) => w.status === "done")) return "done";
1942
+ if (workers.some((w) => w.status === "running")) return "running";
1943
+ return fallback;
1944
+ }
1945
+
1946
+ // src/resource-gate.ts
1947
+ var DEFAULT_PER_WORKER_MEM_BYTES = 500 * 1024 * 1024;
1948
+ var DEFAULT_MEM_RESERVE_BYTES = 4 * 1024 * 1024 * 1024;
1949
+ var DEFAULT_MEM_UTILIZATION = 0.85;
1950
+ var AUTO_MAX_WORKERS_CEILING = 64;
1951
+ function positiveInt(value, fallback) {
1952
+ const n = Number(value);
1953
+ if (!Number.isFinite(n) || n <= 0) return fallback;
1954
+ return Math.floor(n);
1955
+ }
1956
+ function resolveResourceConfig(config = loadUserConfig(), configuredMaxWorkersOverride, totalMemBytes) {
1957
+ const perWorkerMemBytes = positiveInt(config.perWorkerMemBytes, DEFAULT_PER_WORKER_MEM_BYTES);
1958
+ const memReserveBytes = positiveInt(config.memReserveBytes, DEFAULT_MEM_RESERVE_BYTES);
1959
+ const memUtilization = Math.min(
1960
+ 1,
1961
+ Math.max(0.1, Number(config.memUtilization) > 0 ? Number(config.memUtilization) : DEFAULT_MEM_UTILIZATION)
1962
+ );
1963
+ const cap = resolveWorkerCap({
1964
+ config,
1965
+ configuredMaxWorkersOverride,
1966
+ totalMemBytes: totalMemBytes ?? os2.totalmem()
1967
+ });
1968
+ return {
1969
+ perWorkerMemBytes,
1970
+ memReserveBytes,
1971
+ memUtilization,
1972
+ configuredMaxWorkers: cap.configuredMaxWorkers,
1973
+ autoCap: cap.autoCap,
1974
+ workerCapSource: cap.workerCapSource
1975
+ };
1976
+ }
1977
+ function computeAutoMaxWorkers(totalMemBytes, opts = {}) {
1978
+ const perWorkerMemBytes = opts.perWorkerMemBytes ?? DEFAULT_PER_WORKER_MEM_BYTES;
1979
+ const memReserveBytes = opts.memReserveBytes ?? DEFAULT_MEM_RESERVE_BYTES;
1980
+ const memUtilization = opts.memUtilization ?? DEFAULT_MEM_UTILIZATION;
1981
+ const budgetBytes = Math.max(0, Math.floor(totalMemBytes * memUtilization) - memReserveBytes);
1982
+ const raw = Math.max(1, Math.floor(budgetBytes / perWorkerMemBytes));
1983
+ return Math.min(raw, AUTO_MAX_WORKERS_CEILING);
1984
+ }
1985
+ function readAvailableMemBytes() {
1986
+ return readMemAvailableBytes();
1987
+ }
1988
+ function isActiveHarnessWorker(worker) {
1989
+ if (typeof worker.completionBlocker === "string" && worker.completionBlocker.trim()) {
1990
+ return false;
1991
+ }
1992
+ const status = computeWorkerStatus(worker);
1993
+ return status.alive && !status.finalResult && status.attention.state !== "done";
1994
+ }
1995
+ function countActiveWorkersForRun(run) {
1996
+ let active = 0;
1997
+ for (const name of listRunWorkerNames(run)) {
1998
+ const worker = readJson(
1999
+ path7.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
2000
+ void 0
2001
+ );
2002
+ if (!worker || !isActiveHarnessWorker(worker)) continue;
2003
+ active++;
2004
+ }
2005
+ return active;
2006
+ }
2007
+ function countActiveWorkersGlobal() {
2008
+ let active = 0;
2009
+ for (const run of listRunRecords()) active += countActiveWorkersForRun(run);
2010
+ return active;
2011
+ }
2012
+ function observeRunnerResourceGate(input) {
2013
+ const config = input.config ?? loadUserConfig();
2014
+ const totalMemBytes = input.totalMemBytes ?? os2.totalmem();
2015
+ const { perWorkerMemBytes, memReserveBytes, memUtilization, configuredMaxWorkers, autoCap: resolvedAutoCap, workerCapSource } = resolveResourceConfig(config, input.configuredMaxWorkersOverride, totalMemBytes);
2016
+ const boxKind = resolveBoxKindFromConfig(config);
2017
+ const freeMemBytes = input.freeMemBytes ?? readAvailableMemBytes();
2018
+ const activeWorkers = input.activeWorkers ?? countActiveWorkersGlobal();
2019
+ const budgetBytes = Math.max(0, Math.floor(totalMemBytes * memUtilization) - memReserveBytes);
2020
+ const capacityFromTotal = Math.max(0, Math.floor(budgetBytes / perWorkerMemBytes));
2021
+ const capacityFromFree = Math.max(0, Math.floor(Math.max(0, freeMemBytes - memReserveBytes) / perWorkerMemBytes));
2022
+ const autoCap = resolvedAutoCap;
2023
+ const targetCap = configuredMaxWorkers ?? autoCap;
2024
+ const maxConcurrentWorkers = Math.max(0, Math.min(targetCap, capacityFromTotal));
2025
+ const slotsByCapacity = Math.max(0, maxConcurrentWorkers - activeWorkers);
2026
+ const slotsByFreeMem = capacityFromFree;
2027
+ let slotsAvailable = Math.min(slotsByCapacity, slotsByFreeMem);
2028
+ const skipDisk = input.skipDiskGate || process.env.KYNVER_RESOURCE_GATE_SKIP_DISK === "1";
2029
+ const diskGate = skipDisk ? void 0 : observeRunnerDiskGate({
2030
+ diskPath: input.diskPath?.trim() || process.env.KYNVER_DISK_GUARD_PATH?.trim() || "/"
2031
+ });
2032
+ if (diskGate && !diskGate.ok) slotsAvailable = 0;
2033
+ let reason = null;
2034
+ if (slotsAvailable <= 0) {
2035
+ if (diskGate && !diskGate.ok) {
2036
+ reason = diskGate.reason ?? "disk gate blocked worker admission";
2037
+ } else if (activeWorkers >= maxConcurrentWorkers) {
2038
+ reason = `at worker limit (${activeWorkers}/${maxConcurrentWorkers} running)`;
2039
+ } else if (capacityFromFree <= 0) {
2040
+ reason = "insufficient free memory \u2014 waiting for workers to finish";
2041
+ } else {
2042
+ reason = "no worker slots available";
2043
+ }
2044
+ }
2045
+ return {
2046
+ ok: slotsAvailable > 0,
2047
+ totalMemBytes,
2048
+ freeMemBytes,
2049
+ memReserveBytes,
2050
+ perWorkerMemBytes,
2051
+ configuredMaxWorkers,
2052
+ workerCapSource,
2053
+ boxKind,
2054
+ autoCap,
2055
+ capacityWorkers: capacityFromTotal,
2056
+ maxConcurrentWorkers,
2057
+ activeWorkers,
2058
+ slotsAvailable,
2059
+ reason,
2060
+ ...diskGate ? { diskGate } : {}
2061
+ };
2062
+ }
2063
+
2064
+ // src/worker-cap-source.ts
2065
+ function positiveInt2(value, fallback) {
2066
+ const n = Number(value);
2067
+ if (!Number.isFinite(n) || n <= 0) return fallback;
2068
+ return Math.floor(n);
2069
+ }
2070
+ function resolveWorkerCap(input) {
2071
+ const config = input.config ?? {};
2072
+ const env = input.env ?? process.env;
2073
+ const perWorkerMemBytes = positiveInt2(config.perWorkerMemBytes, DEFAULT_PER_WORKER_MEM_BYTES);
2074
+ const memReserveBytes = positiveInt2(config.memReserveBytes, DEFAULT_MEM_RESERVE_BYTES);
2075
+ const memUtilization = Math.min(
2076
+ 1,
2077
+ Math.max(0.1, Number(config.memUtilization) > 0 ? Number(config.memUtilization) : DEFAULT_MEM_UTILIZATION)
2078
+ );
2079
+ const autoCap = computeAutoMaxWorkers(input.totalMemBytes, {
2080
+ perWorkerMemBytes,
2081
+ memReserveBytes,
2082
+ memUtilization
2083
+ });
2084
+ if (input.configuredMaxWorkersOverride !== void 0 && input.configuredMaxWorkersOverride !== null) {
2085
+ return {
2086
+ configuredMaxWorkers: positiveInt2(input.configuredMaxWorkersOverride, autoCap),
2087
+ autoCap,
2088
+ workerCapSource: "workspace_override"
2089
+ };
2090
+ }
2091
+ if (config.maxConcurrentWorkers !== void 0 && config.maxConcurrentWorkers !== null) {
2092
+ const configured = positiveInt2(config.maxConcurrentWorkers, 0) || null;
2093
+ if (configured) {
2094
+ return {
2095
+ configuredMaxWorkers: Math.min(configured, AUTO_MAX_WORKERS_CEILING),
2096
+ autoCap,
2097
+ workerCapSource: "config"
2098
+ };
2099
+ }
2100
+ }
2101
+ const envIntentional = env.KYNVER_MAX_WORKERS_INTENTIONAL === "1" || env.KYNVER_MAX_WORKERS_INTENTIONAL === "true";
2102
+ const envCap = envIntentional && env.KYNVER_MAX_WORKERS ? positiveInt2(env.KYNVER_MAX_WORKERS, 0) || null : null;
2103
+ if (envCap) {
2104
+ return {
2105
+ configuredMaxWorkers: Math.min(envCap, AUTO_MAX_WORKERS_CEILING),
2106
+ autoCap,
2107
+ workerCapSource: "env"
2108
+ };
2109
+ }
2110
+ return {
2111
+ configuredMaxWorkers: null,
2112
+ autoCap,
2113
+ workerCapSource: "auto"
2114
+ };
2115
+ }
2116
+ function recommendSetupWorkerCap(input = {}) {
2117
+ const totalMemBytes = input.totalMemBytes ?? 0;
2118
+ const autoCap = computeAutoMaxWorkers(totalMemBytes, {
2119
+ perWorkerMemBytes: positiveInt2(input.config?.perWorkerMemBytes, DEFAULT_PER_WORKER_MEM_BYTES),
2120
+ memReserveBytes: positiveInt2(input.config?.memReserveBytes, DEFAULT_MEM_RESERVE_BYTES),
2121
+ memUtilization: input.config?.memUtilization && Number(input.config.memUtilization) > 0 ? Number(input.config.memUtilization) : DEFAULT_MEM_UTILIZATION
2122
+ });
2123
+ const diskGateOk = input.diskGateOk ?? true;
2124
+ const recommendedMaxWorkers = diskGateOk ? autoCap : Math.max(1, Math.min(autoCap, 4));
2125
+ return {
2126
+ totalMemBytes,
2127
+ autoCap,
2128
+ recommendedMaxWorkers,
2129
+ diskPath: input.diskPath ?? "/",
2130
+ diskGateOk,
2131
+ diskFreeBytes: input.diskFreeBytes ?? null
2132
+ };
2133
+ }
2134
+
2135
+ // src/config.ts
2136
+ import os3 from "node:os";
2137
+ var CONFIG_DIR = path8.join(homedir4(), ".kynver");
2138
+ var CONFIG_FILE = path8.join(CONFIG_DIR, "config.json");
2139
+ var CREDENTIALS_FILE = path8.join(CONFIG_DIR, "credentials");
2140
+ function loadUserConfig() {
2141
+ if (!existsSync9(CONFIG_FILE)) return {};
2142
+ try {
2143
+ return JSON.parse(readFileSync7(CONFIG_FILE, "utf8"));
2144
+ } catch {
2145
+ return {};
2146
+ }
2147
+ }
2148
+ function saveUserConfig(config) {
2149
+ mkdirSync2(CONFIG_DIR, { recursive: true });
2150
+ writeFileSync2(CONFIG_FILE, `${JSON.stringify(normalizeConfigPaths(config), null, 2)}
2151
+ `, { mode: 384 });
2152
+ }
2153
+ function normalizeConfigPaths(config) {
2154
+ return {
2155
+ ...config,
2156
+ ...config.harnessRoot?.trim() ? { harnessRoot: redactHomePath(config.harnessRoot.trim()) } : {},
2157
+ ...config.defaultRepo?.trim() ? { defaultRepo: redactHomePath(config.defaultRepo.trim()) } : {}
2158
+ };
2159
+ }
2160
+ function presentUserConfig(config) {
2161
+ return normalizeConfigPaths(config);
2162
+ }
2163
+ function inferSetupFields(existing, args) {
2164
+ const creds = loadCredentialsFile();
2165
+ 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();
2166
+ 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);
2167
+ const explicitRepo = typeof args.repo === "string" ? args.repo : args.discoverRepo === true || args.discoverRepo === "true" ? discoverDefaultRepo()?.repo : void 0;
2168
+ const defaultRepo = explicitRepo || existing.defaultRepo?.trim() || process.env.KYNVER_DEFAULT_REPO?.trim() || process.env.KYNVER_HARNESS_REPO?.trim() || discoverDefaultRepo()?.repo;
2169
+ 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();
2170
+ return {
2171
+ ...apiBaseUrl ? { apiBaseUrl: trimTrailingSlash(apiBaseUrl) } : {},
2172
+ ...agentOsId ? { agentOsId } : {},
2173
+ ...defaultRepo ? { defaultRepo } : {},
2174
+ ...harnessRoot ? { harnessRoot } : {},
2175
+ ...typeof args.agentOsSlug === "string" ? { agentOsSlug: args.agentOsSlug } : existing.agentOsSlug ? { agentOsSlug: existing.agentOsSlug } : {}
2176
+ };
2177
+ }
2178
+ var SETUP_PER_WORKER_MEM_BYTES = 500 * 1024 * 1024;
2179
+ var SETUP_MEM_RESERVE_BYTES = 4 * 1024 * 1024 * 1024;
2180
+ function resolveSetupWorkerConfig(existing, args, totalMemBytes = totalmem()) {
2181
+ const maxWorkersRaw = typeof args.maxWorkers === "string" ? args.maxWorkers : typeof args.maxConcurrentWorkers === "string" ? args.maxConcurrentWorkers : void 0;
2182
+ const explicitBoxKindArg = typeof args.boxKind === "string" ? args.boxKind : typeof args["box-kind"] === "string" ? String(args["box-kind"]) : void 0;
2183
+ const boxKind = resolveBoxIdentity(process.env, {
2184
+ ...existing,
2185
+ ...explicitBoxKindArg ? { boxKind: normalizeWorkerPoolBoxKind(explicitBoxKindArg) } : {}
2186
+ }).boxKind;
2187
+ const diskGate = observeRunnerDiskGate({
2188
+ diskPath: typeof args.diskPath === "string" ? args.diskPath : "/"
2189
+ });
2190
+ const capRecommendation = recommendSetupWorkerCap({
2191
+ totalMemBytes,
2192
+ diskPath: diskGate.path,
2193
+ diskGateOk: diskGate.ok,
2194
+ diskFreeBytes: diskGate.freeBytes,
2195
+ config: existing
2196
+ });
2197
+ if (maxWorkersRaw) {
2198
+ return {
2199
+ maxConcurrentWorkers: Math.max(1, Math.floor(Number(maxWorkersRaw))),
2200
+ maxConcurrentWorkersSource: "setup-flag",
2201
+ boxKind
2202
+ };
2203
+ }
2204
+ if (existing.maxConcurrentWorkers !== void 0 && existing.maxConcurrentWorkers !== null) {
2205
+ return {
2206
+ maxConcurrentWorkers: Math.max(1, Math.floor(Number(existing.maxConcurrentWorkers))),
2207
+ maxConcurrentWorkersSource: existing.maxConcurrentWorkersSource ?? "operator",
2208
+ boxKind
2209
+ };
2210
+ }
2211
+ return {
2212
+ maxConcurrentWorkers: capRecommendation.recommendedMaxWorkers,
2213
+ maxConcurrentWorkersSource: "setup-auto",
2214
+ boxKind
2215
+ };
2216
+ }
2217
+ function loadCredentialsFile() {
2218
+ if (!existsSync9(CREDENTIALS_FILE)) return {};
2219
+ try {
2220
+ return JSON.parse(readFileSync7(CREDENTIALS_FILE, "utf8"));
2221
+ } catch {
2222
+ return {};
2223
+ }
2224
+ }
2225
+ function saveCredentialsFile(parsed) {
2226
+ mkdirSync2(CONFIG_DIR, { recursive: true });
2227
+ writeFileSync2(CREDENTIALS_FILE, `${JSON.stringify(parsed, null, 2)}
2228
+ `, { mode: 384 });
2229
+ }
2230
+ function loadApiKey() {
2231
+ if (process.env.KYNVER_API_KEY) return process.env.KYNVER_API_KEY;
2232
+ return loadCredentialsFile().apiKey;
2233
+ }
2234
+ function saveApiKey(apiKey) {
2235
+ saveCredentialsFile({ ...loadCredentialsFile(), apiKey });
2236
+ }
2237
+ function loadRunnerToken(agentOsId) {
2238
+ const envToken = process.env.KYNVER_RUNNER_TOKEN?.trim();
2239
+ if (envToken) return envToken;
2240
+ const creds = loadCredentialsFile();
2241
+ if (!creds.runnerToken) return void 0;
2242
+ if (agentOsId && creds.runnerTokenAgentOsId && creds.runnerTokenAgentOsId !== agentOsId) {
2243
+ return void 0;
2244
+ }
2245
+ return creds.runnerToken;
2246
+ }
2247
+ function saveRunnerToken(agentOsId, token) {
2248
+ saveCredentialsFile({
2249
+ ...loadCredentialsFile(),
2250
+ runnerToken: token,
2251
+ runnerTokenAgentOsId: agentOsId
2252
+ });
2253
+ }
2254
+ function resolveBaseUrl(argsBaseUrl) {
2255
+ const baseUrl = resolveConfiguredBaseUrl(argsBaseUrl);
2256
+ if (!baseUrl) failConfig("requires --base-url, KYNVER_API_URL, KYNVER_CRON_FIRE_BASE_URL, or ~/.kynver/config.json apiBaseUrl");
2257
+ return baseUrl;
2258
+ }
2259
+ function resolveConfiguredBaseUrl(argsBaseUrl) {
2260
+ const baseUrl = argsBaseUrl || process.env.KYNVER_API_URL || process.env.KYNVER_CRON_FIRE_BASE_URL || process.env.OPENCLAW_CRON_FIRE_BASE_URL || loadUserConfig().apiBaseUrl;
2261
+ return baseUrl ? trimTrailingSlash(String(baseUrl)) : void 0;
2262
+ }
2263
+ function resolveConfiguredCallbackSecret(argsSecret, agentOsId) {
2264
+ const scoped = argsSecret || loadRunnerToken(agentOsId) || (agentOsId ? void 0 : loadRunnerToken(loadUserConfig().agentOsId));
2265
+ if (scoped) return String(scoped);
2266
+ const globalSecret = process.env.KYNVER_RUNTIME_SECRET || process.env.KYNVER_CRON_SECRET || process.env.OPENCLAW_CRON_SECRET;
2267
+ if (globalSecret) {
2268
+ console.warn(
2269
+ "[kynver] using deployment-level callback secret; run `kynver runner credential --agent-os-id <id>` for a scoped token"
2270
+ );
2271
+ return String(globalSecret);
2272
+ }
2273
+ return void 0;
2274
+ }
2275
+ async function resolveCallbackSecretWithMint(argsSecret, agentOsId, opts) {
2276
+ const configured = resolveConfiguredCallbackSecret(argsSecret, agentOsId);
2277
+ if (configured) return configured;
2278
+ const apiKey = loadApiKey();
2279
+ const baseUrl = resolveConfiguredBaseUrl(opts?.baseUrl);
2280
+ if (apiKey && agentOsId && baseUrl) {
2281
+ try {
2282
+ const token = await fetchRunnerCredential(agentOsId, { baseUrl, apiKey });
2283
+ saveRunnerToken(agentOsId, token);
2284
+ return token;
2285
+ } catch (error) {
2286
+ failConfig(`runner credential mint failed: ${error.message}`);
2287
+ }
2288
+ }
2289
+ failConfig(
2290
+ "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"
2291
+ );
2292
+ }
2293
+ async function refreshRunnerToken(agentOsId, opts) {
2294
+ const apiKey = loadApiKey();
2295
+ const baseUrl = resolveConfiguredBaseUrl(opts?.baseUrl);
2296
+ if (!apiKey || !agentOsId || !baseUrl) return null;
2297
+ try {
2298
+ const token = await fetchRunnerCredential(agentOsId, { baseUrl, apiKey });
2299
+ saveRunnerToken(agentOsId, token);
2300
+ return token;
2301
+ } catch {
2302
+ return null;
2303
+ }
2304
+ }
2305
+ async function refreshRunnerTokenForAuthFailure(rejectedSecret, agentOsId, opts) {
2306
+ const apiKey = loadApiKey();
2307
+ const baseUrl = resolveConfiguredBaseUrl(opts?.baseUrl);
2308
+ if (!apiKey) return { ok: false, reason: "KYNVER_API_KEY is required to refresh a rejected runner token" };
2309
+ if (!agentOsId) return { ok: false, reason: "agentOsId is required to refresh a rejected runner token" };
2310
+ if (!baseUrl) return { ok: false, reason: "KYNVER_API_URL or --base-url is required to refresh a rejected runner token" };
2311
+ try {
2312
+ const token = await fetchRunnerCredential(agentOsId, { baseUrl, apiKey });
2313
+ if (token && token !== rejectedSecret) {
2314
+ saveRunnerToken(agentOsId, token);
2315
+ return { ok: true, token };
2316
+ }
2317
+ return { ok: false, reason: "runner credential refresh returned the rejected token" };
2318
+ } catch (error) {
2319
+ return { ok: false, reason: error.message };
2320
+ }
2321
+ }
2322
+ async function fetchRunnerCredential(agentOsId, opts) {
2323
+ const apiKey = opts?.apiKey || loadApiKey();
2324
+ if (!apiKey) throw new Error("API key required \u2014 run `kynver login` first");
2325
+ const base = resolveBaseUrl(opts?.baseUrl);
2326
+ const url = `${base}/api/agent-os/by-id/${encodeURIComponent(agentOsId)}/runner-credentials`;
2327
+ const res = await fetch(url, {
2328
+ method: "POST",
2329
+ headers: {
2330
+ "Content-Type": "application/json",
2331
+ Authorization: `Bearer ${apiKey}`
2332
+ },
2333
+ body: JSON.stringify({})
2334
+ });
2335
+ const text = await res.text();
2336
+ let parsed = null;
2337
+ try {
2338
+ parsed = JSON.parse(text);
2339
+ } catch {
2340
+ parsed = null;
2341
+ }
2342
+ if (!res.ok || !parsed?.token) {
2343
+ throw new Error(
2344
+ `runner credential mint failed (${res.status}): ${parsed?.error ?? text.slice(0, 200)}`
2345
+ );
2346
+ }
2347
+ return parsed.token;
2348
+ }
2349
+ async function mintRunnerCredential(args) {
2350
+ const agentOsId = (args.agentOsId ? String(args.agentOsId) : loadUserConfig().agentOsId) || "";
2351
+ if (!agentOsId) failConfig("runner credential requires --agent-os-id or agentOsId in ~/.kynver/config.json");
2352
+ try {
2353
+ const token = await fetchRunnerCredential(agentOsId, {
2354
+ baseUrl: args.baseUrl ? String(args.baseUrl) : void 0
2355
+ });
2356
+ saveRunnerToken(agentOsId, token);
2357
+ console.log(
2358
+ JSON.stringify(
2359
+ {
2360
+ ok: true,
2361
+ agentOsId,
2362
+ credentialsPath: displayUserPath(CREDENTIALS_FILE),
2363
+ tokenPrefix: `${token.slice(0, 12)}\u2026`,
2364
+ note: "Scoped runner token saved; callbacks use X-Kynver-Runner-Token."
2365
+ },
2366
+ null,
2367
+ 2
2368
+ )
2369
+ );
2370
+ } catch (err) {
2371
+ console.error(err instanceof Error ? err.message : String(err));
2372
+ process.exit(1);
2373
+ }
2374
+ }
2375
+ function failConfig(message) {
2376
+ console.error(message);
2377
+ process.exit(1);
2378
+ }
2379
+ function parseArgs(argv) {
2380
+ const args = {};
2381
+ for (let i = 0; i < argv.length; i++) {
2382
+ const item = argv[i];
2383
+ if (!item.startsWith("--")) continue;
2384
+ const key = item.slice(2).replace(/-([a-z])/g, (_, c) => c.toUpperCase());
2385
+ const next = argv[i + 1];
2386
+ if (!next || next.startsWith("--")) args[key] = true;
2387
+ else {
2388
+ args[key] = next;
2389
+ i++;
2390
+ }
2391
+ }
2392
+ return args;
2393
+ }
2394
+ async function runSetup(args) {
2395
+ const existing = loadUserConfig();
2396
+ const diskGate = observeRunnerDiskGate({
2397
+ diskPath: typeof args.diskPath === "string" ? args.diskPath : "/"
2398
+ });
2399
+ const capRecommendation = recommendSetupWorkerCap({
2400
+ totalMemBytes: os3.totalmem(),
2401
+ diskPath: diskGate.path,
2402
+ diskGateOk: diskGate.ok,
2403
+ diskFreeBytes: diskGate.freeBytes,
2404
+ config: existing
2405
+ });
2406
+ const workerConfig = resolveSetupWorkerConfig(existing, args);
2407
+ const config = normalizeConfigPaths({
2408
+ ...existing,
2409
+ ...inferSetupFields(existing, args),
2410
+ ...workerConfig,
2411
+ workerProvider: typeof args.provider === "string" ? args.provider : existing.workerProvider || "cursor"
2412
+ });
2413
+ saveUserConfig(config);
2414
+ const boxIdentity = resolveBoxIdentity(process.env, config);
2415
+ let runnerCredentialNote;
2416
+ const apiKey = loadApiKey();
2417
+ const agentOsId = config.agentOsId;
2418
+ if (apiKey && agentOsId) {
2419
+ try {
2420
+ const token = await fetchRunnerCredential(agentOsId, {
2421
+ baseUrl: typeof args.apiBaseUrl === "string" ? args.apiBaseUrl : config.apiBaseUrl,
2422
+ apiKey
2423
+ });
2424
+ saveRunnerToken(agentOsId, token);
2425
+ runnerCredentialNote = "Scoped runner token minted and saved to ~/.kynver/credentials.";
2426
+ } catch {
2427
+ runnerCredentialNote = "Runner token not minted (server offline or master secret unset). Run `kynver runner credential` after deploy.";
2428
+ }
2429
+ }
2430
+ console.log(
2431
+ JSON.stringify(
2432
+ {
2433
+ ok: true,
2434
+ configPath: displayUserPath(CONFIG_FILE),
2435
+ config: presentUserConfig(config),
2436
+ boxKind: config.boxKind,
2437
+ boxKindSource: boxIdentity.source,
2438
+ workerCapRecommendation: capRecommendation,
2439
+ ...boxIdentity.warnings.length ? { boxIdentityWarnings: boxIdentity.warnings } : {},
2440
+ note: runnerCredentialNote ?? "boxKind and maxConcurrentWorkers persisted; override with --box-kind and --max-workers. Run `kynver login` + `kynver runner credential` for scoped callbacks."
2441
+ },
2442
+ null,
2443
+ 2
2444
+ )
2445
+ );
2446
+ }
2447
+ async function runLogin(args) {
2448
+ const apiKey = typeof args.apiKey === "string" ? args.apiKey : process.env.KYNVER_API_KEY;
2449
+ if (!apiKey) failConfig("kynver login requires --api-key or KYNVER_API_KEY");
2450
+ saveApiKey(apiKey);
2451
+ console.log(JSON.stringify({ ok: true, credentialsPath: displayUserPath(CREDENTIALS_FILE) }, null, 2));
2452
+ }
2453
+
2454
+ // src/callback-headers.ts
2455
+ function buildHarnessCallbackHeaders(secret) {
2456
+ const trimmed = String(secret).trim();
2457
+ if (trimmed.startsWith("krc1.")) {
2458
+ return {
2459
+ "Content-Type": "application/json",
2460
+ "X-Kynver-Runner-Token": trimmed
2461
+ };
2462
+ }
2463
+ return {
2464
+ "Content-Type": "application/json",
2465
+ // Canonical header. We keep sending the legacy `X-OpenClaw-Cron-Secret`
2466
+ // (and `X-Kynver-Runtime-Secret`) so an un-upgraded Kynver server that
2467
+ // only reads the old header still authenticates this runner during the
2468
+ // rename compat window.
2469
+ "X-Kynver-Cron-Secret": trimmed,
2470
+ "X-OpenClaw-Cron-Secret": trimmed,
2471
+ "X-Kynver-Runtime-Secret": trimmed
2252
2472
  };
2253
2473
  }
2254
- function isFinishedWorkerStatus(status) {
2255
- if (status.finalResult) return true;
2256
- if (status.alive === false) return true;
2257
- if (status.status === "exited" || status.status === "done") return true;
2258
- return false;
2474
+
2475
+ // src/callbacks.ts
2476
+ function callbackTimeoutMs() {
2477
+ const parsed = Number(process.env.KYNVER_CALLBACK_TIMEOUT_MS);
2478
+ if (Number.isFinite(parsed) && parsed > 0) return Math.floor(parsed);
2479
+ return 3e4;
2259
2480
  }
2260
- function isLandingBlockedWorkerStatus(status) {
2261
- if (!status.finalResult) return false;
2262
- return status.attention.state === "needs_attention" || status.attention.state === "blocked";
2481
+ async function withTimeout(fn) {
2482
+ const controller = new AbortController();
2483
+ const timeout = setTimeout(() => controller.abort(), callbackTimeoutMs());
2484
+ try {
2485
+ return await fn(controller.signal);
2486
+ } finally {
2487
+ clearTimeout(timeout);
2488
+ }
2263
2489
  }
2264
- function deriveRunStatus(fallback, workers) {
2265
- if (workers.length === 0) return fallback;
2266
- if (workers.some((w) => w.attention === "needs_attention" || w.attention === "stale" || w.attention === "blocked")) {
2267
- return "needs_attention";
2490
+ async function postJson(url, secret, body) {
2491
+ const res = await withTimeout(
2492
+ (signal) => fetch(url, {
2493
+ method: "POST",
2494
+ headers: buildHarnessCallbackHeaders(secret),
2495
+ body: JSON.stringify(body),
2496
+ signal
2497
+ })
2498
+ );
2499
+ let response = null;
2500
+ try {
2501
+ response = await res.json();
2502
+ } catch {
2503
+ response = null;
2268
2504
  }
2269
- if (workers.every((w) => w.status === "done")) return "done";
2270
- if (workers.some((w) => w.status === "running")) return "running";
2271
- return fallback;
2505
+ return { ok: res.ok, status: res.status, response };
2272
2506
  }
2273
-
2274
- // src/resource-gate.ts
2275
- var DEFAULT_PER_WORKER_MEM_BYTES = 500 * 1024 * 1024;
2276
- var DEFAULT_MEM_RESERVE_BYTES = 4 * 1024 * 1024 * 1024;
2277
- var DEFAULT_MEM_UTILIZATION = 0.85;
2278
- var AUTO_MAX_WORKERS_CEILING = 64;
2279
- function positiveInt(value, fallback) {
2280
- const n = Number(value);
2281
- if (!Number.isFinite(n) || n <= 0) return fallback;
2282
- return Math.floor(n);
2507
+ async function postJsonWithCredentialRefresh(url, secret, body, opts) {
2508
+ const first = await postJson(url, secret, body);
2509
+ if (first.ok || first.status !== 401) return first;
2510
+ const refreshed = await refreshRunnerTokenForAuthFailure(secret, opts.agentOsId, { baseUrl: opts.baseUrl });
2511
+ if (!refreshed.ok) return { ...first, authRefreshFailure: refreshed.reason };
2512
+ const retry = await postJson(url, refreshed.token, body);
2513
+ return { ...retry, refreshedAuth: true };
2283
2514
  }
2284
- function resolveResourceConfig(config = loadUserConfig(), configuredMaxWorkersOverride) {
2285
- const perWorkerMemBytes = positiveInt(config.perWorkerMemBytes, DEFAULT_PER_WORKER_MEM_BYTES);
2286
- const memReserveBytes = positiveInt(config.memReserveBytes, DEFAULT_MEM_RESERVE_BYTES);
2287
- const memUtilization = Math.min(
2288
- 1,
2289
- Math.max(0.1, Number(config.memUtilization) > 0 ? Number(config.memUtilization) : DEFAULT_MEM_UTILIZATION)
2515
+ async function getJson(url, secret) {
2516
+ const res = await withTimeout(
2517
+ (signal) => fetch(url, {
2518
+ method: "GET",
2519
+ headers: buildHarnessCallbackHeaders(secret),
2520
+ signal
2521
+ })
2290
2522
  );
2291
- const envCap = process.env.KYNVER_MAX_WORKERS ? positiveInt(process.env.KYNVER_MAX_WORKERS, 0) || null : null;
2292
- const configuredMaxWorkers = configuredMaxWorkersOverride !== void 0 ? configuredMaxWorkersOverride : envCap ?? (config.maxConcurrentWorkers !== void 0 && config.maxConcurrentWorkers !== null ? positiveInt(config.maxConcurrentWorkers, 0) || null : null);
2293
- return { perWorkerMemBytes, memReserveBytes, memUtilization, configuredMaxWorkers };
2294
- }
2295
- function computeAutoMaxWorkers(totalMemBytes, opts = {}) {
2296
- const perWorkerMemBytes = opts.perWorkerMemBytes ?? DEFAULT_PER_WORKER_MEM_BYTES;
2297
- const memReserveBytes = opts.memReserveBytes ?? DEFAULT_MEM_RESERVE_BYTES;
2298
- const memUtilization = opts.memUtilization ?? DEFAULT_MEM_UTILIZATION;
2299
- const budgetBytes = Math.max(0, Math.floor(totalMemBytes * memUtilization) - memReserveBytes);
2300
- const raw = Math.max(1, Math.floor(budgetBytes / perWorkerMemBytes));
2301
- return Math.min(raw, AUTO_MAX_WORKERS_CEILING);
2523
+ let response = null;
2524
+ try {
2525
+ response = await res.json();
2526
+ } catch {
2527
+ response = null;
2528
+ }
2529
+ return { ok: res.ok, status: res.status, response };
2302
2530
  }
2303
- function readAvailableMemBytes() {
2304
- return readMemAvailableBytes();
2531
+
2532
+ // src/dispatch-lane-normalization.ts
2533
+ function trimLower(value) {
2534
+ return (value ?? "").trim().toLowerCase();
2305
2535
  }
2306
- function isActiveHarnessWorker(worker) {
2307
- if (typeof worker.completionBlocker === "string" && worker.completionBlocker.trim()) {
2308
- return false;
2536
+ function roleLaneToDispatchLane(roleLane) {
2537
+ switch (trimLower(roleLane)) {
2538
+ case "implementer":
2539
+ case "repair_implementer":
2540
+ case "plan_author":
2541
+ case "runtime_verifier":
2542
+ return "implementation";
2543
+ case "plan_reviewer":
2544
+ case "report_reviewer":
2545
+ case "deep_reviewer":
2546
+ return "review";
2547
+ default:
2548
+ return null;
2309
2549
  }
2310
- const status = computeWorkerStatus(worker);
2311
- return status.alive && !status.finalResult && status.attention.state !== "done";
2312
2550
  }
2313
- function countActiveWorkersForRun(run) {
2314
- let active = 0;
2315
- for (const name of listRunWorkerNames(run)) {
2316
- const worker = readJson(
2317
- path8.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
2318
- void 0
2319
- );
2320
- if (!worker || !isActiveHarnessWorker(worker)) continue;
2321
- active++;
2551
+ function normalizeDispatchNextLaneFilter(raw) {
2552
+ const key = trimLower(raw);
2553
+ if (!key) return void 0;
2554
+ if (key === "implementation" || key === "review" || key === "landing" || key === "any") {
2555
+ return key;
2322
2556
  }
2323
- return active;
2324
- }
2325
- function countActiveWorkersGlobal() {
2326
- let active = 0;
2327
- for (const run of listRunRecords()) active += countActiveWorkersForRun(run);
2328
- return active;
2557
+ const mapped = roleLaneToDispatchLane(key);
2558
+ if (mapped) return mapped;
2559
+ if (key === "implement" || key === "repair" || key === "coding") return "implementation";
2560
+ if (key === "land" || key === "merge") return "landing";
2561
+ return void 0;
2329
2562
  }
2330
- function observeRunnerResourceGate(input) {
2331
- const { perWorkerMemBytes, memReserveBytes, memUtilization, configuredMaxWorkers } = resolveResourceConfig(
2332
- input.config,
2333
- input.configuredMaxWorkersOverride
2334
- );
2335
- const totalMemBytes = input.totalMemBytes ?? os2.totalmem();
2336
- const freeMemBytes = input.freeMemBytes ?? readAvailableMemBytes();
2337
- const activeWorkers = input.activeWorkers ?? countActiveWorkersGlobal();
2338
- const budgetBytes = Math.max(0, Math.floor(totalMemBytes * memUtilization) - memReserveBytes);
2339
- const capacityFromTotal = Math.max(0, Math.floor(budgetBytes / perWorkerMemBytes));
2340
- const capacityFromFree = Math.max(0, Math.floor(Math.max(0, freeMemBytes - memReserveBytes) / perWorkerMemBytes));
2341
- const autoCap = computeAutoMaxWorkers(totalMemBytes, { perWorkerMemBytes, memReserveBytes, memUtilization });
2342
- const targetCap = configuredMaxWorkers ?? autoCap;
2343
- const maxConcurrentWorkers = Math.max(0, Math.min(targetCap, capacityFromTotal));
2344
- const slotsByCapacity = Math.max(0, maxConcurrentWorkers - activeWorkers);
2345
- const slotsByFreeMem = capacityFromFree;
2346
- let slotsAvailable = Math.min(slotsByCapacity, slotsByFreeMem);
2347
- const skipDisk = input.skipDiskGate || process.env.KYNVER_RESOURCE_GATE_SKIP_DISK === "1";
2348
- const diskGate = skipDisk ? void 0 : observeRunnerDiskGate({
2349
- diskPath: input.diskPath?.trim() || process.env.KYNVER_DISK_GUARD_PATH?.trim() || "/"
2350
- });
2351
- if (diskGate && !diskGate.ok) slotsAvailable = 0;
2352
- let reason = null;
2353
- if (slotsAvailable <= 0) {
2354
- if (diskGate && !diskGate.ok) {
2355
- reason = diskGate.reason ?? "disk gate blocked worker admission";
2356
- } else if (activeWorkers >= maxConcurrentWorkers) {
2357
- reason = `at worker limit (${activeWorkers}/${maxConcurrentWorkers} running)`;
2358
- } else if (capacityFromFree <= 0) {
2359
- reason = "insufficient free memory \u2014 waiting for workers to finish";
2360
- } else {
2361
- reason = "no worker slots available";
2362
- }
2363
- }
2364
- return {
2365
- ok: slotsAvailable > 0,
2366
- totalMemBytes,
2367
- freeMemBytes,
2368
- memReserveBytes,
2369
- perWorkerMemBytes,
2370
- configuredMaxWorkers,
2371
- autoCap,
2372
- capacityWorkers: capacityFromTotal,
2373
- maxConcurrentWorkers,
2374
- activeWorkers,
2375
- slotsAvailable,
2376
- reason,
2377
- ...diskGate ? { diskGate } : {}
2378
- };
2563
+ function resolveDispatchNextLaneFilter(raw) {
2564
+ return normalizeDispatchNextLaneFilter(raw) ?? "any";
2379
2565
  }
2380
2566
 
2381
2567
  // src/worker-persona-catalog.ts
@@ -3639,15 +3825,15 @@ function resolveModelFallback(startedModel, launchModel, providerDefault) {
3639
3825
  }
3640
3826
 
3641
3827
  // src/retry-limits.ts
3642
- function positiveInt2(value, fallback) {
3828
+ function positiveInt3(value, fallback) {
3643
3829
  const n = Number(value);
3644
3830
  if (!Number.isFinite(n) || n <= 0) return fallback;
3645
3831
  return Math.floor(n);
3646
3832
  }
3647
3833
  function readHarnessRetryLimits() {
3648
3834
  return {
3649
- maxTaskAttempts: positiveInt2(process.env.KYNVER_MAX_TASK_ATTEMPTS, 4),
3650
- dispatchCooldownMs: positiveInt2(process.env.KYNVER_DISPATCH_COOLDOWN_MS, 5e3)
3835
+ maxTaskAttempts: positiveInt3(process.env.KYNVER_MAX_TASK_ATTEMPTS, 4),
3836
+ dispatchCooldownMs: positiveInt3(process.env.KYNVER_DISPATCH_COOLDOWN_MS, 5e3)
3651
3837
  };
3652
3838
  }
3653
3839
 
@@ -3688,22 +3874,12 @@ function resolveHarnessLeaseOwnerForRenewal(input) {
3688
3874
  }
3689
3875
 
3690
3876
  // src/runner-identity.ts
3691
- import os4 from "node:os";
3877
+ import os5 from "node:os";
3692
3878
 
3693
3879
  // src/box-resource-snapshot-shared.ts
3694
- import os3 from "node:os";
3695
- function normalizeWorkerPoolBoxKind(raw) {
3696
- const kind = (raw ?? "").trim().toLowerCase();
3697
- if (kind === "ghost" || kind === "forge") return kind;
3698
- if (kind.includes("forge")) return "forge";
3699
- if (kind.includes("ghost") || kind.includes("openclaw")) return "ghost";
3700
- return "forge";
3701
- }
3702
- function resolveBoxKindFromEnv(env = process.env) {
3703
- return normalizeWorkerPoolBoxKind(env.KYNVER_BOX_KIND ?? env.KYNVER_AGENT_OS_SLUG ?? "forge");
3704
- }
3880
+ import os4 from "node:os";
3705
3881
  function defaultBoxId(boxKind, hostLabel) {
3706
- const host = (hostLabel ?? os3.hostname()).trim().toLowerCase().replace(/\s+/g, "-") || "unknown-host";
3882
+ const host = (hostLabel ?? os4.hostname()).trim().toLowerCase().replace(/\s+/g, "-") || "unknown-host";
3707
3883
  return `${boxKind}:${host}`;
3708
3884
  }
3709
3885
 
@@ -3714,10 +3890,10 @@ function trimOrNull5(value) {
3714
3890
  }
3715
3891
  function resolveRunnerPresencePayload(input = {}) {
3716
3892
  const env = input.env ?? process.env;
3717
- const runnerId = trimOrNull5(env.KYNVER_RUNTIME_ID) ?? trimOrNull5(env.OPENCLAW_RUNTIME_ID) ?? trimOrNull5(env.HOSTNAME) ?? os4.hostname();
3893
+ const runnerId = trimOrNull5(env.KYNVER_RUNTIME_ID) ?? trimOrNull5(env.OPENCLAW_RUNTIME_ID) ?? trimOrNull5(env.HOSTNAME) ?? os5.hostname();
3718
3894
  return {
3719
3895
  runnerId,
3720
- hostname: trimOrNull5(env.HOSTNAME) ?? os4.hostname(),
3896
+ hostname: trimOrNull5(env.HOSTNAME) ?? os5.hostname(),
3721
3897
  profile: trimOrNull5(env.KYNVER_RUNNER_PROFILE) ?? trimOrNull5(env.OPENCLAW_RUNNER_PROFILE),
3722
3898
  harnessRepo: trimOrNull5(env.KYNVER_HARNESS_REPO) ?? trimOrNull5(env.KYNVER_DEFAULT_REPO),
3723
3899
  runId: input.runId ?? null
@@ -3893,13 +4069,13 @@ function buildPrompt(input) {
3893
4069
  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."
3894
4070
  ];
3895
4071
  const mergeGateLines = compact ? [
3896
- "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)."
4072
+ "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)."
3897
4073
  ] : [
3898
4074
  "GitHub Actions merge-gate cost control (Kynver/Hermes PRs):",
3899
- "- Prefer local cached package verification (`node scripts/verify-pr-local.mjs --emit-json`) and Vercel preview evidence before GitHub Actions.",
4075
+ "- 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.",
3900
4076
  "- 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.",
3901
- "- Record evidence: POST `/api/agent-os/by-id/<agentOsId>/pr-merge-gate/refresh` with prUrl + local/vercel payloads.",
3902
- "- Request the final Actions run only when local + Vercel are green: POST `.../pr-merge-gate/request-run` (applies `merge-gate` label).",
4077
+ "- Record evidence: POST `/api/agent-os/by-id/<agentOsId>/pr-merge-gate/refresh` with prUrl + local payloads.",
4078
+ "- Request the final Actions run only when local verification is green: POST `.../pr-merge-gate/request-run` (applies `merge-gate` label).",
3903
4079
  "- Empty failed Actions jobs (no runner/steps/logs) are infra/quota \u2014 do not enter repair loops; escalate to operator."
3904
4080
  ];
3905
4081
  const planArtifactLines = compact ? [
@@ -3953,7 +4129,7 @@ function buildPrompt(input) {
3953
4129
  // src/providers/cursor.ts
3954
4130
  import { closeSync as closeSync4, existsSync as existsSync17, mkdirSync as mkdirSync3, openSync as openSync4, statSync as statSync4, unlinkSync } from "node:fs";
3955
4131
  import { spawn as spawn4 } from "node:child_process";
3956
- import os5 from "node:os";
4132
+ import os6 from "node:os";
3957
4133
  import path16 from "node:path";
3958
4134
 
3959
4135
  // src/providers/cursor-windows.ts
@@ -4064,7 +4240,7 @@ function positiveIntEnv(name, fallback) {
4064
4240
  return Number.isFinite(parsed) && parsed >= 0 ? Math.floor(parsed) : fallback;
4065
4241
  }
4066
4242
  function cursorStartLockPath() {
4067
- const root = process.env.KYNVER_CURSOR_START_LOCK_DIR?.trim() || path16.join(os5.homedir(), ".kynver", "locks");
4243
+ const root = process.env.KYNVER_CURSOR_START_LOCK_DIR?.trim() || path16.join(os6.homedir(), ".kynver", "locks");
4068
4244
  mkdirSync3(root, { recursive: true });
4069
4245
  return path16.join(root, "cursor-agent-start.lock");
4070
4246
  }
@@ -4799,8 +4975,8 @@ function dirtyPathsCoveredByDisposableRemoval(changedFiles, removed) {
4799
4975
  if (removed.length === 0) return false;
4800
4976
  const removedSet = new Set(removed.map((p) => normalizeRelativePath(p)));
4801
4977
  return material.every((line) => {
4802
- const path65 = normalizeRelativePath(pathFromGitStatusLine(line));
4803
- return removedSet.has(path65);
4978
+ const path67 = normalizeRelativePath(pathFromGitStatusLine(line));
4979
+ return removedSet.has(path67);
4804
4980
  });
4805
4981
  }
4806
4982
 
@@ -8113,6 +8289,45 @@ function discardDisposableCli(args) {
8113
8289
  if (!result.ok) process.exit(1);
8114
8290
  }
8115
8291
 
8292
+ // src/daemon-box-identity.ts
8293
+ import os7 from "node:os";
8294
+ function emitDaemonIdentityMessage(level, message) {
8295
+ console.error(JSON.stringify({ event: "daemon_identity", level, message }));
8296
+ }
8297
+ function validateDaemonInstallIdentity(config = loadUserConfig(), env = process.env) {
8298
+ const box = resolveBoxIdentity(env, config);
8299
+ const cap = resolveWorkerCap({
8300
+ config,
8301
+ totalMemBytes: os7.totalmem(),
8302
+ env
8303
+ });
8304
+ const warnings = [...box.warnings];
8305
+ const errors = [];
8306
+ if (!config.boxKind?.trim() && !env.KYNVER_BOX_KIND?.trim()) {
8307
+ warnings.push(
8308
+ "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"
8309
+ );
8310
+ }
8311
+ if (box.slugInferenceBlocked) {
8312
+ const strict = env.KYNVER_DAEMON_STRICT_IDENTITY === "1" || env.KYNVER_DAEMON_STRICT_IDENTITY === "true";
8313
+ const msg = "ambiguous box identity: KYNVER_AGENT_OS_SLUG is set without KYNVER_BOX_KIND or config.boxKind; treating this host as forge";
8314
+ if (strict) errors.push(msg);
8315
+ else warnings.push(msg);
8316
+ }
8317
+ const ok = errors.length === 0;
8318
+ for (const warning of warnings) emitDaemonIdentityMessage("warn", warning);
8319
+ for (const error of errors) emitDaemonIdentityMessage("error", error);
8320
+ return {
8321
+ ok,
8322
+ box,
8323
+ workerCapSource: cap.workerCapSource,
8324
+ maxConcurrentWorkers: cap.configuredMaxWorkers ?? cap.autoCap,
8325
+ autoCap: cap.autoCap,
8326
+ warnings,
8327
+ errors
8328
+ };
8329
+ }
8330
+
8116
8331
  // src/cron/cron-env.ts
8117
8332
  import { existsSync as existsSync27 } from "node:fs";
8118
8333
  import { homedir as homedir11 } from "node:os";
@@ -8578,7 +8793,7 @@ async function runKynverCronTick(opts = {}) {
8578
8793
  }
8579
8794
 
8580
8795
  // src/pipeline-tick.ts
8581
- import path53 from "node:path";
8796
+ import path54 from "node:path";
8582
8797
 
8583
8798
  // src/pipeline-dispatch.ts
8584
8799
  var RESERVED_REVIEW_STARTS = 1;
@@ -8709,10 +8924,10 @@ function resolvePipelineMaxStarts(resourceGate, operatorTick) {
8709
8924
  }
8710
8925
 
8711
8926
  // src/box-resource-snapshot.ts
8712
- import os6 from "node:os";
8927
+ import os8 from "node:os";
8713
8928
  function buildBoxResourceSnapshotFromGate(gate, input = {}) {
8714
- const boxKind = (input.boxKind ?? resolveBoxKindFromEnv()).trim().toLowerCase() || "forge";
8715
- const hostLabel = input.hostLabel ?? os6.hostname();
8929
+ const boxKind = (input.boxKind ?? resolveBoxKindFromConfig(loadUserConfig())).trim().toLowerCase() || "forge";
8930
+ const hostLabel = input.hostLabel ?? os8.hostname();
8716
8931
  const boxId = input.boxId ?? defaultBoxId(boxKind, hostLabel);
8717
8932
  return {
8718
8933
  boxId,
@@ -8787,7 +9002,8 @@ async function syncActiveWorkerPlanProgress(runId, args) {
8787
9002
 
8788
9003
  // src/workspace-runtime-config.ts
8789
9004
  function shouldApplyWorkspaceRuntimePreferences(env = process.env) {
8790
- return resolveBoxKindFromEnv(env) !== "forge";
9005
+ const config = loadUserConfig();
9006
+ return resolveBoxKindFromConfig(config, env) !== "forge";
8791
9007
  }
8792
9008
  function configuredMaxWorkersOverrideForBox(preferences, env = process.env) {
8793
9009
  if (!shouldApplyWorkspaceRuntimePreferences(env)) return void 0;
@@ -8814,7 +9030,7 @@ async function fetchWorkspaceRuntimePreferences(agentOsId, args) {
8814
9030
  }
8815
9031
 
8816
9032
  // src/cleanup.ts
8817
- import path51 from "node:path";
9033
+ import path52 from "node:path";
8818
9034
 
8819
9035
  // src/cleanup-guards.ts
8820
9036
  import path39 from "node:path";
@@ -8985,11 +9201,11 @@ var LIVE_SKIP_REASONS = /* @__PURE__ */ new Set([
8985
9201
  function collectPreservedLivePaths(actions, skips) {
8986
9202
  const out = [];
8987
9203
  const seen = /* @__PURE__ */ new Set();
8988
- const push = (path65, reason, detail) => {
8989
- const key = `${path65}\0${reason}`;
9204
+ const push = (path67, reason, detail) => {
9205
+ const key = `${path67}\0${reason}`;
8990
9206
  if (seen.has(key) || out.length >= MAX_PRESERVED_LIVE_PATH_SAMPLES) return;
8991
9207
  seen.add(key);
8992
- out.push({ path: path65, reason, ...detail ? { detail } : {} });
9208
+ out.push({ path: path67, reason, ...detail ? { detail } : {} });
8993
9209
  };
8994
9210
  for (const skip2 of skips) {
8995
9211
  if (!LIVE_SKIP_REASONS.has(skip2.reason)) continue;
@@ -9118,8 +9334,7 @@ function scanStaleRunDirectoryCandidates(opts) {
9118
9334
  }
9119
9335
 
9120
9336
  // src/cleanup-execute.ts
9121
- import { existsSync as existsSync32, rmSync as rmSync3 } from "node:fs";
9122
- import path43 from "node:path";
9337
+ import { existsSync as existsSync33, rmSync as rmSync4 } from "node:fs";
9123
9338
 
9124
9339
  // src/cleanup-dir-size.ts
9125
9340
  import { execFileSync } from "node:child_process";
@@ -9172,50 +9387,232 @@ function directorySizeBytes(root, maxEntries = 5e4) {
9172
9387
  return total;
9173
9388
  }
9174
9389
 
9175
- // src/cleanup-execute.ts
9176
- function skipRunMetadataRemoval(candidate) {
9177
- const harnessRoot = candidate.harnessRoot;
9178
- if (!harnessRoot || !isHarnessRunMetadataPath(candidate.path, harnessRoot)) return null;
9390
+ // src/cleanup-remove-path.ts
9391
+ import { existsSync as existsSync32, rmSync as rmSync3 } from "node:fs";
9392
+
9393
+ // src/cleanup-path-ownership.ts
9394
+ import { lstatSync as lstatSync2, readdirSync as readdirSync11 } from "node:fs";
9395
+ function readPathOwnership(targetPath) {
9396
+ try {
9397
+ const st = lstatSync2(targetPath);
9398
+ const effectiveUid = typeof process.getuid === "function" ? process.getuid() : null;
9399
+ const effectiveGid = typeof process.getgid === "function" ? process.getgid() : null;
9400
+ const foreign = effectiveUid !== null && (st.uid !== effectiveUid || effectiveGid !== null && st.gid !== effectiveGid);
9401
+ return { uid: st.uid, gid: st.gid, foreign };
9402
+ } catch {
9403
+ return null;
9404
+ }
9405
+ }
9406
+ function pathHasForeignOwnedEntry(targetPath, maxEntries = 32) {
9407
+ const root = readPathOwnership(targetPath);
9408
+ if (!root) return false;
9409
+ if (root.foreign) return true;
9410
+ try {
9411
+ const names = readdirSync11(targetPath);
9412
+ let checked = 0;
9413
+ for (const name of names) {
9414
+ if (checked >= maxEntries) break;
9415
+ const child = `${targetPath.replace(/\/$/, "")}/${name}`;
9416
+ const info = readPathOwnership(child);
9417
+ checked += 1;
9418
+ if (info?.foreign) return true;
9419
+ }
9420
+ } catch {
9421
+ }
9422
+ return false;
9423
+ }
9424
+
9425
+ // src/cleanup-privileged-remove.ts
9426
+ import { spawnSync as spawnSync4 } from "node:child_process";
9427
+ import path44 from "node:path";
9428
+
9429
+ // src/cleanup-harness-path-validate.ts
9430
+ import path43 from "node:path";
9431
+ function isHarnessDependencyCachePath(targetPath, harnessRoot, worktreesDir, cacheDirName) {
9432
+ const resolved = path43.resolve(targetPath);
9433
+ const suffix = `${path43.sep}${cacheDirName}`;
9434
+ const cachePath = resolved.endsWith(suffix) ? resolved : null;
9435
+ if (!cachePath) return "path_outside_harness";
9436
+ const rel = path43.relative(worktreesDir, cachePath);
9437
+ if (rel.startsWith("..") || path43.isAbsolute(rel)) return "path_outside_harness";
9438
+ const parts = rel.split(path43.sep);
9439
+ if (parts.length < 3 || parts[parts.length - 1] !== cacheDirName) return "path_outside_harness";
9440
+ if (!resolved.startsWith(path43.resolve(harnessRoot))) return "path_outside_harness";
9441
+ return null;
9442
+ }
9443
+ function isHarnessNodeModulesPath(targetPath, harnessRoot, worktreesDir) {
9444
+ return isHarnessDependencyCachePath(targetPath, harnessRoot, worktreesDir, "node_modules");
9445
+ }
9446
+ function isHarnessNextCachePath(targetPath, harnessRoot, worktreesDir) {
9447
+ return isHarnessDependencyCachePath(targetPath, harnessRoot, worktreesDir, ".next");
9448
+ }
9449
+ function isHarnessBuildCachePath(targetPath, harnessRoot, worktreesDir) {
9450
+ const resolved = path43.resolve(targetPath);
9451
+ const relToWt = path43.relative(worktreesDir, resolved);
9452
+ if (relToWt.startsWith("..") || path43.isAbsolute(relToWt)) return "path_outside_harness";
9453
+ const parts = relToWt.split(path43.sep);
9454
+ if (parts.length < 3) return "path_outside_harness";
9455
+ if (!resolved.startsWith(path43.resolve(harnessRoot))) return "path_outside_harness";
9456
+ return null;
9457
+ }
9458
+ function isHarnessGeneratedCachePath(targetPath, harnessRoot, worktreesDir) {
9459
+ const resolved = path43.resolve(targetPath);
9460
+ return isHarnessNodeModulesPath(resolved, harnessRoot, worktreesDir) === null || isHarnessNextCachePath(resolved, harnessRoot, worktreesDir) === null || isHarnessBuildCachePath(resolved, harnessRoot, worktreesDir) === null;
9461
+ }
9462
+
9463
+ // src/cleanup-privileged-remove.ts
9464
+ function resolvePrivilegedCleanupMode() {
9465
+ const raw = (process.env.KYNVER_CLEANUP_PRIVILEGED ?? "auto").trim().toLowerCase();
9466
+ if (raw === "0" || raw === "false" || raw === "off" || raw === "no") return "off";
9467
+ if (raw === "1" || raw === "true" || raw === "force" || raw === "yes") return "force";
9468
+ return "auto";
9469
+ }
9470
+ function runSudoNonInteractive(argv) {
9471
+ const res = spawnSync4("sudo", ["-n", ...argv], {
9472
+ encoding: "utf8",
9473
+ stdio: ["ignore", "pipe", "pipe"]
9474
+ });
9179
9475
  return {
9180
- ...candidate,
9181
- executed: false,
9182
- skipped: true,
9183
- skipReason: "run_metadata_protected"
9476
+ ok: res.status === 0,
9477
+ stderr: `${res.stderr ?? ""}${res.stdout ?? ""}`.trim()
9184
9478
  };
9185
9479
  }
9186
- function removeDependencyCache(candidate, execute) {
9187
- const metadataSkip = skipRunMetadataRemoval(candidate);
9188
- if (metadataSkip) return metadataSkip;
9480
+ function tryPrivilegedReclaimHarnessCache(targetPath, harnessRoot, worktreesDir) {
9481
+ if (!isHarnessGeneratedCachePath(targetPath, harnessRoot, worktreesDir)) {
9482
+ return { ok: false, error: "path is not an allowed harness generated cache" };
9483
+ }
9484
+ const effectiveUid = typeof process.getuid === "function" ? process.getuid() : null;
9485
+ const effectiveGid = typeof process.getgid === "function" ? process.getgid() : null;
9486
+ if (effectiveUid === null || effectiveGid === null) {
9487
+ return { ok: false, error: "privileged reclaim requires POSIX uid/gid" };
9488
+ }
9489
+ const chown = runSudoNonInteractive([
9490
+ "chown",
9491
+ "-R",
9492
+ `${effectiveUid}:${effectiveGid}`,
9493
+ path44.resolve(targetPath)
9494
+ ]);
9495
+ if (chown.ok) {
9496
+ return { ok: true, method: "chown_then_rm" };
9497
+ }
9498
+ const rm = runSudoNonInteractive(["rm", "-rf", path44.resolve(targetPath)]);
9499
+ if (rm.ok) {
9500
+ return { ok: true, method: "sudo_rm" };
9501
+ }
9502
+ const detail = chown.stderr || rm.stderr || "sudo -n failed (password required or not permitted)";
9503
+ return {
9504
+ ok: false,
9505
+ error: detail
9506
+ };
9507
+ }
9508
+ 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.";
9509
+
9510
+ // src/cleanup-remove-path.ts
9511
+ function permissionFailure(error) {
9512
+ const code = error?.code;
9513
+ return code === "EACCES" || code === "EPERM";
9514
+ }
9515
+ function removeHarnessGeneratedPath(candidate, execute, deps = {}) {
9189
9516
  if (!existsSync32(candidate.path)) {
9190
- return {
9191
- ...candidate,
9192
- executed: false,
9193
- skipped: true,
9194
- skipReason: "missing_worktree"
9195
- };
9517
+ return { executed: false, skipped: true, skipReason: "missing_worktree" };
9196
9518
  }
9197
9519
  if (!execute) {
9198
- return { ...candidate, executed: false, skipped: true, skipReason: "dry_run" };
9520
+ return { executed: false, skipped: true, skipReason: "dry_run" };
9199
9521
  }
9522
+ const harnessRoot = candidate.harnessRoot;
9523
+ const worktreesDir = harnessRoot ? harnessWorktreesDir(harnessRoot) : null;
9524
+ const bytesBefore = candidate.bytes ?? directorySizeBytes(candidate.path);
9525
+ const bytesReported = bytesBefore ?? void 0;
9526
+ const removePath = deps.removePath ?? rmSync3;
9527
+ const hasForeignOwnedEntry = deps.hasForeignOwnedEntry ?? pathHasForeignOwnedEntry;
9200
9528
  try {
9201
- const bytesBefore = candidate.bytes ?? directorySizeBytes(candidate.path);
9202
- rmSync3(candidate.path, { recursive: true, force: true });
9203
- return {
9204
- ...candidate,
9205
- bytes: bytesBefore,
9206
- executed: true,
9207
- skipped: false
9208
- };
9529
+ removePath(candidate.path, { recursive: true, force: true });
9530
+ return { executed: true, skipped: false, bytes: bytesReported };
9209
9531
  } catch (error) {
9532
+ if (!permissionFailure(error) || !harnessRoot || !worktreesDir) {
9533
+ return {
9534
+ executed: false,
9535
+ skipped: true,
9536
+ skipReason: "remove_failed",
9537
+ error: error.message
9538
+ };
9539
+ }
9540
+ const foreign = hasForeignOwnedEntry(candidate.path);
9541
+ const mode = resolvePrivilegedCleanupMode();
9542
+ const shouldTryPrivileged = mode === "force" || mode === "auto" && foreign;
9543
+ if (!shouldTryPrivileged) {
9544
+ return foreign ? {
9545
+ executed: false,
9546
+ skipped: true,
9547
+ skipReason: "foreign_owner",
9548
+ error: `${error.message}; ${HARNESS_ROOT_OWNED_CACHE_RUNBOOK}`
9549
+ } : {
9550
+ executed: false,
9551
+ skipped: true,
9552
+ skipReason: "remove_failed",
9553
+ error: error.message
9554
+ };
9555
+ }
9556
+ const reclaim = tryPrivilegedReclaimHarnessCache(candidate.path, harnessRoot, worktreesDir);
9557
+ if (reclaim.ok && reclaim.method === "sudo_rm") {
9558
+ return {
9559
+ executed: true,
9560
+ skipped: false,
9561
+ bytes: bytesReported,
9562
+ privilegedReclaim: true
9563
+ };
9564
+ }
9565
+ if (reclaim.ok) {
9566
+ try {
9567
+ removePath(candidate.path, { recursive: true, force: true });
9568
+ return {
9569
+ executed: true,
9570
+ skipped: false,
9571
+ bytes: bytesReported,
9572
+ privilegedReclaim: true
9573
+ };
9574
+ } catch (retryError) {
9575
+ return {
9576
+ executed: false,
9577
+ skipped: true,
9578
+ skipReason: "foreign_owner",
9579
+ error: `${retryError.message}; privileged chown succeeded but rm still failed`
9580
+ };
9581
+ }
9582
+ }
9210
9583
  return {
9211
- ...candidate,
9212
9584
  executed: false,
9213
9585
  skipped: true,
9214
- skipReason: "remove_failed",
9215
- error: error.message
9586
+ skipReason: "foreign_owner",
9587
+ error: `${error.message}; privileged reclaim failed: ${reclaim.error}; ${HARNESS_ROOT_OWNED_CACHE_RUNBOOK}`
9216
9588
  };
9217
9589
  }
9218
9590
  }
9591
+
9592
+ // src/cleanup-execute.ts
9593
+ function skipRunMetadataRemoval(candidate) {
9594
+ const harnessRoot = candidate.harnessRoot;
9595
+ if (!harnessRoot || !isHarnessRunMetadataPath(candidate.path, harnessRoot)) return null;
9596
+ return {
9597
+ ...candidate,
9598
+ executed: false,
9599
+ skipped: true,
9600
+ skipReason: "run_metadata_protected"
9601
+ };
9602
+ }
9603
+ function removeDependencyCache(candidate, execute) {
9604
+ const metadataSkip = skipRunMetadataRemoval(candidate);
9605
+ if (metadataSkip) return metadataSkip;
9606
+ const outcome = removeHarnessGeneratedPath(candidate, execute);
9607
+ return {
9608
+ ...candidate,
9609
+ bytes: outcome.bytes ?? candidate.bytes,
9610
+ executed: outcome.executed,
9611
+ skipped: outcome.skipped,
9612
+ skipReason: outcome.skipReason,
9613
+ error: outcome.error
9614
+ };
9615
+ }
9219
9616
  function removeNodeModules(candidate, execute) {
9220
9617
  return removeDependencyCache(candidate, execute);
9221
9618
  }
@@ -9228,7 +9625,7 @@ function removeBuildCache(candidate, execute) {
9228
9625
  function removeRunDirectory(candidate, execute) {
9229
9626
  const metadataSkip = skipRunMetadataRemoval(candidate);
9230
9627
  if (metadataSkip) return metadataSkip;
9231
- if (!existsSync32(candidate.path)) {
9628
+ if (!existsSync33(candidate.path)) {
9232
9629
  return {
9233
9630
  ...candidate,
9234
9631
  executed: false,
@@ -9241,7 +9638,7 @@ function removeRunDirectory(candidate, execute) {
9241
9638
  }
9242
9639
  try {
9243
9640
  const bytesBefore = candidate.bytes ?? directorySizeBytes(candidate.path);
9244
- rmSync3(candidate.path, { recursive: true, force: true });
9641
+ rmSync4(candidate.path, { recursive: true, force: true });
9245
9642
  return {
9246
9643
  ...candidate,
9247
9644
  bytes: bytesBefore,
@@ -9261,7 +9658,7 @@ function removeRunDirectory(candidate, execute) {
9261
9658
  function removeWorktree(candidate, execute) {
9262
9659
  const metadataSkip = skipRunMetadataRemoval(candidate);
9263
9660
  if (metadataSkip) return metadataSkip;
9264
- if (!existsSync32(candidate.path)) {
9661
+ if (!existsSync33(candidate.path)) {
9265
9662
  return {
9266
9663
  ...candidate,
9267
9664
  executed: false,
@@ -9278,8 +9675,8 @@ function removeWorktree(candidate, execute) {
9278
9675
  if (repo) {
9279
9676
  git(repo, ["worktree", "remove", "--force", candidate.path], { allowFailure: true });
9280
9677
  }
9281
- if (existsSync32(candidate.path)) {
9282
- rmSync3(candidate.path, { recursive: true, force: true });
9678
+ if (existsSync33(candidate.path)) {
9679
+ rmSync4(candidate.path, { recursive: true, force: true });
9283
9680
  }
9284
9681
  return {
9285
9682
  ...candidate,
@@ -9297,37 +9694,10 @@ function removeWorktree(candidate, execute) {
9297
9694
  };
9298
9695
  }
9299
9696
  }
9300
- function isHarnessDependencyCachePath(targetPath, harnessRoot, worktreesDir, cacheDirName) {
9301
- const resolved = path43.resolve(targetPath);
9302
- const suffix = `${path43.sep}${cacheDirName}`;
9303
- const cachePath = resolved.endsWith(suffix) ? resolved : null;
9304
- if (!cachePath) return "path_outside_harness";
9305
- const rel = path43.relative(worktreesDir, cachePath);
9306
- if (rel.startsWith("..") || path43.isAbsolute(rel)) return "path_outside_harness";
9307
- const parts = rel.split(path43.sep);
9308
- if (parts.length < 3 || parts[parts.length - 1] !== cacheDirName) return "path_outside_harness";
9309
- if (!resolved.startsWith(path43.resolve(harnessRoot))) return "path_outside_harness";
9310
- return null;
9311
- }
9312
- function isHarnessNodeModulesPath(targetPath, harnessRoot, worktreesDir) {
9313
- return isHarnessDependencyCachePath(targetPath, harnessRoot, worktreesDir, "node_modules");
9314
- }
9315
- function isHarnessNextCachePath(targetPath, harnessRoot, worktreesDir) {
9316
- return isHarnessDependencyCachePath(targetPath, harnessRoot, worktreesDir, ".next");
9317
- }
9318
- function isHarnessBuildCachePath(targetPath, harnessRoot, worktreesDir) {
9319
- const resolved = path43.resolve(targetPath);
9320
- const relToWt = path43.relative(worktreesDir, resolved);
9321
- if (relToWt.startsWith("..") || path43.isAbsolute(relToWt)) return "path_outside_harness";
9322
- const parts = relToWt.split(path43.sep);
9323
- if (parts.length < 3) return "path_outside_harness";
9324
- if (!resolved.startsWith(path43.resolve(harnessRoot))) return "path_outside_harness";
9325
- return null;
9326
- }
9327
9697
 
9328
9698
  // src/cleanup-scan.ts
9329
- import { existsSync as existsSync33, readdirSync as readdirSync11, statSync as statSync9 } from "node:fs";
9330
- import path44 from "node:path";
9699
+ import { existsSync as existsSync34, readdirSync as readdirSync12, statSync as statSync9 } from "node:fs";
9700
+ import path45 from "node:path";
9331
9701
  function pathAgeMs2(target, now) {
9332
9702
  try {
9333
9703
  const mtime = statSync9(target).mtimeMs;
@@ -9337,16 +9707,16 @@ function pathAgeMs2(target, now) {
9337
9707
  }
9338
9708
  }
9339
9709
  function isPathInside(child, parent) {
9340
- const rel = path44.relative(parent, child);
9341
- return rel === "" || !rel.startsWith("..") && !path44.isAbsolute(rel);
9710
+ const rel = path45.relative(parent, child);
9711
+ return rel === "" || !rel.startsWith("..") && !path45.isAbsolute(rel);
9342
9712
  }
9343
9713
  function collectBuildCacheForWorktree(worktreePath, opts, seen, meta) {
9344
9714
  const out = [];
9345
9715
  for (const rel of HARNESS_BUILD_CACHE_RELATIVE_PATHS) {
9346
9716
  if (rel === ".next") continue;
9347
- const target = path44.join(worktreePath, rel);
9348
- if (!existsSync33(target)) continue;
9349
- const resolved = path44.resolve(target);
9717
+ const target = path45.join(worktreePath, rel);
9718
+ if (!existsSync34(target)) continue;
9719
+ const resolved = path45.resolve(target);
9350
9720
  if (seen.has(resolved)) continue;
9351
9721
  if (!isPathInside(resolved, opts.harnessRoot)) continue;
9352
9722
  seen.add(resolved);
@@ -9375,13 +9745,13 @@ function scanBuildCacheCandidates(opts) {
9375
9745
  })
9376
9746
  );
9377
9747
  }
9378
- if (!opts.includeOrphans || !existsSync33(opts.worktreesDir)) return candidates;
9379
- for (const runEntry of readdirSync11(opts.worktreesDir, { withFileTypes: true })) {
9748
+ if (!opts.includeOrphans || !existsSync34(opts.worktreesDir)) return candidates;
9749
+ for (const runEntry of readdirSync12(opts.worktreesDir, { withFileTypes: true })) {
9380
9750
  if (!runEntry.isDirectory()) continue;
9381
- const runPath = path44.join(opts.worktreesDir, runEntry.name);
9382
- for (const workerEntry of readdirSync11(runPath, { withFileTypes: true })) {
9751
+ const runPath = path45.join(opts.worktreesDir, runEntry.name);
9752
+ for (const workerEntry of readdirSync12(runPath, { withFileTypes: true })) {
9383
9753
  if (!workerEntry.isDirectory()) continue;
9384
- const worktreePath = path44.join(runPath, workerEntry.name);
9754
+ const worktreePath = path45.join(runPath, workerEntry.name);
9385
9755
  candidates.push(
9386
9756
  ...collectBuildCacheForWorktree(worktreePath, opts, seen, {
9387
9757
  runId: runEntry.name,
@@ -9402,7 +9772,7 @@ function scanWorktreeCandidates(opts) {
9402
9772
  for (const entry of opts.index.values()) {
9403
9773
  if (opts.runIdFilter && entry.runId !== opts.runIdFilter) continue;
9404
9774
  const resolved = entry.worktreePath;
9405
- if (!existsSync33(resolved)) continue;
9775
+ if (!existsSync34(resolved)) continue;
9406
9776
  if (seen.has(resolved)) continue;
9407
9777
  seen.add(resolved);
9408
9778
  candidates.push({
@@ -9416,24 +9786,24 @@ function scanWorktreeCandidates(opts) {
9416
9786
  });
9417
9787
  }
9418
9788
  }
9419
- if (!orphanEnabled || !existsSync33(opts.worktreesDir)) return candidates;
9789
+ if (!orphanEnabled || !existsSync34(opts.worktreesDir)) return candidates;
9420
9790
  const indexedPaths = /* @__PURE__ */ new Set();
9421
9791
  for (const entry of opts.index.values()) {
9422
- indexedPaths.add(path44.resolve(entry.worktreePath));
9792
+ indexedPaths.add(path45.resolve(entry.worktreePath));
9423
9793
  }
9424
- for (const runEntry of readdirSync11(opts.worktreesDir, { withFileTypes: true })) {
9794
+ for (const runEntry of readdirSync12(opts.worktreesDir, { withFileTypes: true })) {
9425
9795
  if (!runEntry.isDirectory()) continue;
9426
9796
  if (opts.runIdFilter && runEntry.name !== opts.runIdFilter) continue;
9427
- const runPath = path44.join(opts.worktreesDir, runEntry.name);
9797
+ const runPath = path45.join(opts.worktreesDir, runEntry.name);
9428
9798
  let workerEntries;
9429
9799
  try {
9430
- workerEntries = readdirSync11(runPath, { withFileTypes: true });
9800
+ workerEntries = readdirSync12(runPath, { withFileTypes: true });
9431
9801
  } catch {
9432
9802
  continue;
9433
9803
  }
9434
9804
  for (const workerEntry of workerEntries) {
9435
9805
  if (!workerEntry.isDirectory()) continue;
9436
- const worktreePath = path44.resolve(path44.join(runPath, workerEntry.name));
9806
+ const worktreePath = path45.resolve(path45.join(runPath, workerEntry.name));
9437
9807
  if (seen.has(worktreePath)) continue;
9438
9808
  if (indexedPaths.has(worktreePath)) continue;
9439
9809
  if (!isPathInside(worktreePath, opts.harnessRoot)) continue;
@@ -9452,8 +9822,8 @@ function scanWorktreeCandidates(opts) {
9452
9822
  }
9453
9823
 
9454
9824
  // src/cleanup-dependency-scan.ts
9455
- import { existsSync as existsSync34, readdirSync as readdirSync12, statSync as statSync10 } from "node:fs";
9456
- import path45 from "node:path";
9825
+ import { existsSync as existsSync35, readdirSync as readdirSync13, statSync as statSync10 } from "node:fs";
9826
+ import path46 from "node:path";
9457
9827
  var DEPENDENCY_CACHE_DIRS = [
9458
9828
  { dirName: "node_modules", kind: "remove_node_modules" },
9459
9829
  { dirName: ".next", kind: "remove_next_cache" }
@@ -9467,12 +9837,12 @@ function pathAgeMs3(target, now) {
9467
9837
  }
9468
9838
  }
9469
9839
  function isPathInside2(child, parent) {
9470
- const rel = path45.relative(parent, child);
9471
- return rel === "" || !rel.startsWith("..") && !path45.isAbsolute(rel);
9840
+ const rel = path46.relative(parent, child);
9841
+ return rel === "" || !rel.startsWith("..") && !path46.isAbsolute(rel);
9472
9842
  }
9473
9843
  function pushCandidate2(candidates, seen, opts, targetPath, kind, meta) {
9474
- if (!existsSync34(targetPath)) return;
9475
- const resolved = path45.resolve(targetPath);
9844
+ if (!existsSync35(targetPath)) return;
9845
+ const resolved = path46.resolve(targetPath);
9476
9846
  if (seen.has(resolved)) return;
9477
9847
  if (!isPathInside2(resolved, opts.harnessRoot)) return;
9478
9848
  seen.add(resolved);
@@ -9489,7 +9859,7 @@ function pushCandidate2(candidates, seen, opts, targetPath, kind, meta) {
9489
9859
  }
9490
9860
  function scanWorktreeDependencyCaches(candidates, seen, opts, worktreePath, meta) {
9491
9861
  for (const entry of DEPENDENCY_CACHE_DIRS) {
9492
- pushCandidate2(candidates, seen, opts, path45.join(worktreePath, entry.dirName), entry.kind, meta);
9862
+ pushCandidate2(candidates, seen, opts, path46.join(worktreePath, entry.dirName), entry.kind, meta);
9493
9863
  }
9494
9864
  }
9495
9865
  function scanDependencyCacheCandidates(opts) {
@@ -9503,20 +9873,20 @@ function scanDependencyCacheCandidates(opts) {
9503
9873
  repo: entry.run.repo
9504
9874
  });
9505
9875
  }
9506
- if (!opts.includeOrphans || !existsSync34(opts.worktreesDir)) return candidates;
9507
- for (const runEntry of readdirSync12(opts.worktreesDir, { withFileTypes: true })) {
9876
+ if (!opts.includeOrphans || !existsSync35(opts.worktreesDir)) return candidates;
9877
+ for (const runEntry of readdirSync13(opts.worktreesDir, { withFileTypes: true })) {
9508
9878
  if (!runEntry.isDirectory()) continue;
9509
9879
  if (opts.runIdFilter && runEntry.name !== opts.runIdFilter) continue;
9510
- const runPath = path45.join(opts.worktreesDir, runEntry.name);
9880
+ const runPath = path46.join(opts.worktreesDir, runEntry.name);
9511
9881
  let workerEntries;
9512
9882
  try {
9513
- workerEntries = readdirSync12(runPath, { withFileTypes: true });
9883
+ workerEntries = readdirSync13(runPath, { withFileTypes: true });
9514
9884
  } catch {
9515
9885
  continue;
9516
9886
  }
9517
9887
  for (const workerEntry of workerEntries) {
9518
9888
  if (!workerEntry.isDirectory()) continue;
9519
- const worktreePath = path45.join(runPath, workerEntry.name);
9889
+ const worktreePath = path46.join(runPath, workerEntry.name);
9520
9890
  scanWorktreeDependencyCaches(candidates, seen, opts, worktreePath, {
9521
9891
  runId: runEntry.name,
9522
9892
  worker: workerEntry.name
@@ -9527,8 +9897,8 @@ function scanDependencyCacheCandidates(opts) {
9527
9897
  }
9528
9898
 
9529
9899
  // src/cleanup-duplicate-worktrees.ts
9530
- import { existsSync as existsSync35, statSync as statSync11 } from "node:fs";
9531
- import path46 from "node:path";
9900
+ import { existsSync as existsSync36, statSync as statSync11 } from "node:fs";
9901
+ import path47 from "node:path";
9532
9902
  function pathAgeMs4(target, now) {
9533
9903
  try {
9534
9904
  const mtime = statSync11(target).mtimeMs;
@@ -9558,8 +9928,8 @@ function parseWorktreePorcelain(output) {
9558
9928
  return records;
9559
9929
  }
9560
9930
  function isUnderWorktreesDir(worktreePath, worktreesDir) {
9561
- const rel = path46.relative(path46.resolve(worktreesDir), path46.resolve(worktreePath));
9562
- return rel !== "" && !rel.startsWith("..") && !path46.isAbsolute(rel);
9931
+ const rel = path47.relative(path47.resolve(worktreesDir), path47.resolve(worktreePath));
9932
+ return rel !== "" && !rel.startsWith("..") && !path47.isAbsolute(rel);
9563
9933
  }
9564
9934
  function isCleanWorktree(worktreePath, repoRoot) {
9565
9935
  try {
@@ -9572,14 +9942,14 @@ function isCleanWorktree(worktreePath, repoRoot) {
9572
9942
  }
9573
9943
  }
9574
9944
  function scanDuplicateWorktreeCandidates(opts) {
9575
- if (!opts.includeOrphans || !existsSync35(opts.worktreesDir)) return [];
9945
+ if (!opts.includeOrphans || !existsSync36(opts.worktreesDir)) return [];
9576
9946
  const repos = /* @__PURE__ */ new Set();
9577
9947
  for (const entry of opts.index.values()) {
9578
- if (entry.run.repo) repos.add(path46.resolve(entry.run.repo));
9948
+ if (entry.run.repo) repos.add(path47.resolve(entry.run.repo));
9579
9949
  }
9580
9950
  const indexedPaths = /* @__PURE__ */ new Set();
9581
9951
  for (const entry of opts.index.values()) {
9582
- indexedPaths.add(path46.resolve(entry.worktreePath));
9952
+ indexedPaths.add(path47.resolve(entry.worktreePath));
9583
9953
  }
9584
9954
  const candidates = [];
9585
9955
  const seen = /* @__PURE__ */ new Set();
@@ -9592,15 +9962,15 @@ function scanDuplicateWorktreeCandidates(opts) {
9592
9962
  }
9593
9963
  const worktrees = parseWorktreePorcelain(porcelain);
9594
9964
  for (const wt of worktrees) {
9595
- const resolved = path46.resolve(wt.path);
9596
- if (resolved === path46.resolve(repoRoot)) continue;
9965
+ const resolved = path47.resolve(wt.path);
9966
+ if (resolved === path47.resolve(repoRoot)) continue;
9597
9967
  if (!isUnderWorktreesDir(resolved, opts.worktreesDir)) continue;
9598
9968
  if (indexedPaths.has(resolved)) continue;
9599
9969
  if (seen.has(resolved)) continue;
9600
- if (!existsSync35(resolved)) continue;
9970
+ if (!existsSync36(resolved)) continue;
9601
9971
  if (!isCleanWorktree(resolved, repoRoot)) continue;
9602
- const rel = path46.relative(opts.worktreesDir, resolved);
9603
- const parts = rel.split(path46.sep);
9972
+ const rel = path47.relative(opts.worktreesDir, resolved);
9973
+ const parts = rel.split(path47.sep);
9604
9974
  const runId = parts[0];
9605
9975
  const worker = parts[1] ?? "unknown";
9606
9976
  seen.add(resolved);
@@ -9619,12 +9989,12 @@ function scanDuplicateWorktreeCandidates(opts) {
9619
9989
  }
9620
9990
 
9621
9991
  // src/cleanup-worktree-index.ts
9622
- import path47 from "node:path";
9992
+ import path48 from "node:path";
9623
9993
  function buildWorktreeIndexAt(harnessRoot) {
9624
9994
  const index = /* @__PURE__ */ new Map();
9625
9995
  for (const run of listRunRecordsForHarnessRoot(harnessRoot)) {
9626
9996
  for (const name of Object.keys(run.workers || {})) {
9627
- const workerPath = path47.join(
9997
+ const workerPath = path48.join(
9628
9998
  runDirectoryAt(harnessRoot, run.id),
9629
9999
  "workers",
9630
10000
  safeSlug(name),
@@ -9632,9 +10002,9 @@ function buildWorktreeIndexAt(harnessRoot) {
9632
10002
  );
9633
10003
  const worker = readJson(workerPath, void 0);
9634
10004
  if (!worker?.worktreePath) continue;
9635
- index.set(path47.resolve(worker.worktreePath), {
10005
+ index.set(path48.resolve(worker.worktreePath), {
9636
10006
  harnessRoot,
9637
- worktreePath: path47.resolve(worker.worktreePath),
10007
+ worktreePath: path48.resolve(worker.worktreePath),
9638
10008
  runId: run.id,
9639
10009
  workerName: name,
9640
10010
  run,
@@ -9703,15 +10073,15 @@ function resolvePipelineHarnessRetention(runId) {
9703
10073
  }
9704
10074
 
9705
10075
  // src/cleanup-orphan-safety.ts
9706
- import { existsSync as existsSync36, statSync as statSync12 } from "node:fs";
9707
- import path48 from "node:path";
10076
+ import { existsSync as existsSync37, statSync as statSync12 } from "node:fs";
10077
+ import path49 from "node:path";
9708
10078
  var DEFAULT_HEARTBEAT_FRESH_MS = 30 * 60 * 1e3;
9709
10079
  function assessOrphanWorktreeSafety(input) {
9710
10080
  const now = input.now ?? Date.now();
9711
10081
  const heartbeatFreshMs = input.heartbeatFreshMs ?? DEFAULT_HEARTBEAT_FRESH_MS;
9712
- if (!existsSync36(input.worktreePath)) return null;
10082
+ if (!existsSync37(input.worktreePath)) return null;
9713
10083
  if (input.runId && input.workerName) {
9714
- const heartbeatPath = path48.join(
10084
+ const heartbeatPath = path49.join(
9715
10085
  input.harnessRoot,
9716
10086
  "runs",
9717
10087
  input.runId,
@@ -9725,8 +10095,8 @@ function assessOrphanWorktreeSafety(input) {
9725
10095
  } catch {
9726
10096
  }
9727
10097
  }
9728
- const gitDir = path48.join(input.worktreePath, ".git");
9729
- if (!existsSync36(gitDir)) return null;
10098
+ const gitDir = path49.join(input.worktreePath, ".git");
10099
+ if (!existsSync37(gitDir)) return null;
9730
10100
  const porcelain = gitCapture(input.worktreePath, ["status", "--porcelain"]);
9731
10101
  if (porcelain.status !== 0) return "pr_or_unmerged_commits";
9732
10102
  const dirtyLines = porcelain.stdout.split("\n").map((line) => line.trim()).filter((line) => line.length > 0);
@@ -9755,14 +10125,14 @@ function assessOrphanWorktreeSafety(input) {
9755
10125
  }
9756
10126
 
9757
10127
  // src/harness-storage-snapshot.ts
9758
- import { existsSync as existsSync37, readdirSync as readdirSync13, statSync as statSync13 } from "node:fs";
9759
- import path49 from "node:path";
10128
+ import { existsSync as existsSync38, readdirSync as readdirSync14, statSync as statSync13 } from "node:fs";
10129
+ import path50 from "node:path";
9760
10130
  function harnessStorageSnapshot(opts = {}) {
9761
10131
  const harnessRoot = normalizeHarnessRoot(opts.harnessRoot ?? resolveHarnessRoot());
9762
10132
  const worktreesDir = harnessWorktreesDir(harnessRoot);
9763
10133
  const now = opts.now ?? Date.now();
9764
10134
  const scannedAt = new Date(now).toISOString();
9765
- if (!existsSync37(worktreesDir)) {
10135
+ if (!existsSync38(worktreesDir)) {
9766
10136
  return {
9767
10137
  harnessRoot,
9768
10138
  worktreesDir,
@@ -9779,7 +10149,7 @@ function harnessStorageSnapshot(opts = {}) {
9779
10149
  let oldestMs = null;
9780
10150
  let entries;
9781
10151
  try {
9782
- entries = readdirSync13(worktreesDir, { withFileTypes: true });
10152
+ entries = readdirSync14(worktreesDir, { withFileTypes: true });
9783
10153
  } catch {
9784
10154
  return {
9785
10155
  harnessRoot,
@@ -9794,14 +10164,14 @@ function harnessStorageSnapshot(opts = {}) {
9794
10164
  for (const runEntry of entries) {
9795
10165
  if (!runEntry.isDirectory()) continue;
9796
10166
  runCount += 1;
9797
- const runPath = path49.join(worktreesDir, runEntry.name);
10167
+ const runPath = path50.join(worktreesDir, runEntry.name);
9798
10168
  try {
9799
10169
  const st = statSync13(runPath);
9800
10170
  oldestMs = oldestMs === null ? st.mtimeMs : Math.min(oldestMs, st.mtimeMs);
9801
10171
  } catch {
9802
10172
  }
9803
10173
  try {
9804
- for (const workerEntry of readdirSync13(runPath, { withFileTypes: true })) {
10174
+ for (const workerEntry of readdirSync14(runPath, { withFileTypes: true })) {
9805
10175
  if (workerEntry.isDirectory()) workerCount += 1;
9806
10176
  }
9807
10177
  } catch {
@@ -9829,12 +10199,12 @@ function harnessStorageSnapshot(opts = {}) {
9829
10199
  }
9830
10200
 
9831
10201
  // src/cleanup-harness-roots.ts
9832
- import { existsSync as existsSync38 } from "node:fs";
10202
+ import { existsSync as existsSync39 } from "node:fs";
9833
10203
  import { homedir as homedir12 } from "node:os";
9834
- import path50 from "node:path";
10204
+ import path51 from "node:path";
9835
10205
  var WELL_KNOWN_HARNESS_SCAN_ROOTS = [
9836
10206
  "/var/tmp/kynver-harness",
9837
- path50.join(homedir12(), ".openclaw", "harness")
10207
+ path51.join(homedir12(), ".openclaw", "harness")
9838
10208
  ];
9839
10209
  function addRoot(seen, roots, candidate) {
9840
10210
  if (!candidate?.trim()) return;
@@ -9856,8 +10226,8 @@ function resolveHarnessScanRoots(options = {}) {
9856
10226
  for (const candidate of extra ?? []) addRoot(seen, roots, candidate);
9857
10227
  if (shouldScanWellKnownRoots(options)) {
9858
10228
  for (const candidate of WELL_KNOWN_HARNESS_SCAN_ROOTS) {
9859
- const resolved = path50.resolve(candidate);
9860
- if (!seen.has(resolved) && existsSync38(resolved)) addRoot(seen, roots, resolved);
10229
+ const resolved = path51.resolve(candidate);
10230
+ if (!seen.has(resolved) && existsSync39(resolved)) addRoot(seen, roots, resolved);
9861
10231
  }
9862
10232
  }
9863
10233
  return roots;
@@ -9887,11 +10257,19 @@ function observeCleanupDiskPressure(input = {}) {
9887
10257
  }
9888
10258
  function applyDiskPressureToRetention(retention, pressure) {
9889
10259
  if (!pressure.pressured) return retention;
9890
- const executeOnPressure = retention.execute || envFlag3("KYNVER_CLEANUP_EXECUTE_ON_PRESSURE");
10260
+ const executeOnPressure = retention.execute || !envFlag3("KYNVER_CLEANUP_DRY_RUN_ON_PRESSURE");
9891
10261
  return {
9892
10262
  ...retention,
9893
10263
  execute: executeOnPressure,
9894
10264
  nodeModulesAgeMs: 0,
10265
+ // Disk pressure means the current-run-only cleanup scope has already fallen
10266
+ // behind. Expand the sweep to every known harness root/run, but keep the
10267
+ // worktree salvage/PR/dirty/live-worker guards in the cleanup pipeline.
10268
+ runIdFilter: void 0,
10269
+ includeOrphans: true,
10270
+ terminalWorktreesAgeMs: 0,
10271
+ runDirectoriesAgeMs: 0,
10272
+ worktreesAgeMs: retention.worktreesAgeMs > 0 ? retention.worktreesAgeMs : DEFAULT_WORKTREES_AGE_MS,
9895
10273
  diskPressure: true,
9896
10274
  diskGate: pressure.diskGate
9897
10275
  };
@@ -10003,9 +10381,9 @@ function mergeWorktreeIndexes(scanRoots) {
10003
10381
  }
10004
10382
  function worktreePathForCandidate(candidate, worktreesDir) {
10005
10383
  if (candidate.runId && candidate.worker) {
10006
- return path51.join(worktreesDir, candidate.runId, candidate.worker);
10384
+ return path52.join(worktreesDir, candidate.runId, candidate.worker);
10007
10385
  }
10008
- return path51.resolve(candidate.path, "..");
10386
+ return path52.resolve(candidate.path, "..");
10009
10387
  }
10010
10388
  function runHarnessCleanup(options = {}) {
10011
10389
  let retention = resolveHarnessRetention(options);
@@ -10030,7 +10408,7 @@ function runHarnessCleanup(options = {}) {
10030
10408
  for (const harnessRoot of paths.scanRoots) {
10031
10409
  if (atSweepCap()) break;
10032
10410
  emitCleanupProgress("root", harnessRoot);
10033
- const worktreesDir = path51.join(harnessRoot, "worktrees");
10411
+ const worktreesDir = path52.join(harnessRoot, "worktrees");
10034
10412
  const scanOpts = {
10035
10413
  harnessRoot,
10036
10414
  worktreesDir,
@@ -10043,7 +10421,7 @@ function runHarnessCleanup(options = {}) {
10043
10421
  };
10044
10422
  for (const raw of scanDependencyCacheCandidates(scanOpts)) {
10045
10423
  if (atSweepCap()) break;
10046
- const resolved = path51.resolve(raw.path);
10424
+ const resolved = path52.resolve(raw.path);
10047
10425
  if (processedPaths.has(resolved)) continue;
10048
10426
  processedPaths.add(resolved);
10049
10427
  const candidate = { ...raw, path: resolved };
@@ -10054,7 +10432,7 @@ function runHarnessCleanup(options = {}) {
10054
10432
  continue;
10055
10433
  }
10056
10434
  const worktreePath = worktreePathForCandidate(candidate, worktreesDir);
10057
- const indexed = index.get(path51.resolve(worktreePath)) ?? null;
10435
+ const indexed = index.get(path52.resolve(worktreePath)) ?? null;
10058
10436
  const guardReason = skipDependencyCacheRemoval({
10059
10437
  indexed,
10060
10438
  includeOrphans: true,
@@ -10078,7 +10456,7 @@ function runHarnessCleanup(options = {}) {
10078
10456
  }
10079
10457
  for (const raw of scanBuildCacheCandidates(scanOpts)) {
10080
10458
  if (atSweepCap()) break;
10081
- const resolved = path51.resolve(raw.path);
10459
+ const resolved = path52.resolve(raw.path);
10082
10460
  if (processedPaths.has(resolved)) continue;
10083
10461
  processedPaths.add(resolved);
10084
10462
  const candidate = { ...raw, path: resolved };
@@ -10089,7 +10467,7 @@ function runHarnessCleanup(options = {}) {
10089
10467
  continue;
10090
10468
  }
10091
10469
  const worktreePath = worktreePathForCandidate(candidate, worktreesDir);
10092
- const indexed = index.get(path51.resolve(worktreePath)) ?? null;
10470
+ const indexed = index.get(path52.resolve(worktreePath)) ?? null;
10093
10471
  const guardReason = skipBuildCacheRemoval({
10094
10472
  indexed,
10095
10473
  includeOrphans: true,
@@ -10119,11 +10497,11 @@ function runHarnessCleanup(options = {}) {
10119
10497
  const worktreeSeen = /* @__PURE__ */ new Set();
10120
10498
  for (const raw of worktreeCandidates) {
10121
10499
  if (atSweepCap()) break;
10122
- const resolved = path51.resolve(raw.path);
10500
+ const resolved = path52.resolve(raw.path);
10123
10501
  if (worktreeSeen.has(resolved)) continue;
10124
10502
  worktreeSeen.add(resolved);
10125
10503
  const candidate = { ...raw, path: resolved };
10126
- const indexed = index.get(path51.resolve(candidate.path)) ?? null;
10504
+ const indexed = index.get(path52.resolve(candidate.path)) ?? null;
10127
10505
  const orphanSafety = indexed ? null : assessOrphanWorktreeSafety({
10128
10506
  worktreePath: candidate.path,
10129
10507
  harnessRoot,
@@ -10133,7 +10511,7 @@ function runHarnessCleanup(options = {}) {
10133
10511
  });
10134
10512
  const guardSkip = skipWorktreeRemoval({
10135
10513
  indexed,
10136
- worktreePath: path51.resolve(candidate.path),
10514
+ worktreePath: path52.resolve(candidate.path),
10137
10515
  includeOrphans: retention.includeOrphans,
10138
10516
  worktreesAgeMs: retention.worktreesAgeMs,
10139
10517
  terminalWorktreesAgeMs: retention.terminalWorktreesAgeMs,
@@ -10165,11 +10543,11 @@ function runHarnessCleanup(options = {}) {
10165
10543
  now: paths.now
10166
10544
  })) {
10167
10545
  if (atSweepCap()) break;
10168
- const resolved = path51.resolve(raw.path);
10546
+ const resolved = path52.resolve(raw.path);
10169
10547
  if (processedPaths.has(resolved)) continue;
10170
10548
  processedPaths.add(resolved);
10171
10549
  const candidate = { ...raw, path: resolved };
10172
- const runId = candidate.runId ?? path51.basename(resolved);
10550
+ const runId = candidate.runId ?? path52.basename(resolved);
10173
10551
  const dirSkip = skipRunDirectoryRemoval({
10174
10552
  harnessRoot,
10175
10553
  runId,
@@ -10305,7 +10683,7 @@ function isPipelineCleanupEnabled() {
10305
10683
  // src/installed-package-versions.ts
10306
10684
  import { readFile } from "node:fs/promises";
10307
10685
  import { homedir as homedir13 } from "node:os";
10308
- import path52 from "node:path";
10686
+ import path53 from "node:path";
10309
10687
  var MANAGED_PACKAGES = [
10310
10688
  "@kynver-app/runtime",
10311
10689
  "@kynver-app/openclaw-agent-os",
@@ -10320,12 +10698,12 @@ function unique(values) {
10320
10698
  }
10321
10699
  function moduleRoots() {
10322
10700
  const home = homedir13();
10323
- const openClawPrefix = trim(process.env.KYNVER_OPENCLAW_NPM_ROOT) ?? trim(process.env.OPENCLAW_NPM_ROOT) ?? path52.join(home, ".openclaw", "npm");
10324
- const npmGlobalRoot = trim(process.env.KYNVER_NPM_GLOBAL_ROOT) ?? trim(process.env.KYNVER_NPM_GLOBAL_MODULES_ROOT) ?? (trim(process.env.NPM_CONFIG_PREFIX) ? path52.join(trim(process.env.NPM_CONFIG_PREFIX), "lib", "node_modules") : path52.join(home, ".npm-global", "lib", "node_modules"));
10701
+ const openClawPrefix = trim(process.env.KYNVER_OPENCLAW_NPM_ROOT) ?? trim(process.env.OPENCLAW_NPM_ROOT) ?? path53.join(home, ".openclaw", "npm");
10702
+ const npmGlobalRoot = trim(process.env.KYNVER_NPM_GLOBAL_ROOT) ?? trim(process.env.KYNVER_NPM_GLOBAL_MODULES_ROOT) ?? (trim(process.env.NPM_CONFIG_PREFIX) ? path53.join(trim(process.env.NPM_CONFIG_PREFIX), "lib", "node_modules") : path53.join(home, ".npm-global", "lib", "node_modules"));
10325
10703
  return unique([
10326
- path52.join(openClawPrefix, "lib", "node_modules"),
10327
- path52.join(openClawPrefix, "node_modules"),
10328
- npmGlobalRoot.endsWith("node_modules") ? npmGlobalRoot : path52.join(npmGlobalRoot, "lib", "node_modules")
10704
+ path53.join(openClawPrefix, "lib", "node_modules"),
10705
+ path53.join(openClawPrefix, "node_modules"),
10706
+ npmGlobalRoot.endsWith("node_modules") ? npmGlobalRoot : path53.join(npmGlobalRoot, "lib", "node_modules")
10329
10707
  ]);
10330
10708
  }
10331
10709
  async function readVersion(packageJsonPath) {
@@ -10341,7 +10719,7 @@ async function collectInstalledPackageVersions(observedAt = (/* @__PURE__ */ new
10341
10719
  const out = {};
10342
10720
  for (const packageName of MANAGED_PACKAGES) {
10343
10721
  for (const root of roots) {
10344
- const packageJsonPath = path52.join(root, packageName, "package.json");
10722
+ const packageJsonPath = path53.join(root, packageName, "package.json");
10345
10723
  const version = await readVersion(packageJsonPath);
10346
10724
  if (!version) continue;
10347
10725
  out[packageName] = { version, observedAt, path: packageJsonPath };
@@ -10357,7 +10735,7 @@ async function completeFinishedWorkers(runId, args) {
10357
10735
  const outcomes = [];
10358
10736
  for (const name of Object.keys(run.workers || {})) {
10359
10737
  const worker = readJson(
10360
- path53.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
10738
+ path54.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
10361
10739
  void 0
10362
10740
  );
10363
10741
  if (!worker?.taskId || worker.localOnly) continue;
@@ -10396,7 +10774,10 @@ async function postOperatorTick(agentOsId, runId, resourceGate, args, harnessCle
10396
10774
  ingestHarness: true,
10397
10775
  harnessBoardSnapshot: buildRunBoard(runId),
10398
10776
  resourceGate,
10399
- boxResourceSnapshot: buildBoxResourceSnapshotFromGate(resourceGate, { harnessRunId: runId }),
10777
+ boxResourceSnapshot: buildBoxResourceSnapshotFromGate(resourceGate, {
10778
+ harnessRunId: runId,
10779
+ boxKind: resolveBoxKindFromConfig(loadUserConfig())
10780
+ }),
10400
10781
  packageVersions,
10401
10782
  ...harnessCleanup ? { harnessCleanup } : {},
10402
10783
  runnerPresence: resolveRunnerPresencePayload({ runId }),
@@ -10514,7 +10895,30 @@ async function runDaemon(args) {
10514
10895
  process.on("SIGTERM", () => {
10515
10896
  stopping = true;
10516
10897
  });
10517
- console.error(JSON.stringify({ event: "daemon_start", runId, agentOsId, execute, intervalMs }));
10898
+ const identity = validateDaemonInstallIdentity(loadUserConfig());
10899
+ if (!identity.ok) {
10900
+ console.error(
10901
+ JSON.stringify({
10902
+ event: "daemon_start_blocked",
10903
+ runId,
10904
+ agentOsId,
10905
+ errors: identity.errors
10906
+ })
10907
+ );
10908
+ process.exit(1);
10909
+ }
10910
+ console.error(
10911
+ JSON.stringify({
10912
+ event: "daemon_start",
10913
+ runId,
10914
+ agentOsId,
10915
+ execute,
10916
+ intervalMs,
10917
+ boxKind: identity.box.boxKind,
10918
+ workerCapSource: identity.workerCapSource,
10919
+ maxConcurrentWorkers: identity.maxConcurrentWorkers
10920
+ })
10921
+ );
10518
10922
  const cronEnv = resolveKynverCronEnv();
10519
10923
  while (!stopping) {
10520
10924
  try {
@@ -10545,7 +10949,7 @@ async function runDaemon(args) {
10545
10949
  }
10546
10950
 
10547
10951
  // src/plan-progress.ts
10548
- import path54 from "node:path";
10952
+ import path56 from "node:path";
10549
10953
 
10550
10954
  // src/bounded-build/constants.ts
10551
10955
  var DEFAULT_BUILD_MEM_BUDGET_BYTES = 1536 * 1024 * 1024;
@@ -10583,7 +10987,7 @@ function formatNodeOptionsFlag(mb = resolveNodeOldSpaceSizeMb()) {
10583
10987
  }
10584
10988
 
10585
10989
  // src/bounded-build/systemd-wrap.ts
10586
- import { spawnSync as spawnSync4 } from "node:child_process";
10990
+ import { spawnSync as spawnSync5 } from "node:child_process";
10587
10991
  var systemdAvailableCache;
10588
10992
  function isSystemdRunAvailable() {
10589
10993
  if (process.env.KYNVER_BUILD_SKIP_SYSTEMD === "1" || process.env.KYNVER_BUILD_SKIP_SYSTEMD === "true") {
@@ -10594,7 +10998,7 @@ function isSystemdRunAvailable() {
10594
10998
  systemdAvailableCache = false;
10595
10999
  return false;
10596
11000
  }
10597
- const res = spawnSync4("systemd-run", ["--version"], { encoding: "utf8", stdio: ["ignore", "ignore", "pipe"] });
11001
+ const res = spawnSync5("systemd-run", ["--version"], { encoding: "utf8", stdio: ["ignore", "ignore", "pipe"] });
10598
11002
  systemdAvailableCache = res.status === 0;
10599
11003
  return systemdAvailableCache;
10600
11004
  }
@@ -10618,18 +11022,18 @@ function buildSystemdRunArgv(opts) {
10618
11022
  }
10619
11023
 
10620
11024
  // src/bounded-build/admission.ts
10621
- import { spawnSync as spawnSync5 } from "node:child_process";
10622
- function positiveInt3(value, fallback) {
11025
+ import { spawnSync as spawnSync6 } from "node:child_process";
11026
+ function positiveInt4(value, fallback) {
10623
11027
  const n = Number(value);
10624
11028
  if (!Number.isFinite(n) || n <= 0) return fallback;
10625
11029
  return Math.floor(n);
10626
11030
  }
10627
11031
  function resolveBuildAdmissionConfig(config = loadUserConfig()) {
10628
- const envBudget = process.env.KYNVER_BUILD_MEM_BUDGET_BYTES ? positiveInt3(process.env.KYNVER_BUILD_MEM_BUDGET_BYTES, DEFAULT_BUILD_MEM_BUDGET_BYTES) : void 0;
10629
- const envReserve = process.env.KYNVER_BUILD_MEM_RESERVE_BYTES ? positiveInt3(process.env.KYNVER_BUILD_MEM_RESERVE_BYTES, DEFAULT_BUILD_MEM_RESERVE_BYTES) : void 0;
11032
+ const envBudget = process.env.KYNVER_BUILD_MEM_BUDGET_BYTES ? positiveInt4(process.env.KYNVER_BUILD_MEM_BUDGET_BYTES, DEFAULT_BUILD_MEM_BUDGET_BYTES) : void 0;
11033
+ const envReserve = process.env.KYNVER_BUILD_MEM_RESERVE_BYTES ? positiveInt4(process.env.KYNVER_BUILD_MEM_RESERVE_BYTES, DEFAULT_BUILD_MEM_RESERVE_BYTES) : void 0;
10630
11034
  return {
10631
- perBuildBudgetBytes: envBudget ?? positiveInt3(config.perWorkerMemBytes, DEFAULT_BUILD_MEM_BUDGET_BYTES),
10632
- reserveBytes: envReserve ?? positiveInt3(config.memReserveBytes, DEFAULT_BUILD_MEM_RESERVE_BYTES)
11035
+ perBuildBudgetBytes: envBudget ?? positiveInt4(config.perWorkerMemBytes, DEFAULT_BUILD_MEM_BUDGET_BYTES),
11036
+ reserveBytes: envReserve ?? positiveInt4(config.memReserveBytes, DEFAULT_BUILD_MEM_RESERVE_BYTES)
10633
11037
  };
10634
11038
  }
10635
11039
  var activeBuilds = 0;
@@ -10654,7 +11058,7 @@ function assessBuildAdmission(opts = {}) {
10654
11058
  }
10655
11059
  function sleepMs2(ms) {
10656
11060
  if (ms <= 0) return;
10657
- spawnSync5(process.execPath, ["-e", `const d=Date.now()+${Math.floor(ms)};while(Date.now()<d);`], {
11061
+ spawnSync6(process.execPath, ["-e", `const d=Date.now()+${Math.floor(ms)};while(Date.now()<d);`], {
10658
11062
  stdio: "ignore"
10659
11063
  });
10660
11064
  }
@@ -10675,7 +11079,28 @@ function waitForBuildAdmission(timeoutMs, pollMs = 2e3, opts = {}) {
10675
11079
  }
10676
11080
 
10677
11081
  // src/bounded-build/exec.ts
10678
- import { spawnSync as spawnSync6 } from "node:child_process";
11082
+ import { spawnSync as spawnSync7 } from "node:child_process";
11083
+
11084
+ // src/harness-worktree-build-guard.ts
11085
+ import path55 from "node:path";
11086
+ function isPathUnderHarnessWorktree(cwd) {
11087
+ const worktreesDir = harnessWorktreesDir(resolveHarnessRoot());
11088
+ const rel = path55.relative(worktreesDir, path55.resolve(cwd));
11089
+ return rel.length > 0 && !rel.startsWith("..") && !path55.isAbsolute(rel);
11090
+ }
11091
+ function assessHarnessWorktreeBuildGuard(cwd) {
11092
+ if (!isPathUnderHarnessWorktree(cwd)) return { ok: true };
11093
+ const uid = typeof process.getuid === "function" ? process.getuid() : null;
11094
+ if (uid === 0) {
11095
+ return {
11096
+ ok: false,
11097
+ 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)."
11098
+ };
11099
+ }
11100
+ return { ok: true };
11101
+ }
11102
+
11103
+ // src/bounded-build/exec.ts
10679
11104
  function envArgv(env) {
10680
11105
  const out = [];
10681
11106
  for (const [key, value] of Object.entries(env)) {
@@ -10685,7 +11110,7 @@ function envArgv(env) {
10685
11110
  return out;
10686
11111
  }
10687
11112
  function runSpawn(argv, opts) {
10688
- const res = spawnSync6(argv[0], argv.slice(1), {
11113
+ const res = spawnSync7(argv[0], argv.slice(1), {
10689
11114
  cwd: opts.cwd,
10690
11115
  env: opts.env,
10691
11116
  encoding: "utf8",
@@ -10715,6 +11140,20 @@ function runBoundedBuildCheck(input) {
10715
11140
  command: input.command
10716
11141
  };
10717
11142
  }
11143
+ const worktreeGuard = assessHarnessWorktreeBuildGuard(input.cwd);
11144
+ if (!worktreeGuard.ok) {
11145
+ return {
11146
+ ok: false,
11147
+ exitCode: 1,
11148
+ stdout: "",
11149
+ stderr: worktreeGuard.reason,
11150
+ admitted: true,
11151
+ wrappedWithSystemd: false,
11152
+ nodeOptionsFlag: formatNodeOptionsFlag(),
11153
+ admission,
11154
+ command: input.command
11155
+ };
11156
+ }
10718
11157
  const env = mergeNodeOptionsForBuildCheck({ ...process.env, ...input.env });
10719
11158
  const nodeOptionsFlag = formatNodeOptionsFlag();
10720
11159
  const useSystemd = isSystemdRunAvailable();
@@ -10832,7 +11271,7 @@ async function emitPlanProgress(args) {
10832
11271
  }
10833
11272
  function verifyPlanLocal(args) {
10834
11273
  const worktree = required(args.worktree ? String(args.worktree) : void 0, "worktree");
10835
- const cwd = path54.resolve(worktree);
11274
+ const cwd = path56.resolve(worktree);
10836
11275
  const summary = runHarnessVerifyCommands(cwd);
10837
11276
  const emitJson = args.json === true || args.json === "true";
10838
11277
  const payload = { passed: summary.passed, worktree: cwd, steps: summary.steps };
@@ -10881,9 +11320,9 @@ async function verifyPlan(args) {
10881
11320
  }
10882
11321
 
10883
11322
  // src/harness-verify-cli.ts
10884
- import path55 from "node:path";
11323
+ import path57 from "node:path";
10885
11324
  function runHarnessVerifyCli(args) {
10886
- const cwd = path55.resolve(required(args.worktree ? String(args.worktree) : void 0, "worktree"));
11325
+ const cwd = path57.resolve(required(args.worktree ? String(args.worktree) : void 0, "worktree"));
10887
11326
  const emitJson = args.json === true || args.json === "true" || args.emitJson === true || args.emitJson === "true";
10888
11327
  const commands = [];
10889
11328
  const rawCmd = args.command;
@@ -11090,7 +11529,7 @@ function formatMonitorTickNotice(tick) {
11090
11529
  }
11091
11530
 
11092
11531
  // src/monitor/monitor.service.ts
11093
- import path57 from "node:path";
11532
+ import path59 from "node:path";
11094
11533
 
11095
11534
  // src/monitor/monitor.classify.ts
11096
11535
  function classifyWorkerHealth(input) {
@@ -11142,11 +11581,11 @@ function classifyWorkerHealth(input) {
11142
11581
  }
11143
11582
 
11144
11583
  // src/monitor/monitor.store.ts
11145
- import { existsSync as existsSync39, mkdirSync as mkdirSync7, readdirSync as readdirSync14, unlinkSync as unlinkSync4 } from "node:fs";
11146
- import path56 from "node:path";
11584
+ import { existsSync as existsSync40, mkdirSync as mkdirSync7, readdirSync as readdirSync15, unlinkSync as unlinkSync4 } from "node:fs";
11585
+ import path58 from "node:path";
11147
11586
  function monitorsDir() {
11148
11587
  const { harnessRoot } = getHarnessPaths();
11149
- const dir = path56.join(harnessRoot, "monitors");
11588
+ const dir = path58.join(harnessRoot, "monitors");
11150
11589
  mkdirSync7(dir, { recursive: true });
11151
11590
  return dir;
11152
11591
  }
@@ -11154,7 +11593,7 @@ function monitorIdFor(runId, workerName) {
11154
11593
  return workerName ? `${safeSlug(runId)}--${safeSlug(workerName)}` : safeSlug(runId);
11155
11594
  }
11156
11595
  function monitorPath(monitorId) {
11157
- return path56.join(monitorsDir(), `${monitorId}.json`);
11596
+ return path58.join(monitorsDir(), `${monitorId}.json`);
11158
11597
  }
11159
11598
  function loadMonitorSession(monitorId) {
11160
11599
  return readJson(monitorPath(monitorId), void 0);
@@ -11164,18 +11603,18 @@ function saveMonitorSession(session) {
11164
11603
  }
11165
11604
  function deleteMonitorSession(monitorId) {
11166
11605
  const file = monitorPath(monitorId);
11167
- if (!existsSync39(file)) return false;
11606
+ if (!existsSync40(file)) return false;
11168
11607
  unlinkSync4(file);
11169
11608
  return true;
11170
11609
  }
11171
11610
  function listMonitorSessions() {
11172
11611
  const dir = monitorsDir();
11173
- if (!existsSync39(dir)) return [];
11612
+ if (!existsSync40(dir)) return [];
11174
11613
  const entries = [];
11175
- for (const name of readdirSync14(dir)) {
11614
+ for (const name of readdirSync15(dir)) {
11176
11615
  if (!name.endsWith(".json")) continue;
11177
11616
  const session = readJson(
11178
- path56.join(dir, name),
11617
+ path58.join(dir, name),
11179
11618
  void 0
11180
11619
  );
11181
11620
  if (!session?.monitorId) continue;
@@ -11266,7 +11705,7 @@ async function fetchTaskLeasesForWorkers(input) {
11266
11705
  // src/monitor/monitor.service.ts
11267
11706
  function workerRecord2(runId, name) {
11268
11707
  return readJson(
11269
- path57.join(runDirectory(runId), "workers", safeSlug(name), "worker.json"),
11708
+ path59.join(runDirectory(runId), "workers", safeSlug(name), "worker.json"),
11270
11709
  void 0
11271
11710
  );
11272
11711
  }
@@ -11472,18 +11911,18 @@ async function runMonitorLoop(args) {
11472
11911
 
11473
11912
  // src/monitor/monitor-spawn.ts
11474
11913
  import { spawn as spawn6 } from "node:child_process";
11475
- import { closeSync as closeSync7, existsSync as existsSync40, openSync as openSync7 } from "node:fs";
11476
- import path58 from "node:path";
11914
+ import { closeSync as closeSync7, existsSync as existsSync41, openSync as openSync7 } from "node:fs";
11915
+ import path60 from "node:path";
11477
11916
  import { fileURLToPath as fileURLToPath3 } from "node:url";
11478
11917
  function resolveDefaultCliPath2() {
11479
- return path58.join(fileURLToPath3(new URL(".", import.meta.url)), "cli.js");
11918
+ return path60.join(fileURLToPath3(new URL(".", import.meta.url)), "cli.js");
11480
11919
  }
11481
11920
  function spawnMonitorSidecar(opts) {
11482
11921
  const cliPath = opts.cliPath ?? resolveDefaultCliPath2();
11483
- if (!existsSync40(cliPath)) return void 0;
11922
+ if (!existsSync41(cliPath)) return void 0;
11484
11923
  const monitorId = monitorIdFor(opts.runId, opts.workerName);
11485
11924
  const { harnessRoot } = getHarnessPaths();
11486
- const logPath = path58.join(harnessRoot, "monitors", `${monitorId}.log`);
11925
+ const logPath = path60.join(harnessRoot, "monitors", `${monitorId}.log`);
11487
11926
  let logFd;
11488
11927
  try {
11489
11928
  logFd = openSync7(logPath, "a");
@@ -11603,13 +12042,13 @@ async function monitorTickCli(args) {
11603
12042
  }
11604
12043
 
11605
12044
  // src/package-version.ts
11606
- import { existsSync as existsSync41, readFileSync as readFileSync13 } from "node:fs";
12045
+ import { existsSync as existsSync42, readFileSync as readFileSync13 } from "node:fs";
11607
12046
  import { dirname, join } from "node:path";
11608
12047
  import { fileURLToPath as fileURLToPath4 } from "node:url";
11609
12048
  function resolvePackageRoot(moduleUrl) {
11610
12049
  let dir = dirname(fileURLToPath4(moduleUrl));
11611
12050
  for (let depth = 0; depth < 6; depth += 1) {
11612
- if (existsSync41(join(dir, "package.json"))) return dir;
12051
+ if (existsSync42(join(dir, "package.json"))) return dir;
11613
12052
  const parent = dirname(dir);
11614
12053
  if (parent === dir) break;
11615
12054
  dir = parent;
@@ -11639,8 +12078,8 @@ function handleCliVersionFlag(argv, moduleUrl = import.meta.url, binName) {
11639
12078
  }
11640
12079
 
11641
12080
  // src/memory-cost-package-version-guard.ts
11642
- import { existsSync as existsSync42, readFileSync as readFileSync14 } from "node:fs";
11643
- import path59 from "node:path";
12081
+ import { existsSync as existsSync43, readFileSync as readFileSync14 } from "node:fs";
12082
+ import path61 from "node:path";
11644
12083
  var MEMORY_COST_PACKAGE_MIN_VERSIONS = {
11645
12084
  "@kynver-app/runtime": "0.1.83",
11646
12085
  "@kynver-app/openclaw-agent-os": "0.1.43",
@@ -11702,8 +12141,8 @@ function resolveRepoRoot(cwd, explicitRepoRoot) {
11702
12141
  (value) => Boolean(value?.trim())
11703
12142
  );
11704
12143
  for (const candidate of candidates) {
11705
- const resolved = path59.resolve(candidate);
11706
- if (existsSync42(path59.join(resolved, "packages/kynver-runtime/package.json")) && existsSync42(path59.join(resolved, "package.json"))) {
12144
+ const resolved = path61.resolve(candidate);
12145
+ if (existsSync43(path61.join(resolved, "packages/kynver-runtime/package.json")) && existsSync43(path61.join(resolved, "package.json"))) {
11707
12146
  return resolved;
11708
12147
  }
11709
12148
  }
@@ -11715,7 +12154,7 @@ function probeRepoPackageVersions(input = {}) {
11715
12154
  if (!repoRoot) return {};
11716
12155
  const out = {};
11717
12156
  for (const packageName of MEMORY_COST_MANAGED_PACKAGES) {
11718
- const packageJsonPath = path59.join(repoRoot, REPO_PACKAGE_JSON_RELATIVE[packageName]);
12157
+ const packageJsonPath = path61.join(repoRoot, REPO_PACKAGE_JSON_RELATIVE[packageName]);
11719
12158
  const version = readPackageJsonVersion(packageJsonPath);
11720
12159
  if (!version) continue;
11721
12160
  out[packageName] = { version, source: "repo", path: packageJsonPath };
@@ -11867,7 +12306,7 @@ function shouldEnforceMemoryCostPackageGuardCli(scope, action) {
11867
12306
  }
11868
12307
 
11869
12308
  // src/post-restart-unblock.ts
11870
- import path60 from "node:path";
12309
+ import path62 from "node:path";
11871
12310
  function skip(runId, worker, taskId, agentOsId, leaseOwner, reason) {
11872
12311
  return { runId, worker, taskId, agentOsId, leaseOwner, action: "skipped", reason };
11873
12312
  }
@@ -11880,7 +12319,7 @@ async function postRestartUnblock(args) {
11880
12319
  const errors = [];
11881
12320
  for (const run of listRunRecords()) {
11882
12321
  for (const name of Object.keys(run.workers ?? {})) {
11883
- const workerPath = path60.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json");
12322
+ const workerPath = path62.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json");
11884
12323
  const worker = readJson(workerPath, void 0);
11885
12324
  if (!worker) {
11886
12325
  skipped.push(skip(run.id, name, "", "", "", "worker.json missing"));
@@ -11992,9 +12431,9 @@ async function postRestartUnblockCli(args) {
11992
12431
  }
11993
12432
 
11994
12433
  // src/default-repo-cli.ts
11995
- import path61 from "node:path";
12434
+ import path63 from "node:path";
11996
12435
  import { homedir as homedir14 } from "node:os";
11997
- var CONFIG_FILE2 = path61.join(homedir14(), ".kynver", "config.json");
12436
+ var CONFIG_FILE2 = path63.join(homedir14(), ".kynver", "config.json");
11998
12437
  function ensureDefaultRepo(opts) {
11999
12438
  const existing = loadUserConfig();
12000
12439
  const resolved = resolveDefaultRepo({ ...opts, config: existing });
@@ -12075,16 +12514,16 @@ function summarizeResolvedDefaultRepo(resolved) {
12075
12514
  }
12076
12515
 
12077
12516
  // src/doctor/runtime-takeover.ts
12078
- import path63 from "node:path";
12517
+ import path65 from "node:path";
12079
12518
 
12080
12519
  // src/doctor/runtime-takeover.probes.ts
12081
- import { accessSync, constants, existsSync as existsSync43, readFileSync as readFileSync15 } from "node:fs";
12520
+ import { accessSync, constants, existsSync as existsSync44, readFileSync as readFileSync15 } from "node:fs";
12082
12521
  import { homedir as homedir15 } from "node:os";
12083
- import path62 from "node:path";
12084
- import { spawnSync as spawnSync7 } from "node:child_process";
12522
+ import path64 from "node:path";
12523
+ import { spawnSync as spawnSync8 } from "node:child_process";
12085
12524
  function captureCommand(bin, args) {
12086
12525
  try {
12087
- const res = spawnSync7(bin, args, { encoding: "utf8" });
12526
+ const res = spawnSync8(bin, args, { encoding: "utf8" });
12088
12527
  const stdout = (res.stdout || "").trim();
12089
12528
  const stderr = (res.stderr || "").trim();
12090
12529
  const ok = res.status === 0;
@@ -12109,7 +12548,7 @@ function tokenPrefix(token) {
12109
12548
  return trimmed.length <= 12 ? `${trimmed}\u2026` : `${trimmed.slice(0, 12)}\u2026`;
12110
12549
  }
12111
12550
  function isWritable(target) {
12112
- if (!existsSync43(target)) return false;
12551
+ if (!existsSync44(target)) return false;
12113
12552
  try {
12114
12553
  accessSync(target, constants.W_OK);
12115
12554
  return true;
@@ -12122,11 +12561,11 @@ var defaultRuntimeTakeoverProbes = {
12122
12561
  commandOnPath: (bin) => captureCommand(process.platform === "win32" ? "where" : "which", [bin]),
12123
12562
  kynverVersion: (bin) => captureCommand(bin, ["--version"]),
12124
12563
  loadConfig: () => loadUserConfig(),
12125
- configFilePath: () => path62.join(homedir15(), ".kynver", "config.json"),
12126
- credentialsFilePath: () => path62.join(homedir15(), ".kynver", "credentials"),
12564
+ configFilePath: () => path64.join(homedir15(), ".kynver", "config.json"),
12565
+ credentialsFilePath: () => path64.join(homedir15(), ".kynver", "credentials"),
12127
12566
  readCredentials: () => {
12128
- const credPath = path62.join(homedir15(), ".kynver", "credentials");
12129
- if (!existsSync43(credPath)) {
12567
+ const credPath = path64.join(homedir15(), ".kynver", "credentials");
12568
+ if (!existsSync44(credPath)) {
12130
12569
  return { hasApiKey: false };
12131
12570
  }
12132
12571
  try {
@@ -12160,8 +12599,8 @@ var defaultRuntimeTakeoverProbes = {
12160
12599
  })()
12161
12600
  }),
12162
12601
  harnessRoot: () => resolveHarnessRoot(),
12163
- legacyOpenclawHarnessRoot: () => path62.join(homedir15(), ".openclaw", "harness"),
12164
- pathExists: (target) => existsSync43(target),
12602
+ legacyOpenclawHarnessRoot: () => path64.join(homedir15(), ".openclaw", "harness"),
12603
+ pathExists: (target) => existsSync44(target),
12165
12604
  pathWritable: (target) => isWritable(target),
12166
12605
  vercelVersion: () => captureCommand("vercel", ["--version"]),
12167
12606
  vercelWhoami: () => captureCommand("vercel", ["whoami"])
@@ -12560,8 +12999,8 @@ function assessVercelCli(probes) {
12560
12999
  }
12561
13000
  function assessHarnessDirs(probes) {
12562
13001
  const harnessRoot = probes.harnessRoot();
12563
- const runsDir = path63.join(harnessRoot, "runs");
12564
- const worktreesDir = path63.join(harnessRoot, "worktrees");
13002
+ const runsDir = path65.join(harnessRoot, "runs");
13003
+ const worktreesDir = path65.join(harnessRoot, "worktrees");
12565
13004
  const displayHarnessRoot = redactHomePath(harnessRoot);
12566
13005
  const displayRunsDir = redactHomePath(runsDir);
12567
13006
  const displayWorktreesDir = redactHomePath(worktreesDir);
@@ -12825,9 +13264,9 @@ function applySchedulerCutoverAttestation(config) {
12825
13264
  }
12826
13265
 
12827
13266
  // src/scheduler-cutover-cli.ts
12828
- import path64 from "node:path";
13267
+ import path66 from "node:path";
12829
13268
  import { homedir as homedir16 } from "node:os";
12830
- var CONFIG_FILE3 = path64.join(homedir16(), ".kynver", "config.json");
13269
+ var CONFIG_FILE3 = path66.join(homedir16(), ".kynver", "config.json");
12831
13270
  function runSchedulerCutoverCheckCli(json = false) {
12832
13271
  const config = loadUserConfig();
12833
13272
  const report = assessSchedulerCutover(config);
@@ -12980,7 +13419,7 @@ function usage(code = 0) {
12980
13419
  "Usage:",
12981
13420
  " kynver login --api-key KEY",
12982
13421
  " kynver runner credential [--agent-os-id ID] [--base-url URL]",
12983
- " kynver setup [--api-base-url URL] [--agent-os-id ID] [--agent-os-slug SLUG] [--repo PATH] [--discover-repo] [--max-workers N] [--provider claude|cursor]",
13422
+ " 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]",
12984
13423
  " kynver daemon --run RUN_ID --agent-os-id AOS_ID [--execute] [--interval-ms MS]",
12985
13424
  " kynver run create [--repo /path/repo] [--name name] [--base origin/main]",
12986
13425
  " kynver run list",