@kynver-app/runtime 0.1.117 → 0.1.119

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -532,23 +532,23 @@ function isWslHost() {
532
532
  function observeWslHostDisk(options = {}) {
533
533
  const wsl = options.forceWsl === void 0 ? isWslHost() : options.forceWsl;
534
534
  if (!wsl) return null;
535
- const path75 = options.wslHostMount?.trim() || process.env.KYNVER_WSL_HOST_MOUNT?.trim() || DEFAULT_WSL_HOST_MOUNT;
535
+ const path79 = options.wslHostMount?.trim() || process.env.KYNVER_WSL_HOST_MOUNT?.trim() || DEFAULT_WSL_HOST_MOUNT;
536
536
  const warnBelowBytes = options.wslHostFreeWarnBytes ?? DEFAULT_WSL_HOST_WARN_FREE_BYTES;
537
537
  const criticalBelowBytes = options.wslHostFreeCriticalBytes ?? DEFAULT_WSL_HOST_CRITICAL_FREE_BYTES;
538
538
  const statfs = options.statfs ?? statfsSync;
539
539
  let stats;
540
540
  try {
541
- stats = statfs(path75);
541
+ stats = statfs(path79);
542
542
  } catch (error) {
543
543
  return {
544
544
  ok: false,
545
- path: path75,
545
+ path: path79,
546
546
  freeBytes: 0,
547
547
  totalBytes: 0,
548
548
  usedPercent: 100,
549
549
  warnBelowBytes,
550
550
  criticalBelowBytes,
551
- reason: `Windows host disk probe failed at ${path75}: ${error.message}`,
551
+ reason: `Windows host disk probe failed at ${path79}: ${error.message}`,
552
552
  probeError: error.message
553
553
  };
554
554
  }
@@ -562,11 +562,11 @@ function observeWslHostDisk(options = {}) {
562
562
  let reason = null;
563
563
  if (!ok) {
564
564
  const tag = criticalFree ? "critical" : "warning";
565
- reason = `Windows host disk ${path75} at ${tag}: ${freeGiB} GiB free (<${(criticalFree ? criticalBelowBytes : warnBelowBytes) / 1024 / 1024 / 1024} GiB); WSL VHDX cannot grow safely. ${summarizeWslRecoverySteps()}`;
565
+ reason = `Windows host disk ${path79} at ${tag}: ${freeGiB} GiB free (<${(criticalFree ? criticalBelowBytes : warnBelowBytes) / 1024 / 1024 / 1024} GiB); WSL VHDX cannot grow safely. ${summarizeWslRecoverySteps()}`;
566
566
  }
567
567
  return {
568
568
  ok,
569
- path: path75,
569
+ path: path79,
570
570
  freeBytes,
571
571
  totalBytes,
572
572
  usedPercent,
@@ -592,12 +592,12 @@ var init_wsl_host = __esm({
592
592
  // src/disk-gate.ts
593
593
  import { statfsSync as statfsSync2 } from "node:fs";
594
594
  function observeRunnerDiskGate(input = {}) {
595
- const path75 = input.diskPath?.trim() || "/";
595
+ const path79 = input.diskPath?.trim() || "/";
596
596
  const warnBelowBytes = input.diskFreeWarnBytes ?? DEFAULT_WARN_FREE_BYTES;
597
597
  const criticalBelowBytes = input.diskFreeCriticalBytes ?? DEFAULT_CRITICAL_FREE_BYTES;
598
598
  const maxUsedPercent = input.diskMaxUsedPercent ?? DEFAULT_MAX_USED_PERCENT;
599
599
  const hardMaxUsedPercent = input.diskHardMaxUsedPercent ?? DEFAULT_HARD_MAX_USED_PERCENT;
600
- const stats = statfsSync2(path75);
600
+ const stats = statfsSync2(path79);
601
601
  const freeBytes = Number(stats.bavail) * Number(stats.bsize);
602
602
  const totalBytes = Number(stats.blocks) * Number(stats.bsize);
603
603
  const usedPercent = totalBytes > 0 ? (totalBytes - freeBytes) / totalBytes * 100 : 100;
@@ -620,7 +620,7 @@ function observeRunnerDiskGate(input = {}) {
620
620
  }
621
621
  return {
622
622
  ok,
623
- path: path75,
623
+ path: path79,
624
624
  freeBytes,
625
625
  totalBytes,
626
626
  usedPercent,
@@ -3165,6 +3165,7 @@ async function enforceMemoryCostPackageGuardAtStartup(input = {}) {
3165
3165
  function shouldEnforceMemoryCostPackageGuardCli(scope, action) {
3166
3166
  if (!scope) return false;
3167
3167
  if (scope === "daemon") return true;
3168
+ if (scope === "start") return true;
3168
3169
  if (scope === "worker") return true;
3169
3170
  if (scope === "monitor") return true;
3170
3171
  if (scope === "run" && (action === "dispatch" || action === "sweep" || action === "reconcile" || action === "unblock")) {
@@ -5857,8 +5858,8 @@ function dirtyPathsCoveredByDisposableRemoval(changedFiles, removed) {
5857
5858
  if (removed.length === 0) return false;
5858
5859
  const removedSet = new Set(removed.map((p) => normalizeRelativePath(p)));
5859
5860
  return material.every((line) => {
5860
- const path75 = normalizeRelativePath(pathFromGitStatusLine(line));
5861
- return removedSet.has(path75);
5861
+ const path79 = normalizeRelativePath(pathFromGitStatusLine(line));
5862
+ return removedSet.has(path79);
5862
5863
  });
5863
5864
  }
5864
5865
 
@@ -8424,7 +8425,10 @@ async function dispatchRun(args) {
8424
8425
  } : {}
8425
8426
  });
8426
8427
  const requestDispatch = async (maxStarts) => {
8427
- const dispatch = await postJsonWithCredentialRefresh(dispatchUrl, secret, buildBody(maxStarts), { agentOsId, baseUrl: base });
8428
+ const dispatch = await postJsonWithCredentialRefresh(dispatchUrl, secret, buildBody(maxStarts), {
8429
+ agentOsId,
8430
+ baseUrl: base
8431
+ }) ?? { ok: false, status: 0, response: null };
8428
8432
  const responseBody = dispatch.response;
8429
8433
  return { dispatch, result: responseBody?.result };
8430
8434
  };
@@ -8525,6 +8529,13 @@ async function dispatchRun(args) {
8525
8529
  task,
8526
8530
  report
8527
8531
  });
8532
+ if (!post.ok) {
8533
+ return abortClaimedSpawn(
8534
+ task,
8535
+ `land_pr completion POST failed (HTTP ${post.status})`,
8536
+ { landPr: true, outcome: report.outcome }
8537
+ );
8538
+ }
8528
8539
  outcomes.push({
8529
8540
  taskId,
8530
8541
  started: true,
@@ -8532,12 +8543,6 @@ async function dispatchRun(args) {
8532
8543
  outcome: report.outcome,
8533
8544
  completionStatus: post.status
8534
8545
  });
8535
- if (!post.ok) {
8536
- return abortClaimedSpawn(
8537
- task,
8538
- `land_pr completion POST failed (HTTP ${post.status})`
8539
- );
8540
- }
8541
8546
  return true;
8542
8547
  } catch (error) {
8543
8548
  return abortClaimedSpawn(task, error.message);
@@ -8676,10 +8681,22 @@ async function dispatchRun(args) {
8676
8681
  return abortClaimedSpawn(task, error.message);
8677
8682
  }
8678
8683
  }
8679
- let shouldContinueDispatch = true;
8680
- for (const decision of result.started) {
8684
+ const failedStartTaskIds = /* @__PURE__ */ new Set();
8685
+ async function admitClaimedDecision(decision) {
8686
+ const task = decision.task;
8687
+ const taskId = String(task.id);
8688
+ if (failedStartTaskIds.has(taskId)) {
8689
+ return abortClaimedSpawn(
8690
+ task,
8691
+ "dispatch_retry_loop_prevented: task already failed to start this tick"
8692
+ );
8693
+ }
8681
8694
  const admitted = isLandPrDecision2(decision) ? await runLandPrClaimed(decision) : await spawnClaimed(decision);
8682
- shouldContinueDispatch = admitted && shouldContinueDispatch;
8695
+ if (!admitted) failedStartTaskIds.add(taskId);
8696
+ return admitted;
8697
+ }
8698
+ for (const decision of result.started) {
8699
+ await admitClaimedDecision(decision);
8683
8700
  }
8684
8701
  skipped.push(
8685
8702
  ...result.skipped ?? []
@@ -8698,9 +8715,7 @@ async function dispatchRun(args) {
8698
8715
  });
8699
8716
  }
8700
8717
  }
8701
- if (exactTargetMode) shouldContinueDispatch = false;
8702
- while (shouldContinueDispatch && outcomes.length < cappedStarts) {
8703
- if (exactTargetMode) break;
8718
+ while (!exactTargetMode && outcomes.length < cappedStarts) {
8704
8719
  const next = await requestDispatch(1);
8705
8720
  if (!next.dispatch.ok || !next.result) {
8706
8721
  outcomes.push({
@@ -8717,9 +8732,7 @@ async function dispatchRun(args) {
8717
8732
  if (started.length === 0) break;
8718
8733
  for (const decision of started) {
8719
8734
  if (outcomes.length >= cappedStarts) break;
8720
- const admitted = isLandPrDecision2(decision) ? await runLandPrClaimed(decision) : await spawnClaimed(decision);
8721
- shouldContinueDispatch = admitted && shouldContinueDispatch;
8722
- if (!shouldContinueDispatch) break;
8735
+ await admitClaimedDecision(decision);
8723
8736
  }
8724
8737
  }
8725
8738
  const startedCount = outcomes.filter((o) => o.started).length;
@@ -8947,8 +8960,8 @@ function resolveOpenAiCodexRetryBudget(input) {
8947
8960
  const env = input.env ?? process.env;
8948
8961
  const base = Math.max(1, input.defaultRetries ?? 3);
8949
8962
  const provider = String(input.provider ?? "").toLowerCase();
8950
- const platform = String(input.platform ?? "").toLowerCase();
8951
- if (provider !== OPENAI_CODEX_PROVIDER || platform !== "cron") {
8963
+ const platform2 = String(input.platform ?? "").toLowerCase();
8964
+ if (provider !== OPENAI_CODEX_PROVIDER || platform2 !== "cron") {
8952
8965
  return base;
8953
8966
  }
8954
8967
  const raw = env.HERMES_CODEX_CRON_API_MAX_RETRIES?.trim();
@@ -10489,7 +10502,9 @@ function createRun(args) {
10489
10502
  workers: {}
10490
10503
  };
10491
10504
  writeJson(path38.join(dir, "run.json"), run);
10492
- console.log(JSON.stringify({ runId: id, runDir: dir, repo, base, baseCommit }, null, 2));
10505
+ const info = { runId: id, runDir: dir, repo, base, baseCommit };
10506
+ console.log(JSON.stringify(info, null, 2));
10507
+ return info;
10493
10508
  }
10494
10509
  function listRuns() {
10495
10510
  listRunsCli();
@@ -11027,11 +11042,11 @@ var LIVE_SKIP_REASONS = /* @__PURE__ */ new Set([
11027
11042
  function collectPreservedLivePaths(actions, skips) {
11028
11043
  const out = [];
11029
11044
  const seen = /* @__PURE__ */ new Set();
11030
- const push = (path75, reason, detail) => {
11031
- const key = `${path75}\0${reason}`;
11045
+ const push = (path79, reason, detail) => {
11046
+ const key = `${path79}\0${reason}`;
11032
11047
  if (seen.has(key) || out.length >= MAX_PRESERVED_LIVE_PATH_SAMPLES) return;
11033
11048
  seen.add(key);
11034
- out.push({ path: path75, reason, ...detail ? { detail } : {} });
11049
+ out.push({ path: path79, reason, ...detail ? { detail } : {} });
11035
11050
  };
11036
11051
  for (const skip2 of skips) {
11037
11052
  if (!LIVE_SKIP_REASONS.has(skip2.reason)) continue;
@@ -11967,7 +11982,10 @@ function observeCleanupDiskPressure(input = {}) {
11967
11982
  diskPath,
11968
11983
  diskMaxUsedPercent: input.diskMaxUsedPercent ?? maxUsedPercent
11969
11984
  });
11970
- const pressured = !diskGate.ok || diskGate.usedPercent >= maxUsedPercent;
11985
+ let pressured = !diskGate.ok || diskGate.usedPercent >= maxUsedPercent;
11986
+ const force = process.env.KYNVER_CLEANUP_DISK_PRESSURE_FORCE?.trim().toLowerCase();
11987
+ if (force === "none" || force === "off" || force === "0") pressured = false;
11988
+ else if (force === "pressured" || force === "on" || force === "1") pressured = true;
11971
11989
  return { diskGate, pressured, maxUsedPercent };
11972
11990
  }
11973
11991
  function applyDiskPressureToRetention(retention, pressure) {
@@ -12448,7 +12466,7 @@ function isPipelineCleanupEnabled() {
12448
12466
 
12449
12467
  // src/cli.ts
12450
12468
  init_config();
12451
- import { mkdirSync as mkdirSync11, realpathSync } from "node:fs";
12469
+ import { mkdirSync as mkdirSync13, realpathSync } from "node:fs";
12452
12470
  import { fileURLToPath as fileURLToPath5 } from "node:url";
12453
12471
 
12454
12472
  // src/bootstrap.ts
@@ -12510,68 +12528,13 @@ async function runBootstrap(args) {
12510
12528
  await runSetup(setupArgs);
12511
12529
  console.log("");
12512
12530
  console.log(` Bootstrap complete \u2014 ${os9.hostname()} is linked to workspace "${primary.slug}".`);
12513
- console.log(" Next: run autonomous work with `kynver daemon --run <RUN_ID> --agent-os-id <AOS_ID> --execute`");
12514
- console.log(" (create a run first with `kynver run create --repo /path/to/repo`).");
12531
+ console.log(" Next: bring your agent online with `kynver start`.");
12532
+ console.log(" (Advanced: `kynver run create --repo /path/to/repo` + `kynver daemon --run <RUN_ID> --agent-os-id <AOS_ID> --execute`.)");
12515
12533
  }
12516
12534
 
12517
- // src/cli.ts
12518
- init_run_store();
12519
-
12520
- // src/discard-disposable.ts
12521
- init_run_store();
12522
- init_status();
12523
- import { existsSync as existsSync41, rmSync as rmSync4 } from "node:fs";
12524
- import path54 from "node:path";
12525
- function normalizeRelativePath2(value) {
12526
- const normalized = value.replace(/\\/g, "/").replace(/^\.\//, "").trim();
12527
- if (!normalized || normalized.startsWith("/") || normalized.includes("..")) {
12528
- throw new Error(`unsafe path: ${value}`);
12529
- }
12530
- return normalized;
12531
- }
12532
- function parsePathsArg(raw) {
12533
- if (typeof raw !== "string" || !raw.trim()) return [];
12534
- return raw.split(",").map((p) => p.trim()).filter(Boolean);
12535
- }
12536
- function discardDisposableArtifacts(args) {
12537
- const { runId, workerName } = resolveWorkerTargetArgs(args);
12538
- const worker = loadWorker(runId, workerName);
12539
- const paths = [
12540
- ...parsePathsArg(args.path),
12541
- ...Array.isArray(args.paths) ? args.paths : []
12542
- ];
12543
- if (paths.length === 0) {
12544
- return { ok: false, removed: [], reason: "requires at least one --path" };
12545
- }
12546
- const worktreeRoot = path54.resolve(worker.worktreePath);
12547
- const removed = [];
12548
- for (const raw of paths) {
12549
- const rel = normalizeRelativePath2(raw);
12550
- const abs = path54.resolve(worktreeRoot, rel);
12551
- if (!abs.startsWith(worktreeRoot + path54.sep) && abs !== worktreeRoot) {
12552
- return { ok: false, removed, reason: `path escapes worktree: ${raw}` };
12553
- }
12554
- if (!existsSync41(abs)) {
12555
- return { ok: false, removed, reason: `path not found: ${raw}` };
12556
- }
12557
- rmSync4(abs, { recursive: true, force: true });
12558
- removed.push(rel);
12559
- }
12560
- const prior = Array.isArray(worker.disposableArtifactsRemoved) ? worker.disposableArtifactsRemoved.filter((p) => typeof p === "string") : [];
12561
- worker.disposableArtifactsRemoved = [.../* @__PURE__ */ new Set([...prior, ...removed])];
12562
- saveWorker(worker.runId, worker);
12563
- const status = computeWorkerStatus(worker);
12564
- return {
12565
- ok: true,
12566
- removed,
12567
- ...status.changedFiles.length ? { reason: "worktree still has other changes" } : {}
12568
- };
12569
- }
12570
- function discardDisposableCli(args) {
12571
- const result = discardDisposableArtifacts(args);
12572
- console.log(JSON.stringify(result, null, 2));
12573
- if (!result.ok) process.exit(1);
12574
- }
12535
+ // src/start.ts
12536
+ init_config();
12537
+ import os12 from "node:os";
12575
12538
 
12576
12539
  // src/daemon.ts
12577
12540
  init_config();
@@ -12621,15 +12584,15 @@ function validateDaemonInstallIdentity(config = loadUserConfig(), env = process.
12621
12584
  // src/daemon-heartbeat.ts
12622
12585
  import { mkdirSync as mkdirSync7, readFileSync as readFileSync16, renameSync as renameSync3, writeFileSync as writeFileSync4 } from "node:fs";
12623
12586
  import { homedir as homedir14 } from "node:os";
12624
- import path55 from "node:path";
12587
+ import path54 from "node:path";
12625
12588
  function daemonHeartbeatPath(agentOsId) {
12626
12589
  const safe = agentOsId.replace(/[^A-Za-z0-9_-]/g, "_");
12627
- return path55.join(homedir14(), ".kynver", `daemon-heartbeat-${safe}.json`);
12590
+ return path54.join(homedir14(), ".kynver", `daemon-heartbeat-${safe}.json`);
12628
12591
  }
12629
12592
  function writeDaemonHeartbeat(input) {
12630
12593
  try {
12631
12594
  const file = daemonHeartbeatPath(input.agentOsId);
12632
- mkdirSync7(path55.dirname(file), { recursive: true });
12595
+ mkdirSync7(path54.dirname(file), { recursive: true });
12633
12596
  const beat = {
12634
12597
  observedAt: (input.now ?? /* @__PURE__ */ new Date()).toISOString(),
12635
12598
  pid: process.pid,
@@ -12679,9 +12642,9 @@ function assertNativeDaemonAllowed() {
12679
12642
 
12680
12643
  // src/cron/cron-env.ts
12681
12644
  init_config();
12682
- import { existsSync as existsSync42 } from "node:fs";
12645
+ import { existsSync as existsSync41 } from "node:fs";
12683
12646
  import { homedir as homedir15 } from "node:os";
12684
- import path56 from "node:path";
12647
+ import path55 from "node:path";
12685
12648
  function envFlag4(name, defaultValue) {
12686
12649
  const raw = process.env[name]?.trim().toLowerCase();
12687
12650
  if (!raw) return defaultValue;
@@ -12697,7 +12660,7 @@ function envInt(name, fallback, min = 1) {
12697
12660
  function defaultKynverCronStorePath() {
12698
12661
  const explicit = process.env.KYNVER_CRON_STORE_PATH?.trim() || process.env.OPENCLAW_CRON_STORE_PATH?.trim();
12699
12662
  if (explicit) return explicit;
12700
- return path56.join(homedir15(), ".kynver", "agent-os-cron.json");
12663
+ return path55.join(homedir15(), ".kynver", "agent-os-cron.json");
12701
12664
  }
12702
12665
  function defaultKynverCronStatePath(storePath = defaultKynverCronStorePath()) {
12703
12666
  const explicit = process.env.KYNVER_CRON_TICK_STATE_PATH?.trim();
@@ -12717,7 +12680,7 @@ function resolveKynverCronEnv() {
12717
12680
  const fireBaseUrl = resolveKynverCronFireBaseUrl();
12718
12681
  const secret = resolveKynverCronSecret();
12719
12682
  const credsReady = Boolean(fireBaseUrl && secret);
12720
- const storeExists = existsSync42(storePath);
12683
+ const storeExists = existsSync41(storePath);
12721
12684
  const defaultEnabled = credsReady && (storeExists || envFlag4("KYNVER_CRON_TICK_FORCE", false));
12722
12685
  return {
12723
12686
  storePath,
@@ -12771,10 +12734,10 @@ async function fireKynverCronJob(input) {
12771
12734
 
12772
12735
  // src/cron/cron-lock.ts
12773
12736
  init_util();
12774
- import { closeSync as closeSync6, existsSync as existsSync43, openSync as openSync6, readFileSync as readFileSync17, unlinkSync as unlinkSync3, writeFileSync as writeFileSync5 } from "node:fs";
12737
+ import { closeSync as closeSync6, existsSync as existsSync42, openSync as openSync6, readFileSync as readFileSync17, unlinkSync as unlinkSync3, writeFileSync as writeFileSync5 } from "node:fs";
12775
12738
  var STALE_LOCK_MS = 10 * 6e4;
12776
12739
  function readLockInfo(lockPath) {
12777
- if (!existsSync43(lockPath)) return null;
12740
+ if (!existsSync42(lockPath)) return null;
12778
12741
  try {
12779
12742
  const parsed = JSON.parse(readFileSync17(lockPath, "utf8"));
12780
12743
  if (typeof parsed.pid === "number" && typeof parsed.at === "string") return parsed;
@@ -12792,14 +12755,14 @@ function lockIsStale(lockPath) {
12792
12755
  return Date.now() - atMs > STALE_LOCK_MS;
12793
12756
  }
12794
12757
  function tryAcquireCronTickLock(lockPath) {
12795
- if (existsSync43(lockPath) && !lockIsStale(lockPath)) {
12758
+ if (existsSync42(lockPath) && !lockIsStale(lockPath)) {
12796
12759
  const info = readLockInfo(lockPath);
12797
12760
  return {
12798
12761
  acquired: false,
12799
12762
  reason: info ? `held by pid ${info.pid}` : "held by another process"
12800
12763
  };
12801
12764
  }
12802
- if (existsSync43(lockPath)) {
12765
+ if (existsSync42(lockPath)) {
12803
12766
  try {
12804
12767
  unlinkSync3(lockPath);
12805
12768
  } catch {
@@ -12936,11 +12899,34 @@ async function loadCronJobs(storePath = defaultKynverCronStorePath()) {
12936
12899
  const raw = await readFileIfExists(storePath);
12937
12900
  return parseCronStore(raw);
12938
12901
  }
12902
+ async function writeCronStore(storePath, entries) {
12903
+ const { mkdir, writeFile, rename } = await import("node:fs/promises");
12904
+ const { randomBytes: randomBytes3 } = await import("node:crypto");
12905
+ const { dirname: dirname3 } = await import("node:path");
12906
+ await mkdir(dirname3(storePath), { recursive: true });
12907
+ const tmp = `${storePath}.${randomBytes3(4).toString("hex")}.tmp`;
12908
+ const body = JSON.stringify({ entries }, null, 2);
12909
+ await writeFile(tmp, body, "utf8");
12910
+ await rename(tmp, storePath);
12911
+ }
12912
+ async function saveCronJob(entry, storePath = defaultKynverCronStorePath()) {
12913
+ const entries = await loadCronJobs(storePath);
12914
+ const idx = entries.findIndex((e) => e.providerScheduleId === entry.providerScheduleId);
12915
+ if (idx >= 0) entries[idx] = entry;
12916
+ else entries.push(entry);
12917
+ await writeCronStore(storePath, entries);
12918
+ }
12919
+ async function ensureCronStoreInitialized(storePath = defaultKynverCronStorePath()) {
12920
+ const raw = await readFileIfExists(storePath);
12921
+ if (raw !== null) return { created: false };
12922
+ await writeCronStore(storePath, []);
12923
+ return { created: true };
12924
+ }
12939
12925
 
12940
12926
  // src/cron/cron-tick-state.ts
12941
12927
  import { randomBytes } from "node:crypto";
12942
12928
  import { promises as fs4 } from "node:fs";
12943
- import path57 from "node:path";
12929
+ import path56 from "node:path";
12944
12930
  var EMPTY = { version: 1, jobs: {} };
12945
12931
  async function readFileIfExists2(filePath) {
12946
12932
  try {
@@ -12967,7 +12953,7 @@ async function loadCronTickState(statePath) {
12967
12953
  return parseCronTickState(raw);
12968
12954
  }
12969
12955
  async function writeStateAtomic(statePath, state) {
12970
- await fs4.mkdir(path57.dirname(statePath), { recursive: true });
12956
+ await fs4.mkdir(path56.dirname(statePath), { recursive: true });
12971
12957
  const suffix = randomBytes(6).toString("hex");
12972
12958
  const tmp = `${statePath}.tmp-${process.pid}-${Date.now()}-${suffix}`;
12973
12959
  await fs4.writeFile(tmp, `${JSON.stringify(state, null, 2)}
@@ -13147,7 +13133,7 @@ async function runKynverCronTick(opts = {}) {
13147
13133
  init_util();
13148
13134
 
13149
13135
  // src/pipeline-tick.ts
13150
- import path60 from "node:path";
13136
+ import path59 from "node:path";
13151
13137
  init_config();
13152
13138
 
13153
13139
  // src/pipeline-dispatch.ts
@@ -13289,7 +13275,7 @@ init_util();
13289
13275
  init_status();
13290
13276
  init_run_store();
13291
13277
  init_util();
13292
- import path58 from "node:path";
13278
+ import path57 from "node:path";
13293
13279
 
13294
13280
  // src/plan-progress-sync.ts
13295
13281
  init_config();
@@ -13314,7 +13300,7 @@ async function syncActiveWorkerPlanProgress(runId, args) {
13314
13300
  const outcomes = [];
13315
13301
  for (const name of Object.keys(run.workers || {})) {
13316
13302
  const worker = readJson(
13317
- path58.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
13303
+ path57.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
13318
13304
  void 0
13319
13305
  );
13320
13306
  if (!worker?.dispatched || !worker.taskId) continue;
@@ -13682,10 +13668,10 @@ function collectProviderEvidence(wanted, opts = {}) {
13682
13668
  // src/provider-evidence/wanted-store.ts
13683
13669
  init_run_store();
13684
13670
  init_util();
13685
- import path59 from "node:path";
13671
+ import path58 from "node:path";
13686
13672
  var WANTED_FILE = "provider-evidence-wanted.json";
13687
13673
  function wantedFilePath(runId) {
13688
- return path59.join(runDirectory(runId), WANTED_FILE);
13674
+ return path58.join(runDirectory(runId), WANTED_FILE);
13689
13675
  }
13690
13676
  function parseWantedItems(value) {
13691
13677
  if (!Array.isArray(value)) return [];
@@ -13724,7 +13710,7 @@ async function completeFinishedWorkers(runId, args) {
13724
13710
  const outcomes = [];
13725
13711
  for (const name of Object.keys(run.workers || {})) {
13726
13712
  const worker = readJson(
13727
- path60.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
13713
+ path59.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
13728
13714
  void 0
13729
13715
  );
13730
13716
  if (!worker?.taskId || worker.localOnly) continue;
@@ -13892,6 +13878,389 @@ async function runPipelineTick(args) {
13892
13878
  };
13893
13879
  }
13894
13880
 
13881
+ // src/chat/chat-claim-loop.ts
13882
+ init_config();
13883
+ import os11 from "node:os";
13884
+
13885
+ // src/chat/anthropic-credentials.ts
13886
+ import { readFileSync as readFileSync18 } from "node:fs";
13887
+ import { homedir as homedir16, platform } from "node:os";
13888
+ import path60 from "node:path";
13889
+ import { execFileSync as execFileSync4 } from "node:child_process";
13890
+ function parseClaudeCredentials(raw, now = Date.now()) {
13891
+ try {
13892
+ const blob = JSON.parse(raw);
13893
+ const token = blob.claudeAiOauth?.accessToken;
13894
+ const expiresAt = blob.claudeAiOauth?.expiresAt;
13895
+ if (typeof token !== "string" || !token) return null;
13896
+ if (typeof expiresAt === "number" && expiresAt - now < 6e4) return null;
13897
+ return token;
13898
+ } catch {
13899
+ return null;
13900
+ }
13901
+ }
13902
+ function readClaudeCliToken() {
13903
+ if (platform() === "darwin") {
13904
+ try {
13905
+ const raw = execFileSync4(
13906
+ "security",
13907
+ ["find-generic-password", "-s", "Claude Code-credentials", "-w"],
13908
+ { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }
13909
+ );
13910
+ return parseClaudeCredentials(raw.trim());
13911
+ } catch {
13912
+ return null;
13913
+ }
13914
+ }
13915
+ try {
13916
+ const raw = readFileSync18(path60.join(homedir16(), ".claude", ".credentials.json"), "utf8");
13917
+ return parseClaudeCredentials(raw);
13918
+ } catch {
13919
+ return null;
13920
+ }
13921
+ }
13922
+ function resolveLocalAnthropicCredentials(env = process.env) {
13923
+ const apiKey = env.ANTHROPIC_API_KEY?.trim();
13924
+ if (apiKey) return { kind: "api_key", key: apiKey };
13925
+ const optIn = env.KYNVER_CHAT_USE_CLAUDE_OAUTH;
13926
+ if (optIn === "1" || optIn === "true" || optIn === "yes") {
13927
+ const token = readClaudeCliToken();
13928
+ if (token) return { kind: "oauth", token };
13929
+ }
13930
+ return null;
13931
+ }
13932
+
13933
+ // src/chat/anthropic-stream.ts
13934
+ var CLAUDE_CODE_IDENTITY = "You are Claude Code, Anthropic's official CLI for Claude.";
13935
+ function createAnthropicAccumulator() {
13936
+ return { blocks: [], stopReason: null, inputTokens: 0, outputTokens: 0 };
13937
+ }
13938
+ function foldAnthropicEvent(acc, event) {
13939
+ const e = event;
13940
+ switch (e.type) {
13941
+ case "message_start":
13942
+ acc.inputTokens = e.message?.usage?.input_tokens ?? 0;
13943
+ return "";
13944
+ case "content_block_start": {
13945
+ const idx = e.index ?? acc.blocks.length;
13946
+ acc.blocks[idx] = {
13947
+ type: e.content_block?.type === "tool_use" ? "tool_use" : "text",
13948
+ text: "",
13949
+ id: e.content_block?.id,
13950
+ name: e.content_block?.name,
13951
+ inputJson: ""
13952
+ };
13953
+ return "";
13954
+ }
13955
+ case "content_block_delta": {
13956
+ const block = acc.blocks[e.index ?? acc.blocks.length - 1];
13957
+ if (!block) return "";
13958
+ if (e.delta?.type === "text_delta" && e.delta.text) {
13959
+ block.text += e.delta.text;
13960
+ return e.delta.text;
13961
+ }
13962
+ if (e.delta?.type === "input_json_delta" && e.delta.partial_json) {
13963
+ block.inputJson += e.delta.partial_json;
13964
+ }
13965
+ return "";
13966
+ }
13967
+ case "message_delta":
13968
+ if (e.delta?.stop_reason) acc.stopReason = e.delta.stop_reason;
13969
+ if (typeof e.usage?.output_tokens === "number") acc.outputTokens = e.usage.output_tokens;
13970
+ return "";
13971
+ default:
13972
+ return "";
13973
+ }
13974
+ }
13975
+ function toLocalTurnMessage(acc) {
13976
+ const content = [];
13977
+ for (const block of acc.blocks) {
13978
+ if (!block) continue;
13979
+ if (block.type === "tool_use") {
13980
+ let input = {};
13981
+ try {
13982
+ input = block.inputJson ? JSON.parse(block.inputJson) : {};
13983
+ } catch {
13984
+ input = {};
13985
+ }
13986
+ content.push({ type: "tool_use", id: block.id ?? "", name: block.name ?? "", input });
13987
+ } else if (block.text) {
13988
+ content.push({ type: "text", text: block.text });
13989
+ }
13990
+ }
13991
+ return {
13992
+ content,
13993
+ stop_reason: acc.stopReason ?? "end_turn",
13994
+ usage: { input_tokens: acc.inputTokens, output_tokens: acc.outputTokens }
13995
+ };
13996
+ }
13997
+ async function* parseSseLines(body) {
13998
+ const reader = body.getReader();
13999
+ const decoder = new TextDecoder();
14000
+ let buffer = "";
14001
+ try {
14002
+ for (; ; ) {
14003
+ const { done, value } = await reader.read();
14004
+ if (done) break;
14005
+ buffer += decoder.decode(value, { stream: true });
14006
+ let idx;
14007
+ while ((idx = buffer.indexOf("\n")) !== -1) {
14008
+ const line = buffer.slice(0, idx).trim();
14009
+ buffer = buffer.slice(idx + 1);
14010
+ if (!line.startsWith("data:")) continue;
14011
+ const payload = line.slice(5).trim();
14012
+ if (!payload || payload === "[DONE]") continue;
14013
+ try {
14014
+ yield JSON.parse(payload);
14015
+ } catch {
14016
+ }
14017
+ }
14018
+ }
14019
+ } finally {
14020
+ reader.releaseLock();
14021
+ }
14022
+ }
14023
+ function getAnthropicBaseUrl(env = process.env) {
14024
+ return (env.ANTHROPIC_BASE_URL?.trim() || "https://api.anthropic.com").replace(/\/$/, "");
14025
+ }
14026
+ async function streamLocalAnthropicTurn(input) {
14027
+ const { creds, model, payload } = input;
14028
+ const headers = {
14029
+ "Content-Type": "application/json",
14030
+ "anthropic-version": "2023-06-01"
14031
+ };
14032
+ let system = payload.system;
14033
+ if (creds.kind === "api_key") {
14034
+ headers["x-api-key"] = creds.key;
14035
+ } else {
14036
+ headers.Authorization = `Bearer ${creds.token}`;
14037
+ headers["anthropic-beta"] = "oauth-2025-04-20";
14038
+ system = [
14039
+ { type: "text", text: CLAUDE_CODE_IDENTITY },
14040
+ { type: "text", text: payload.system }
14041
+ ];
14042
+ }
14043
+ const res = await fetch(`${getAnthropicBaseUrl()}/v1/messages`, {
14044
+ method: "POST",
14045
+ headers,
14046
+ signal: input.signal,
14047
+ body: JSON.stringify({
14048
+ model,
14049
+ max_tokens: payload.maxTokens,
14050
+ system,
14051
+ messages: payload.messages,
14052
+ ...Array.isArray(payload.tools) && payload.tools.length > 0 ? { tools: payload.tools } : {},
14053
+ ...payload.temperature !== void 0 ? { temperature: payload.temperature } : {},
14054
+ stream: true
14055
+ })
14056
+ });
14057
+ if (!res.ok || !res.body) {
14058
+ const detail = await res.text().catch(() => "");
14059
+ throw new Error(`Anthropic stream failed: HTTP ${res.status}${detail ? ` \u2014 ${detail.slice(0, 300)}` : ""}`);
14060
+ }
14061
+ const acc = createAnthropicAccumulator();
14062
+ for await (const event of parseSseLines(res.body)) {
14063
+ const delta = foldAnthropicEvent(acc, event);
14064
+ if (delta) input.onDelta(delta);
14065
+ }
14066
+ return toLocalTurnMessage(acc);
14067
+ }
14068
+
14069
+ // src/chat/delta-batcher.ts
14070
+ var DELTA_FLUSH_FLOOR_MS = 150;
14071
+ var DeltaBatcher = class {
14072
+ constructor(flushFn, floorMs = DELTA_FLUSH_FLOOR_MS) {
14073
+ this.flushFn = flushFn;
14074
+ this.floorMs = floorMs;
14075
+ }
14076
+ buffer = "";
14077
+ timer = null;
14078
+ closed = false;
14079
+ push(text) {
14080
+ if (this.closed || !text) return;
14081
+ this.buffer += text;
14082
+ if (!this.timer) {
14083
+ this.timer = setTimeout(() => {
14084
+ this.timer = null;
14085
+ this.flushNow();
14086
+ }, this.floorMs);
14087
+ }
14088
+ }
14089
+ flushNow() {
14090
+ if (!this.buffer) return;
14091
+ const text = this.buffer;
14092
+ this.buffer = "";
14093
+ this.flushFn(text);
14094
+ }
14095
+ /** Flush any remainder and stop the timer. Idempotent. */
14096
+ close() {
14097
+ if (this.closed) return;
14098
+ this.closed = true;
14099
+ if (this.timer) {
14100
+ clearTimeout(this.timer);
14101
+ this.timer = null;
14102
+ }
14103
+ this.flushNow();
14104
+ }
14105
+ };
14106
+
14107
+ // src/chat/chat-claim-loop.ts
14108
+ var CLAIM_WAIT_MS = 25e3;
14109
+ var CLAIM_FETCH_TIMEOUT_MS = 32e3;
14110
+ var ERROR_BACKOFF_MS = 5e3;
14111
+ var AUTH_BACKOFF_MS = 6e4;
14112
+ var DEFAULT_CHAT_MODEL = "claude-sonnet-4-6";
14113
+ function resolveChatBridgeTarget(env = process.env) {
14114
+ const bridgeUrl = env.KYNVER_RUNTIME_CHAT_BRIDGE_URL?.trim();
14115
+ if (!bridgeUrl) return null;
14116
+ const config = loadUserConfig();
14117
+ const agentOsId = config.agentOsId?.trim();
14118
+ const apiKey = loadApiKey();
14119
+ if (!agentOsId || !apiKey) return null;
14120
+ return {
14121
+ bridgeUrl: bridgeUrl.replace(/\/$/, ""),
14122
+ agentOsId,
14123
+ apiKey,
14124
+ boxId: os11.hostname(),
14125
+ model: env.KYNVER_CHAT_MODEL?.trim() || config.defaultModel?.trim() || DEFAULT_CHAT_MODEL
14126
+ };
14127
+ }
14128
+ async function claimOnce(target, shouldStop) {
14129
+ const controller = new AbortController();
14130
+ const timer = setTimeout(() => controller.abort(), CLAIM_FETCH_TIMEOUT_MS);
14131
+ const stopPoll = setInterval(() => {
14132
+ if (shouldStop()) controller.abort();
14133
+ }, 500);
14134
+ try {
14135
+ const res = await fetch(`${target.bridgeUrl}/runtime-chat/claim`, {
14136
+ method: "POST",
14137
+ headers: {
14138
+ "Content-Type": "application/json",
14139
+ Authorization: `Bearer ${target.apiKey}`
14140
+ },
14141
+ body: JSON.stringify({
14142
+ agentOsId: target.agentOsId,
14143
+ boxId: target.boxId,
14144
+ waitMs: CLAIM_WAIT_MS
14145
+ }),
14146
+ signal: controller.signal
14147
+ });
14148
+ if (res.status === 204) return null;
14149
+ if (res.status === 401 || res.status === 403) return "auth_error";
14150
+ if (!res.ok) throw new Error(`claim failed: HTTP ${res.status}`);
14151
+ const body = await res.json().catch(() => null);
14152
+ return body?.turn ?? null;
14153
+ } catch (err) {
14154
+ if (shouldStop()) return null;
14155
+ throw err;
14156
+ } finally {
14157
+ clearTimeout(timer);
14158
+ clearInterval(stopPoll);
14159
+ }
14160
+ }
14161
+ async function pushEvents(target, turnId, events) {
14162
+ const res = await fetch(`${target.bridgeUrl}/runtime-chat/turns/${encodeURIComponent(turnId)}/events`, {
14163
+ method: "POST",
14164
+ headers: {
14165
+ "Content-Type": "application/json",
14166
+ Authorization: `Bearer ${target.apiKey}`
14167
+ },
14168
+ body: JSON.stringify({ agentOsId: target.agentOsId, events })
14169
+ });
14170
+ return res.ok;
14171
+ }
14172
+ async function executeChatTurn(target, turn) {
14173
+ let seq = 0;
14174
+ let turnClosed = false;
14175
+ const abort = new AbortController();
14176
+ const sendBatch = (text) => {
14177
+ if (turnClosed) return;
14178
+ void pushEvents(target, turn.turnId, [{ seq: seq++, kind: "delta", text }]).then((ok) => {
14179
+ if (!ok) {
14180
+ turnClosed = true;
14181
+ abort.abort();
14182
+ }
14183
+ });
14184
+ };
14185
+ const batcher = new DeltaBatcher(sendBatch);
14186
+ const creds = resolveLocalAnthropicCredentials();
14187
+ if (!creds) {
14188
+ await pushEvents(target, turn.turnId, [
14189
+ { seq: 0, kind: "error", error: "no_local_credentials" }
14190
+ ]).catch(() => {
14191
+ });
14192
+ return;
14193
+ }
14194
+ try {
14195
+ const message = await streamLocalAnthropicTurn({
14196
+ creds,
14197
+ model: target.model,
14198
+ payload: turn.payload,
14199
+ onDelta: (text) => batcher.push(text),
14200
+ signal: abort.signal
14201
+ });
14202
+ batcher.close();
14203
+ if (!turnClosed) {
14204
+ await pushEvents(target, turn.turnId, [{ seq: seq++, kind: "final", message }]);
14205
+ }
14206
+ console.error(JSON.stringify({ event: "chat_turn_done", turnId: turn.turnId, model: target.model }));
14207
+ } catch (err) {
14208
+ batcher.close();
14209
+ if (!turnClosed) {
14210
+ await pushEvents(target, turn.turnId, [
14211
+ { seq: seq++, kind: "error", error: err instanceof Error ? err.message : String(err) }
14212
+ ]).catch(() => {
14213
+ });
14214
+ }
14215
+ console.error(
14216
+ JSON.stringify({
14217
+ event: "chat_turn_error",
14218
+ turnId: turn.turnId,
14219
+ error: err instanceof Error ? err.message : String(err)
14220
+ })
14221
+ );
14222
+ }
14223
+ }
14224
+ async function sleep2(ms, shouldStop) {
14225
+ const step = 500;
14226
+ for (let waited = 0; waited < ms && !shouldStop(); waited += step) {
14227
+ await new Promise((resolve2) => setTimeout(resolve2, Math.min(step, ms - waited)));
14228
+ }
14229
+ }
14230
+ async function runChatClaimLoop(opts) {
14231
+ const target = resolveChatBridgeTarget();
14232
+ if (!target) return;
14233
+ console.error(
14234
+ JSON.stringify({
14235
+ event: "chat_claim_loop_start",
14236
+ bridgeUrl: target.bridgeUrl,
14237
+ agentOsId: target.agentOsId,
14238
+ boxId: target.boxId,
14239
+ model: target.model
14240
+ })
14241
+ );
14242
+ while (!opts.shouldStop()) {
14243
+ try {
14244
+ const claimed = await claimOnce(target, opts.shouldStop);
14245
+ if (claimed === "auth_error") {
14246
+ console.error(JSON.stringify({ event: "chat_claim_auth_error" }));
14247
+ await sleep2(AUTH_BACKOFF_MS, opts.shouldStop);
14248
+ continue;
14249
+ }
14250
+ if (claimed) await executeChatTurn(target, claimed);
14251
+ } catch (err) {
14252
+ console.error(
14253
+ JSON.stringify({
14254
+ event: "chat_claim_error",
14255
+ error: err instanceof Error ? err.message : String(err)
14256
+ })
14257
+ );
14258
+ await sleep2(ERROR_BACKOFF_MS, opts.shouldStop);
14259
+ }
14260
+ }
14261
+ console.error(JSON.stringify({ event: "chat_claim_loop_stop" }));
14262
+ }
14263
+
13895
14264
  // src/daemon.ts
13896
14265
  var DEFAULT_INTERVAL_MS = 6e4;
13897
14266
  var IDLE_INTERVAL_MS = 5 * 6e4;
@@ -13944,6 +14313,14 @@ async function runDaemon(args) {
13944
14313
  })
13945
14314
  );
13946
14315
  const cronEnv = resolveKynverCronEnv();
14316
+ const chatLoop = runChatClaimLoop({ shouldStop: () => stopping }).catch((err) => {
14317
+ console.error(
14318
+ JSON.stringify({
14319
+ event: "chat_claim_loop_crashed",
14320
+ error: err instanceof Error ? err.message : String(err)
14321
+ })
14322
+ );
14323
+ });
13947
14324
  while (!stopping) {
13948
14325
  try {
13949
14326
  writeDaemonHeartbeat({ agentOsId, runId });
@@ -13970,40 +14347,143 @@ async function runDaemon(args) {
13970
14347
  await awaitDaemonBackoff(intervalMs, () => stopping);
13971
14348
  }
13972
14349
  }
14350
+ await chatLoop;
13973
14351
  console.error(JSON.stringify({ event: "daemon_stop", runId, agentOsId }));
13974
14352
  }
13975
14353
 
13976
- // src/daemon-keeper.ts
13977
- init_config();
13978
- import { spawn as spawn6 } from "node:child_process";
13979
- init_util();
13980
- var DEFAULT_STALL_MS = 15 * 6e4;
13981
- var STARTUP_GRACE_MS = 2 * 6e4;
13982
- var KILL_GRACE_MS = 1e4;
13983
- var BACKOFF_BASE_MS = 5e3;
13984
- var BACKOFF_CAP_MS = 5 * 6e4;
13985
- var HEALTHY_RESET_MS = 30 * 6e4;
13986
- var POLL_MS = 5e3;
13987
- function resolveKeeperStallMs(flag, env = process.env) {
13988
- const fromFlag = typeof flag === "string" ? Number.parseInt(flag, 10) : NaN;
13989
- if (Number.isFinite(fromFlag) && fromFlag > 0) return fromFlag;
13990
- const fromEnv = Number.parseInt(env.KYNVER_DAEMON_STALL_MS ?? "", 10);
13991
- if (Number.isFinite(fromEnv) && fromEnv > 0) return fromEnv;
13992
- return DEFAULT_STALL_MS;
14354
+ // src/start.ts
14355
+ init_run_store();
14356
+ function resolveStartRunId(runs, repo) {
14357
+ const candidates = runs.filter((r) => r.repo === repo && !TERMINAL_RUN_STATUSES.has(r.status)).sort((a, b) => (b.createdAt ?? "").localeCompare(a.createdAt ?? ""));
14358
+ return candidates[0]?.id ?? null;
13993
14359
  }
13994
- function shouldRunDaemonKeeper(args, env = process.env) {
13995
- if (args.keeperChild === true || args.keeperChild === "true") return false;
13996
- if (args.noSupervise === true || args.noSupervise === "true") return false;
13997
- if (args.supervised === "false") return false;
13998
- const envFlag5 = (env.KYNVER_DAEMON_SUPERVISED ?? "").trim().toLowerCase();
13999
- if (envFlag5 === "0" || envFlag5 === "false" || envFlag5 === "no" || envFlag5 === "off") {
14000
- return false;
14360
+ async function runStart(args) {
14361
+ assertNativeDaemonAllowed();
14362
+ let config = loadUserConfig();
14363
+ if (!loadApiKey() || !config.agentOsId?.trim()) {
14364
+ console.log(" This machine isn't linked yet \u2014 running bootstrap first.");
14365
+ await runBootstrap(args);
14366
+ config = loadUserConfig();
14001
14367
  }
14002
- return true;
14368
+ const agentOsId = (typeof args.agentOsId === "string" ? args.agentOsId.trim() : "") || config.agentOsId?.trim() || "";
14369
+ if (!agentOsId) {
14370
+ console.error("No AgentOS workspace configured \u2014 run `kynver bootstrap` (or pass --agent-os-id).");
14371
+ process.exit(1);
14372
+ }
14373
+ const repo = (typeof args.repo === "string" ? args.repo.trim() : "") || config.defaultRepo?.trim() || resolveDefaultRepo()?.repo || "";
14374
+ if (!repo) {
14375
+ console.error("No repo configured \u2014 pass --repo /path/to/repo or run `kynver setup --discover-repo`.");
14376
+ process.exit(1);
14377
+ }
14378
+ let runId = typeof args.run === "string" && args.run.trim() ? args.run.trim() : "";
14379
+ if (!runId) {
14380
+ runId = resolveStartRunId(listRunRecords(), repo) ?? "";
14381
+ if (runId) {
14382
+ console.log(` Reusing run ${runId} for ${repo}.`);
14383
+ } else {
14384
+ runId = createRun({ ...args, repo, name: "agent" }).runId;
14385
+ }
14386
+ }
14387
+ console.log("");
14388
+ console.log(` ${os12.hostname()} \u2014 agent coming online`);
14389
+ console.log(` workspace: ${agentOsId}`);
14390
+ console.log(` repo: ${repo}`);
14391
+ console.log(` run: ${runId}`);
14392
+ console.log(" Ctrl-C stops the agent. (Advanced control: `kynver daemon --help`.)");
14393
+ console.log("");
14394
+ await runDaemon({ ...args, run: runId, agentOsId });
14003
14395
  }
14004
- function nextKeeperBackoffMs(consecutiveFailures, base = BACKOFF_BASE_MS, cap = BACKOFF_CAP_MS) {
14005
- const exp = Math.min(Math.max(consecutiveFailures, 1) - 1, 10);
14006
- return Math.min(base * 2 ** exp, cap);
14396
+
14397
+ // src/cli.ts
14398
+ init_run_store();
14399
+
14400
+ // src/discard-disposable.ts
14401
+ init_run_store();
14402
+ init_status();
14403
+ import { existsSync as existsSync43, rmSync as rmSync4 } from "node:fs";
14404
+ import path61 from "node:path";
14405
+ function normalizeRelativePath2(value) {
14406
+ const normalized = value.replace(/\\/g, "/").replace(/^\.\//, "").trim();
14407
+ if (!normalized || normalized.startsWith("/") || normalized.includes("..")) {
14408
+ throw new Error(`unsafe path: ${value}`);
14409
+ }
14410
+ return normalized;
14411
+ }
14412
+ function parsePathsArg(raw) {
14413
+ if (typeof raw !== "string" || !raw.trim()) return [];
14414
+ return raw.split(",").map((p) => p.trim()).filter(Boolean);
14415
+ }
14416
+ function discardDisposableArtifacts(args) {
14417
+ const { runId, workerName } = resolveWorkerTargetArgs(args);
14418
+ const worker = loadWorker(runId, workerName);
14419
+ const paths = [
14420
+ ...parsePathsArg(args.path),
14421
+ ...Array.isArray(args.paths) ? args.paths : []
14422
+ ];
14423
+ if (paths.length === 0) {
14424
+ return { ok: false, removed: [], reason: "requires at least one --path" };
14425
+ }
14426
+ const worktreeRoot = path61.resolve(worker.worktreePath);
14427
+ const removed = [];
14428
+ for (const raw of paths) {
14429
+ const rel = normalizeRelativePath2(raw);
14430
+ const abs = path61.resolve(worktreeRoot, rel);
14431
+ if (!abs.startsWith(worktreeRoot + path61.sep) && abs !== worktreeRoot) {
14432
+ return { ok: false, removed, reason: `path escapes worktree: ${raw}` };
14433
+ }
14434
+ if (!existsSync43(abs)) {
14435
+ return { ok: false, removed, reason: `path not found: ${raw}` };
14436
+ }
14437
+ rmSync4(abs, { recursive: true, force: true });
14438
+ removed.push(rel);
14439
+ }
14440
+ const prior = Array.isArray(worker.disposableArtifactsRemoved) ? worker.disposableArtifactsRemoved.filter((p) => typeof p === "string") : [];
14441
+ worker.disposableArtifactsRemoved = [.../* @__PURE__ */ new Set([...prior, ...removed])];
14442
+ saveWorker(worker.runId, worker);
14443
+ const status = computeWorkerStatus(worker);
14444
+ return {
14445
+ ok: true,
14446
+ removed,
14447
+ ...status.changedFiles.length ? { reason: "worktree still has other changes" } : {}
14448
+ };
14449
+ }
14450
+ function discardDisposableCli(args) {
14451
+ const result = discardDisposableArtifacts(args);
14452
+ console.log(JSON.stringify(result, null, 2));
14453
+ if (!result.ok) process.exit(1);
14454
+ }
14455
+
14456
+ // src/daemon-keeper.ts
14457
+ init_config();
14458
+ import { spawn as spawn6 } from "node:child_process";
14459
+ init_util();
14460
+ var DEFAULT_STALL_MS = 15 * 6e4;
14461
+ var STARTUP_GRACE_MS = 2 * 6e4;
14462
+ var KILL_GRACE_MS = 1e4;
14463
+ var BACKOFF_BASE_MS = 5e3;
14464
+ var BACKOFF_CAP_MS = 5 * 6e4;
14465
+ var HEALTHY_RESET_MS = 30 * 6e4;
14466
+ var POLL_MS = 5e3;
14467
+ function resolveKeeperStallMs(flag, env = process.env) {
14468
+ const fromFlag = typeof flag === "string" ? Number.parseInt(flag, 10) : NaN;
14469
+ if (Number.isFinite(fromFlag) && fromFlag > 0) return fromFlag;
14470
+ const fromEnv = Number.parseInt(env.KYNVER_DAEMON_STALL_MS ?? "", 10);
14471
+ if (Number.isFinite(fromEnv) && fromEnv > 0) return fromEnv;
14472
+ return DEFAULT_STALL_MS;
14473
+ }
14474
+ function shouldRunDaemonKeeper(args, env = process.env) {
14475
+ if (args.keeperChild === true || args.keeperChild === "true") return false;
14476
+ if (args.noSupervise === true || args.noSupervise === "true") return false;
14477
+ if (args.supervised === "false") return false;
14478
+ const envFlag5 = (env.KYNVER_DAEMON_SUPERVISED ?? "").trim().toLowerCase();
14479
+ if (envFlag5 === "0" || envFlag5 === "false" || envFlag5 === "no" || envFlag5 === "off") {
14480
+ return false;
14481
+ }
14482
+ return true;
14483
+ }
14484
+ function nextKeeperBackoffMs(consecutiveFailures, base = BACKOFF_BASE_MS, cap = BACKOFF_CAP_MS) {
14485
+ const exp = Math.min(Math.max(consecutiveFailures, 1) - 1, 10);
14486
+ return Math.min(base * 2 ** exp, cap);
14007
14487
  }
14008
14488
  function keeperRunWasHealthy(startedAtMs, endedAtMs, healthyMs = HEALTHY_RESET_MS) {
14009
14489
  return endedAtMs - startedAtMs >= healthyMs;
@@ -14106,7 +14586,7 @@ async function runDaemonKeeper(args, rawArgv = process.argv.slice(2)) {
14106
14586
 
14107
14587
  // src/plan-progress.ts
14108
14588
  init_config();
14109
- import path64 from "node:path";
14589
+ import path65 from "node:path";
14110
14590
 
14111
14591
  // src/bounded-build/constants.ts
14112
14592
  var DEFAULT_BUILD_MEM_BUDGET_BYTES = 1536 * 1024 * 1024;
@@ -14251,20 +14731,20 @@ import {
14251
14731
  mkdirSync as mkdirSync9,
14252
14732
  openSync as openSync7,
14253
14733
  readdirSync as readdirSync15,
14254
- readFileSync as readFileSync18,
14734
+ readFileSync as readFileSync19,
14255
14735
  unlinkSync as unlinkSync4,
14256
14736
  writeFileSync as writeFileSync6
14257
14737
  } from "node:fs";
14258
- import path62 from "node:path";
14738
+ import path63 from "node:path";
14259
14739
 
14260
14740
  // src/heavy-verification/paths.ts
14261
14741
  import { mkdirSync as mkdirSync8 } from "node:fs";
14262
- import path61 from "node:path";
14742
+ import path62 from "node:path";
14263
14743
  function resolveHeavyVerificationRoot() {
14264
- return path61.join(resolveKynverStateRoot(), "heavy-verification");
14744
+ return path62.join(resolveKynverStateRoot(), "heavy-verification");
14265
14745
  }
14266
14746
  function heavyVerificationSlotsDir() {
14267
- return path61.join(resolveHeavyVerificationRoot(), "slots");
14747
+ return path62.join(resolveHeavyVerificationRoot(), "slots");
14268
14748
  }
14269
14749
  function ensureHeavyVerificationDirs() {
14270
14750
  const dir = heavyVerificationSlotsDir();
@@ -14293,12 +14773,12 @@ function indexedSlotId(index) {
14293
14773
  return `slot-${index}`;
14294
14774
  }
14295
14775
  function slotFilePath(slotId, slotsDir = heavyVerificationSlotsDir()) {
14296
- return path62.join(slotsDir, `${slotId}.json`);
14776
+ return path63.join(slotsDir, `${slotId}.json`);
14297
14777
  }
14298
14778
  function readSlotRecord(filePath) {
14299
14779
  if (!existsSync44(filePath)) return null;
14300
14780
  try {
14301
- const parsed = JSON.parse(readFileSync18(filePath, "utf8"));
14781
+ const parsed = JSON.parse(readFileSync19(filePath, "utf8"));
14302
14782
  if (typeof parsed.slotId === "string" && typeof parsed.pid === "number" && typeof parsed.acquiredAt === "string" && typeof parsed.command === "string") {
14303
14783
  return parsed;
14304
14784
  }
@@ -14332,7 +14812,7 @@ function reclaimStaleHeavyVerificationSlots(opts = {}) {
14332
14812
  let reclaimed = 0;
14333
14813
  for (const name of readdirSync15(slotsDir)) {
14334
14814
  if (!name.endsWith(".json")) continue;
14335
- const filePath = path62.join(slotsDir, name);
14815
+ const filePath = path63.join(slotsDir, name);
14336
14816
  const before = existsSync44(filePath);
14337
14817
  reclaimStaleSlot(filePath, staleMs);
14338
14818
  if (before && !existsSync44(filePath)) reclaimed += 1;
@@ -14346,7 +14826,7 @@ function listActiveHeavyVerificationSlots(opts = {}) {
14346
14826
  const active = [];
14347
14827
  for (const name of readdirSync15(slotsDir)) {
14348
14828
  if (!name.endsWith(".json")) continue;
14349
- const record3 = readSlotRecord(path62.join(slotsDir, name));
14829
+ const record3 = readSlotRecord(path63.join(slotsDir, name));
14350
14830
  if (record3 && !slotIsStale(record3, staleMs)) active.push(record3);
14351
14831
  }
14352
14832
  return active;
@@ -14467,11 +14947,11 @@ function waitForHeavyVerificationSlot(command, timeoutMs, pollMs = 2e3, opts = {
14467
14947
 
14468
14948
  // src/harness-worktree-build-guard.ts
14469
14949
  init_paths();
14470
- import path63 from "node:path";
14950
+ import path64 from "node:path";
14471
14951
  function isPathUnderHarnessWorktree(cwd) {
14472
14952
  const worktreesDir = harnessWorktreesDir(resolveHarnessRoot());
14473
- const rel = path63.relative(worktreesDir, path63.resolve(cwd));
14474
- return rel.length > 0 && !rel.startsWith("..") && !path63.isAbsolute(rel);
14953
+ const rel = path64.relative(worktreesDir, path64.resolve(cwd));
14954
+ return rel.length > 0 && !rel.startsWith("..") && !path64.isAbsolute(rel);
14475
14955
  }
14476
14956
  function assessHarnessWorktreeBuildGuard(cwd) {
14477
14957
  if (!isPathUnderHarnessWorktree(cwd)) return { ok: true };
@@ -14684,7 +15164,7 @@ async function emitPlanProgress(args) {
14684
15164
  }
14685
15165
  function verifyPlanLocal(args) {
14686
15166
  const worktree = required(args.worktree ? String(args.worktree) : void 0, "worktree");
14687
- const cwd = path64.resolve(worktree);
15167
+ const cwd = path65.resolve(worktree);
14688
15168
  const summary = runHarnessVerifyCommands(cwd);
14689
15169
  const emitJson = args.json === true || args.json === "true";
14690
15170
  const payload = { passed: summary.passed, worktree: cwd, steps: summary.steps };
@@ -14733,10 +15213,10 @@ async function verifyPlan(args) {
14733
15213
  }
14734
15214
 
14735
15215
  // src/harness-verify-cli.ts
14736
- import path65 from "node:path";
15216
+ import path66 from "node:path";
14737
15217
  init_util();
14738
15218
  function runHarnessVerifyCli(args) {
14739
- const cwd = path65.resolve(required(args.worktree ? String(args.worktree) : void 0, "worktree"));
15219
+ const cwd = path66.resolve(required(args.worktree ? String(args.worktree) : void 0, "worktree"));
14740
15220
  const emitJson = args.json === true || args.json === "true" || args.emitJson === true || args.emitJson === "true";
14741
15221
  const commands = [];
14742
15222
  const rawCmd = args.command;
@@ -14782,7 +15262,7 @@ function runHarnessVerifyCli(args) {
14782
15262
 
14783
15263
  // src/plan-persist-cli.ts
14784
15264
  init_config();
14785
- import { readFileSync as readFileSync19 } from "node:fs";
15265
+ import { readFileSync as readFileSync20 } from "node:fs";
14786
15266
  init_util();
14787
15267
  var OPERATIONS = ["create", "add_version", "update_metadata"];
14788
15268
  var FAILURE_KINDS = [
@@ -14795,7 +15275,7 @@ var FAILURE_KINDS = [
14795
15275
  function readBodyArg(args) {
14796
15276
  const bodyFile = args.bodyFile ? String(args.bodyFile) : void 0;
14797
15277
  if (bodyFile) {
14798
- return { body: readFileSync19(bodyFile, "utf8"), bodyPathHint: bodyFile };
15278
+ return { body: readFileSync20(bodyFile, "utf8"), bodyPathHint: bodyFile };
14799
15279
  }
14800
15280
  const inline = args.body ? String(args.body) : void 0;
14801
15281
  if (inline) return { body: inline };
@@ -15175,7 +15655,7 @@ ${text.slice(0, 800)}`,
15175
15655
  }
15176
15656
 
15177
15657
  // src/monitor/monitor.service.ts
15178
- import path67 from "node:path";
15658
+ import path68 from "node:path";
15179
15659
  init_run_store();
15180
15660
  init_status();
15181
15661
  init_util();
@@ -15235,10 +15715,10 @@ function classifyWorkerHealth(input) {
15235
15715
  init_paths();
15236
15716
  init_util();
15237
15717
  import { existsSync as existsSync45, mkdirSync as mkdirSync10, readdirSync as readdirSync16, unlinkSync as unlinkSync5 } from "node:fs";
15238
- import path66 from "node:path";
15718
+ import path67 from "node:path";
15239
15719
  function monitorsDir() {
15240
15720
  const { harnessRoot } = getHarnessPaths();
15241
- const dir = path66.join(harnessRoot, "monitors");
15721
+ const dir = path67.join(harnessRoot, "monitors");
15242
15722
  mkdirSync10(dir, { recursive: true });
15243
15723
  return dir;
15244
15724
  }
@@ -15246,7 +15726,7 @@ function monitorIdFor(runId, workerName) {
15246
15726
  return workerName ? `${safeSlug(runId)}--${safeSlug(workerName)}` : safeSlug(runId);
15247
15727
  }
15248
15728
  function monitorPath(monitorId) {
15249
- return path66.join(monitorsDir(), `${monitorId}.json`);
15729
+ return path67.join(monitorsDir(), `${monitorId}.json`);
15250
15730
  }
15251
15731
  function loadMonitorSession(monitorId) {
15252
15732
  return readJson(monitorPath(monitorId), void 0);
@@ -15267,7 +15747,7 @@ function listMonitorSessions() {
15267
15747
  for (const name of readdirSync16(dir)) {
15268
15748
  if (!name.endsWith(".json")) continue;
15269
15749
  const session = readJson(
15270
- path66.join(dir, name),
15750
+ path67.join(dir, name),
15271
15751
  void 0
15272
15752
  );
15273
15753
  if (!session?.monitorId) continue;
@@ -15360,7 +15840,7 @@ async function fetchTaskLeasesForWorkers(input) {
15360
15840
  // src/monitor/monitor.service.ts
15361
15841
  function workerRecord2(runId, name) {
15362
15842
  return readJson(
15363
- path67.join(runDirectory(runId), "workers", safeSlug(name), "worker.json"),
15843
+ path68.join(runDirectory(runId), "workers", safeSlug(name), "worker.json"),
15364
15844
  void 0
15365
15845
  );
15366
15846
  }
@@ -15570,17 +16050,17 @@ init_util();
15570
16050
  init_paths();
15571
16051
  import { spawn as spawn7 } from "node:child_process";
15572
16052
  import { closeSync as closeSync8, existsSync as existsSync46, openSync as openSync8 } from "node:fs";
15573
- import path68 from "node:path";
16053
+ import path69 from "node:path";
15574
16054
  import { fileURLToPath as fileURLToPath4 } from "node:url";
15575
16055
  function resolveDefaultCliPath2() {
15576
- return path68.join(fileURLToPath4(new URL(".", import.meta.url)), "cli.js");
16056
+ return path69.join(fileURLToPath4(new URL(".", import.meta.url)), "cli.js");
15577
16057
  }
15578
16058
  function spawnMonitorSidecar(opts) {
15579
16059
  const cliPath = opts.cliPath ?? resolveDefaultCliPath2();
15580
16060
  if (!existsSync46(cliPath)) return void 0;
15581
16061
  const monitorId = monitorIdFor(opts.runId, opts.workerName);
15582
16062
  const { harnessRoot } = getHarnessPaths();
15583
- const logPath = path68.join(harnessRoot, "monitors", `${monitorId}.log`);
16063
+ const logPath = path69.join(harnessRoot, "monitors", `${monitorId}.log`);
15584
16064
  let logFd;
15585
16065
  try {
15586
16066
  logFd = openSync8(logPath, "a");
@@ -15705,7 +16185,7 @@ init_run_store();
15705
16185
  init_status();
15706
16186
  init_util();
15707
16187
  init_config();
15708
- import path69 from "node:path";
16188
+ import path70 from "node:path";
15709
16189
  function skip(runId, worker, taskId, agentOsId, leaseOwner, reason) {
15710
16190
  return { runId, worker, taskId, agentOsId, leaseOwner, action: "skipped", reason };
15711
16191
  }
@@ -15718,7 +16198,7 @@ async function postRestartUnblock(args) {
15718
16198
  const errors = [];
15719
16199
  for (const run of listRunRecords()) {
15720
16200
  for (const name of Object.keys(run.workers ?? {})) {
15721
- const workerPath = path69.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json");
16201
+ const workerPath = path70.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json");
15722
16202
  const worker = readJson(workerPath, void 0);
15723
16203
  if (!worker) {
15724
16204
  skipped.push(skip(run.id, name, "", "", "", "worker.json missing"));
@@ -15832,9 +16312,9 @@ async function postRestartUnblockCli(args) {
15832
16312
  // src/default-repo-cli.ts
15833
16313
  init_path_values();
15834
16314
  init_config();
15835
- import path70 from "node:path";
15836
- import { homedir as homedir16 } from "node:os";
15837
- var CONFIG_FILE2 = path70.join(homedir16(), ".kynver", "config.json");
16315
+ import path71 from "node:path";
16316
+ import { homedir as homedir17 } from "node:os";
16317
+ var CONFIG_FILE2 = path71.join(homedir17(), ".kynver", "config.json");
15838
16318
  function ensureDefaultRepo(opts) {
15839
16319
  const existing = loadUserConfig();
15840
16320
  const resolved = resolveDefaultRepo({ ...opts, config: existing });
@@ -15915,14 +16395,14 @@ function summarizeResolvedDefaultRepo(resolved) {
15915
16395
  }
15916
16396
 
15917
16397
  // src/doctor/runtime-takeover.ts
15918
- import path72 from "node:path";
16398
+ import path73 from "node:path";
15919
16399
  init_path_values();
15920
16400
 
15921
16401
  // src/doctor/runtime-takeover.probes.ts
15922
16402
  init_config();
15923
- import { accessSync, constants, existsSync as existsSync47, readFileSync as readFileSync20 } from "node:fs";
15924
- import { homedir as homedir17 } from "node:os";
15925
- import path71 from "node:path";
16403
+ import { accessSync, constants, existsSync as existsSync47, readFileSync as readFileSync21 } from "node:fs";
16404
+ import { homedir as homedir18 } from "node:os";
16405
+ import path72 from "node:path";
15926
16406
  import { spawnSync as spawnSync13 } from "node:child_process";
15927
16407
  init_paths();
15928
16408
  function captureCommand(bin, args) {
@@ -15965,15 +16445,15 @@ var defaultRuntimeTakeoverProbes = {
15965
16445
  commandOnPath: (bin) => captureCommand(process.platform === "win32" ? "where" : "which", [bin]),
15966
16446
  kynverVersion: (bin) => captureCommand(bin, ["--version"]),
15967
16447
  loadConfig: () => loadUserConfig(),
15968
- configFilePath: () => path71.join(homedir17(), ".kynver", "config.json"),
15969
- credentialsFilePath: () => path71.join(homedir17(), ".kynver", "credentials"),
16448
+ configFilePath: () => path72.join(homedir18(), ".kynver", "config.json"),
16449
+ credentialsFilePath: () => path72.join(homedir18(), ".kynver", "credentials"),
15970
16450
  readCredentials: () => {
15971
- const credPath = path71.join(homedir17(), ".kynver", "credentials");
16451
+ const credPath = path72.join(homedir18(), ".kynver", "credentials");
15972
16452
  if (!existsSync47(credPath)) {
15973
16453
  return { hasApiKey: false };
15974
16454
  }
15975
16455
  try {
15976
- const parsed = JSON.parse(readFileSync20(credPath, "utf8"));
16456
+ const parsed = JSON.parse(readFileSync21(credPath, "utf8"));
15977
16457
  return {
15978
16458
  hasApiKey: Boolean(parsed.apiKey?.trim()),
15979
16459
  runnerTokenPrefix: tokenPrefix(parsed.runnerToken),
@@ -16003,7 +16483,7 @@ var defaultRuntimeTakeoverProbes = {
16003
16483
  })()
16004
16484
  }),
16005
16485
  harnessRoot: () => resolveHarnessRoot(),
16006
- legacyOpenclawHarnessRoot: () => path71.join(homedir17(), ".openclaw", "harness"),
16486
+ legacyOpenclawHarnessRoot: () => path72.join(homedir18(), ".openclaw", "harness"),
16007
16487
  pathExists: (target) => existsSync47(target),
16008
16488
  pathWritable: (target) => isWritable(target)
16009
16489
  };
@@ -16410,8 +16890,8 @@ function assessVercelDeployEvidence(probes) {
16410
16890
  }
16411
16891
  function assessHarnessDirs(probes) {
16412
16892
  const harnessRoot = probes.harnessRoot();
16413
- const runsDir = path72.join(harnessRoot, "runs");
16414
- const worktreesDir = path72.join(harnessRoot, "worktrees");
16893
+ const runsDir = path73.join(harnessRoot, "runs");
16894
+ const worktreesDir = path73.join(harnessRoot, "worktrees");
16415
16895
  const displayHarnessRoot = redactHomePath(harnessRoot);
16416
16896
  const displayRunsDir = redactHomePath(runsDir);
16417
16897
  const displayWorktreesDir = redactHomePath(worktreesDir);
@@ -16680,9 +17160,9 @@ function applySchedulerCutoverAttestation(config) {
16680
17160
 
16681
17161
  // src/scheduler-cutover-cli.ts
16682
17162
  init_config();
16683
- import path73 from "node:path";
16684
- import { homedir as homedir18 } from "node:os";
16685
- var CONFIG_FILE3 = path73.join(homedir18(), ".kynver", "config.json");
17163
+ import path74 from "node:path";
17164
+ import { homedir as homedir19 } from "node:os";
17165
+ var CONFIG_FILE3 = path74.join(homedir19(), ".kynver", "config.json");
16686
17166
  function runSchedulerCutoverCheckCli(json = false) {
16687
17167
  const config = loadUserConfig();
16688
17168
  const report = assessSchedulerCutover(config);
@@ -16819,9 +17299,674 @@ async function runCronTickCli(args) {
16819
17299
  );
16820
17300
  }
16821
17301
 
17302
+ // src/cron/cron-install-cli.ts
17303
+ init_config();
17304
+
17305
+ // src/cron/cron-install.ts
17306
+ init_config();
17307
+ init_path_values();
17308
+ init_util();
17309
+ import { existsSync as existsSync51 } from "node:fs";
17310
+
17311
+ // src/cron/cron-id.ts
17312
+ import { createHash as createHash5 } from "node:crypto";
17313
+ function deterministicCronProviderId(spec) {
17314
+ const seed = JSON.stringify({
17315
+ cb: spec.callbackPath,
17316
+ cron: spec.cron ?? null,
17317
+ runAt: spec.runAt ?? null,
17318
+ kind: spec.kind,
17319
+ target: spec.target,
17320
+ dedupeKey: spec.dedupeKey ?? null
17321
+ });
17322
+ const sha = createHash5("sha1").update(seed).digest("hex").slice(0, 16);
17323
+ return `kc-cron:${sha}`;
17324
+ }
17325
+
17326
+ // src/cron/cron-install-plan.ts
17327
+ import { homedir as homedir21 } from "node:os";
17328
+ import path76 from "node:path";
17329
+
17330
+ // src/cron/cron-env-file.ts
17331
+ import { existsSync as existsSync48, mkdirSync as mkdirSync11, readFileSync as readFileSync22, writeFileSync as writeFileSync7 } from "node:fs";
17332
+ import { homedir as homedir20 } from "node:os";
17333
+ import path75 from "node:path";
17334
+ var DEFAULT_KYNVER_ENV_FILE = path75.join(homedir20(), ".kynver", ".env");
17335
+ function parseEnvFile(content) {
17336
+ const map = /* @__PURE__ */ new Map();
17337
+ for (const line of content.split(/\r?\n/)) {
17338
+ const trimmed = line.trim();
17339
+ if (!trimmed || trimmed.startsWith("#")) continue;
17340
+ const eq = trimmed.indexOf("=");
17341
+ if (eq <= 0) continue;
17342
+ const key = trimmed.slice(0, eq).trim();
17343
+ let value = trimmed.slice(eq + 1).trim();
17344
+ if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
17345
+ value = value.slice(1, -1);
17346
+ }
17347
+ map.set(key, value);
17348
+ }
17349
+ return map;
17350
+ }
17351
+ function serializeEnvFile(values, header = "# Managed by kynver cron install \u2014 safe to edit; re-run install to merge keys.\n") {
17352
+ const lines = [header.trimEnd(), ""];
17353
+ for (const [key, value] of [...values.entries()].sort(([a], [b]) => a.localeCompare(b))) {
17354
+ const escaped = value.includes(" ") || value.includes("#") || value.includes('"') ? `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"` : value;
17355
+ lines.push(`${key}=${escaped}`);
17356
+ }
17357
+ lines.push("");
17358
+ return lines.join("\n");
17359
+ }
17360
+ function readEnvFile(filePath = DEFAULT_KYNVER_ENV_FILE) {
17361
+ if (!existsSync48(filePath)) return /* @__PURE__ */ new Map();
17362
+ return parseEnvFile(readFileSync22(filePath, "utf8"));
17363
+ }
17364
+ function mergeEnvFile(updates, options = {}) {
17365
+ const filePath = options.filePath ?? DEFAULT_KYNVER_ENV_FILE;
17366
+ const existing = existsSync48(filePath) ? readFileSync22(filePath, "utf8") : "";
17367
+ const map = parseEnvFile(existing);
17368
+ const keysWritten = [];
17369
+ const keysRemoved = [];
17370
+ for (const key of options.removeKeys ?? []) {
17371
+ if (map.delete(key)) keysRemoved.push(key);
17372
+ }
17373
+ for (const [key, value] of Object.entries(updates)) {
17374
+ if (value === void 0) continue;
17375
+ const prev = map.get(key);
17376
+ map.set(key, value);
17377
+ if (prev !== value) keysWritten.push(key);
17378
+ }
17379
+ const prevMap = parseEnvFile(existing);
17380
+ let changed = prevMap.size !== map.size;
17381
+ if (!changed) {
17382
+ for (const [key, value] of map) {
17383
+ if (prevMap.get(key) !== value) {
17384
+ changed = true;
17385
+ break;
17386
+ }
17387
+ }
17388
+ }
17389
+ const nextContent = serializeEnvFile(map);
17390
+ if (changed) {
17391
+ mkdirSync11(path75.dirname(filePath), { recursive: true });
17392
+ writeFileSync7(filePath, nextContent, { mode: 384 });
17393
+ }
17394
+ return { path: filePath, changed, keysWritten, keysRemoved };
17395
+ }
17396
+
17397
+ // src/cron/cron-install-plan.ts
17398
+ var WATCHDOG_DEDUPE_KEY = "watchdog:board-sweep";
17399
+ var DEFAULT_WATCHDOG_CRON = "*/5 * * * *";
17400
+ var VERCEL_KYNVER_CRON_CUTOVER_STEPS = [
17401
+ "Set KYNVER_SCHEDULER_PROVIDER=kynver-cron on the hosted Kynver deployment (Vercel).",
17402
+ "Set KYNVER_CRON_SECRET to the same value written to ~/.kynver/.env by this installer.",
17403
+ "Unset KYNVER_CRON_STORE_PATH on Vercel \u2014 the connected box owns the local store.",
17404
+ "Keep QSTASH_TOKEN only if other non-watchdog schedules still use QStash (analyst/market jobs)."
17405
+ ];
17406
+ function buildCronInstallPlan(input) {
17407
+ const storePath = input.storePath?.trim() || defaultKynverCronStorePath();
17408
+ const envFilePath = input.envFilePath?.trim() || DEFAULT_KYNVER_ENV_FILE;
17409
+ const configPath = path76.join(homedir21(), ".kynver", "config.json");
17410
+ const callbackPath = `/api/agent-os/by-id/${input.agentOsId}/scheduler/fire`;
17411
+ const prerequisites = [];
17412
+ if (!input.apiBaseUrl?.trim()) prerequisites.push("apiBaseUrl \u2014 run `kynver setup --api-base-url \u2026`");
17413
+ if (!input.agentOsId?.trim()) prerequisites.push("agentOsId \u2014 run `kynver setup --agent-os-id \u2026`");
17414
+ const envUpdates = {
17415
+ KYNVER_API_URL: input.apiBaseUrl,
17416
+ KYNVER_CRON_SECRET: input.cronSecret,
17417
+ KYNVER_CRON_STORE_PATH: storePath,
17418
+ KYNVER_CRON_TICK_ENABLED: "1"
17419
+ };
17420
+ const envRemovals = ["OPENCLAW_CRON_STORE_PATH", "OPENCLAW_CRON_SECRET", "OPENCLAW_CRON_FIRE_BASE_URL"];
17421
+ const configUpdates = {
17422
+ deploymentSchedulerProvider: "kynver-cron",
17423
+ apiBaseUrl: input.apiBaseUrl,
17424
+ agentOsId: input.agentOsId,
17425
+ ...input.defaultDaemonRunId ? { defaultDaemonRunId: input.defaultDaemonRunId } : {}
17426
+ };
17427
+ return {
17428
+ envFilePath,
17429
+ configPath,
17430
+ storePath,
17431
+ envUpdates,
17432
+ envRemovals,
17433
+ configUpdates,
17434
+ deploymentSteps: VERCEL_KYNVER_CRON_CUTOVER_STEPS,
17435
+ prerequisites,
17436
+ systemdSupported: process.platform === "linux",
17437
+ watchdogSpec: {
17438
+ kind: "watchdog",
17439
+ cron: DEFAULT_WATCHDOG_CRON,
17440
+ dedupeKey: WATCHDOG_DEDUPE_KEY,
17441
+ callbackPath
17442
+ }
17443
+ };
17444
+ }
17445
+
17446
+ // src/cron/cron-install-api.ts
17447
+ async function apiFetch(url, apiKey, init = {}) {
17448
+ return fetch(url, {
17449
+ ...init,
17450
+ headers: {
17451
+ "Content-Type": "application/json",
17452
+ Authorization: `Bearer ${apiKey}`,
17453
+ ...init.headers
17454
+ }
17455
+ });
17456
+ }
17457
+ async function listSchedulerJobs(baseUrl, agentOsId, apiKey, query = {}) {
17458
+ const sp = new URLSearchParams(query);
17459
+ const url = `${baseUrl}/api/agent-os/by-id/${encodeURIComponent(agentOsId)}/scheduler/jobs?${sp}`;
17460
+ const res = await apiFetch(url, apiKey);
17461
+ const text = await res.text();
17462
+ let parsed = null;
17463
+ try {
17464
+ parsed = JSON.parse(text);
17465
+ } catch {
17466
+ parsed = null;
17467
+ }
17468
+ if (!res.ok) {
17469
+ throw new Error(
17470
+ `list scheduler jobs failed (${res.status}): ${parsed?.error ?? text.slice(0, 200)}`
17471
+ );
17472
+ }
17473
+ return parsed?.items ?? [];
17474
+ }
17475
+ async function ensureWatchdogScheduleRemote(baseUrl, agentOsId, apiKey, cron, options = {}) {
17476
+ const url = `${baseUrl}/api/agent-os/by-id/${encodeURIComponent(agentOsId)}/scheduler/watchdog/ensure`;
17477
+ const body = {};
17478
+ if (cron) body.cron = cron;
17479
+ if (options.requireProvider) body.requireProvider = options.requireProvider;
17480
+ const res = await apiFetch(url, apiKey, {
17481
+ method: "POST",
17482
+ body: JSON.stringify(body)
17483
+ });
17484
+ const text = await res.text();
17485
+ let parsed = null;
17486
+ try {
17487
+ parsed = JSON.parse(text);
17488
+ } catch {
17489
+ parsed = null;
17490
+ }
17491
+ if (!res.ok || !parsed?.job) {
17492
+ throw new Error(
17493
+ `ensure watchdog failed (${res.status}): ${parsed?.error ?? text.slice(0, 200)}`
17494
+ );
17495
+ }
17496
+ return {
17497
+ job: parsed.job,
17498
+ route: parsed.route ?? `/api/agent-os/by-id/${agentOsId}/scheduler/fire`,
17499
+ dedupeKey: parsed.dedupeKey ?? WATCHDOG_DEDUPE_KEY,
17500
+ requestedCron: parsed.requestedCron ?? cron ?? "*/5 * * * *",
17501
+ selectedProvider: parsed.selectedProvider ?? parsed.job.provider ?? null
17502
+ };
17503
+ }
17504
+ async function cancelSchedulerJob(baseUrl, agentOsId, jobId, apiKey) {
17505
+ const url = `${baseUrl}/api/agent-os/by-id/${encodeURIComponent(agentOsId)}/scheduler/jobs/${encodeURIComponent(jobId)}/cancel`;
17506
+ const res = await apiFetch(url, apiKey, { method: "POST", body: "{}" });
17507
+ if (!res.ok) {
17508
+ const text = await res.text();
17509
+ throw new Error(`cancel job ${jobId} failed (${res.status}): ${text.slice(0, 200)}`);
17510
+ }
17511
+ }
17512
+ function buildWatchdogCronSpec(agentOsId, cron, dedupeKey = WATCHDOG_DEDUPE_KEY) {
17513
+ return {
17514
+ kind: "watchdog",
17515
+ scheduleKind: "cron",
17516
+ cron,
17517
+ callbackPath: `/api/agent-os/by-id/${agentOsId}/scheduler/fire`,
17518
+ payload: { source: "agent-os.watchdog-schedule", agentOsId },
17519
+ target: { agentOsId },
17520
+ description: "Watchdog board sweep (repair loop)",
17521
+ dedupeKey
17522
+ };
17523
+ }
17524
+ function findQstashWatchdogLeftovers(jobs) {
17525
+ return jobs.filter(
17526
+ (job) => job.kind === "watchdog" && job.dedupeKey === WATCHDOG_DEDUPE_KEY && job.provider === "qstash" && job.status !== "cancelled"
17527
+ );
17528
+ }
17529
+ function findKynverCronWatchdog(jobs) {
17530
+ return jobs.find(
17531
+ (job) => job.kind === "watchdog" && job.dedupeKey === WATCHDOG_DEDUPE_KEY && job.provider === "kynver-cron" && job.status !== "cancelled"
17532
+ );
17533
+ }
17534
+ function canCancelQstashWatchdogLeftovers(jobs) {
17535
+ if (findKynverCronWatchdog(jobs)) return { allowed: true };
17536
+ return {
17537
+ allowed: false,
17538
+ reason: "Cannot cancel QStash watchdog until hosted KYNVER_SCHEDULER_PROVIDER=kynver-cron is live and a kynver-cron watchdog row exists. Set Vercel env, redeploy, re-run `kynver cron install`, then `--confirm-qstash-removal`."
17539
+ };
17540
+ }
17541
+
17542
+ // src/cron/cron-install-secrets.ts
17543
+ import { randomBytes as randomBytes2 } from "node:crypto";
17544
+ function generateCronSecret() {
17545
+ return randomBytes2(32).toString("base64url");
17546
+ }
17547
+ function resolveCronSecretForInstall(envFilePath = DEFAULT_KYNVER_ENV_FILE) {
17548
+ const fromProcess = resolveKynverCronSecret();
17549
+ if (fromProcess) {
17550
+ return { secret: fromProcess, generated: false, source: "env" };
17551
+ }
17552
+ const file = readEnvFile(envFilePath);
17553
+ const fromFile = file.get("KYNVER_CRON_SECRET")?.trim();
17554
+ if (fromFile) {
17555
+ return { secret: fromFile, generated: false, source: "env-file" };
17556
+ }
17557
+ return { secret: generateCronSecret(), generated: true, source: "generated" };
17558
+ }
17559
+
17560
+ // src/cron/cron-install-systemd.ts
17561
+ import { existsSync as existsSync49, mkdirSync as mkdirSync12, writeFileSync as writeFileSync8 } from "node:fs";
17562
+ import { homedir as homedir22 } from "node:os";
17563
+ import path77 from "node:path";
17564
+ import { spawnSync as spawnSync14 } from "node:child_process";
17565
+ var KYNVER_CRON_DAEMON_UNIT = "kynver-cron-daemon.service";
17566
+ function defaultSystemdUserUnitDir() {
17567
+ return path77.join(homedir22(), ".config", "systemd", "user");
17568
+ }
17569
+ function renderKynverCronDaemonService(input) {
17570
+ const kynverBin = input.kynverBin?.trim() || "kynver";
17571
+ return [
17572
+ "[Unit]",
17573
+ "Description=Kynver AgentOS daemon (pipeline + cron tick)",
17574
+ "After=network-online.target",
17575
+ "",
17576
+ "[Service]",
17577
+ "Type=simple",
17578
+ `EnvironmentFile=${input.envFilePath}`,
17579
+ `ExecStart=${kynverBin} daemon --run ${input.runId} --agent-os-id ${input.agentOsId} --execute`,
17580
+ "Restart=on-failure",
17581
+ "RestartSec=10",
17582
+ "",
17583
+ "[Install]",
17584
+ "WantedBy=default.target",
17585
+ ""
17586
+ ].join("\n");
17587
+ }
17588
+ function installSystemdUserDaemon(input, execute) {
17589
+ if (!isSystemdRunAvailable()) {
17590
+ return {
17591
+ supported: false,
17592
+ unitPath: null,
17593
+ written: false,
17594
+ enabled: false,
17595
+ started: false,
17596
+ note: process.platform === "linux" ? "systemd-run not available \u2014 install user service manually or run `kynver daemon` under your supervisor." : "systemd user units are only supported on Linux; use `kynver daemon` manually on macOS/Windows."
17597
+ };
17598
+ }
17599
+ const unitDir = defaultSystemdUserUnitDir();
17600
+ const unitPath = path77.join(unitDir, KYNVER_CRON_DAEMON_UNIT);
17601
+ const content = renderKynverCronDaemonService(input);
17602
+ if (!execute) {
17603
+ return {
17604
+ supported: true,
17605
+ unitPath,
17606
+ written: false,
17607
+ enabled: false,
17608
+ started: false,
17609
+ note: "Dry-run \u2014 pass --execute to write and enable the user unit."
17610
+ };
17611
+ }
17612
+ mkdirSync12(unitDir, { recursive: true });
17613
+ const existed = existsSync49(unitPath);
17614
+ writeFileSync8(unitPath, content, "utf8");
17615
+ const reload = spawnSync14("systemctl", ["--user", "daemon-reload"], { encoding: "utf8" });
17616
+ if (reload.status !== 0) {
17617
+ return {
17618
+ supported: true,
17619
+ unitPath,
17620
+ written: true,
17621
+ enabled: false,
17622
+ started: false,
17623
+ note: `Wrote ${unitPath} but systemctl --user daemon-reload failed: ${reload.stderr || reload.stdout}`
17624
+ };
17625
+ }
17626
+ const enable = spawnSync14("systemctl", ["--user", "enable", "--now", KYNVER_CRON_DAEMON_UNIT], {
17627
+ encoding: "utf8"
17628
+ });
17629
+ return {
17630
+ supported: true,
17631
+ unitPath,
17632
+ written: !existed || true,
17633
+ enabled: enable.status === 0,
17634
+ started: enable.status === 0,
17635
+ note: enable.status === 0 ? `Enabled and started ${KYNVER_CRON_DAEMON_UNIT}` : `Wrote ${unitPath} but enable failed: ${enable.stderr || enable.stdout}`
17636
+ };
17637
+ }
17638
+
17639
+ // src/cron/cron-install-verify.ts
17640
+ import { existsSync as existsSync50 } from "node:fs";
17641
+ async function verifyCronInstall(input) {
17642
+ const envFilePath = input.envFilePath ?? DEFAULT_KYNVER_ENV_FILE;
17643
+ const checks = [];
17644
+ const env = resolveKynverCronEnv();
17645
+ const status = await buildKynverCronStatusReport(env);
17646
+ const fileEnv = readEnvFile(envFilePath);
17647
+ checks.push({
17648
+ id: "config_agent_os",
17649
+ ok: Boolean(input.config.agentOsId?.trim()),
17650
+ summary: input.config.agentOsId ? `agentOsId configured (${input.config.agentOsId})` : "agentOsId missing in ~/.kynver/config.json",
17651
+ remediation: "Run `kynver setup --agent-os-id <id>`."
17652
+ });
17653
+ checks.push({
17654
+ id: "config_api_base",
17655
+ ok: Boolean(input.config.apiBaseUrl?.trim() || env.fireBaseUrl),
17656
+ summary: env.fireBaseUrl ? `fire base URL resolved (${env.fireBaseUrl})` : "KYNVER_API_URL / apiBaseUrl not configured",
17657
+ remediation: "Run `kynver setup --api-base-url https://\u2026`."
17658
+ });
17659
+ checks.push({
17660
+ id: "deployment_provider",
17661
+ ok: input.config.deploymentSchedulerProvider === "kynver-cron",
17662
+ summary: `deploymentSchedulerProvider=${input.config.deploymentSchedulerProvider ?? "(unset)"}`,
17663
+ remediation: 'Run `kynver cron install` or set deploymentSchedulerProvider to "kynver-cron".'
17664
+ });
17665
+ checks.push({
17666
+ id: "cron_secret",
17667
+ ok: Boolean(env.secret),
17668
+ summary: env.secret ? "KYNVER_CRON_SECRET present" : "KYNVER_CRON_SECRET missing",
17669
+ remediation: "Run `kynver cron install` to generate and persist the shared secret."
17670
+ });
17671
+ checks.push({
17672
+ id: "env_file",
17673
+ ok: existsSync50(envFilePath) && Boolean(fileEnv.get("KYNVER_CRON_SECRET")),
17674
+ summary: existsSync50(envFilePath) ? `~/.kynver/.env present (${fileEnv.size} keys)` : "~/.kynver/.env missing",
17675
+ remediation: "Run `kynver cron install` to write ~/.kynver/.env."
17676
+ });
17677
+ checks.push({
17678
+ id: "cron_store",
17679
+ ok: existsSync50(env.storePath),
17680
+ summary: existsSync50(env.storePath) ? `cron store present (${env.storePath})` : `cron store missing (${env.storePath})`,
17681
+ remediation: "Run `kynver cron install` to initialize the local store."
17682
+ });
17683
+ const jobs = await loadCronJobs(env.storePath).catch(() => []);
17684
+ const watchdog = jobs.find((j) => j.spec.dedupeKey === WATCHDOG_DEDUPE_KEY);
17685
+ checks.push({
17686
+ id: "watchdog_local",
17687
+ ok: Boolean(watchdog),
17688
+ summary: watchdog ? `watchdog entry in local store (${watchdog.providerScheduleId})` : "watchdog entry not mirrored in local cron store",
17689
+ remediation: "Run `kynver cron install` (with API access) to register and mirror watchdog."
17690
+ });
17691
+ checks.push({
17692
+ id: "daemon_primary",
17693
+ ok: status.daemonPrimary,
17694
+ summary: status.daemonPrimary ? "cron tick enabled with credentials (daemon-primary)" : `cron primary=${status.primary}; tickEnabled=${status.env.tickEnabled}`,
17695
+ remediation: "Ensure KYNVER_CRON_TICK_ENABLED=1 and run `kynver daemon` (or enable the systemd user unit)."
17696
+ });
17697
+ const legacyOpenclaw = fileEnv.has("OPENCLAW_CRON_STORE_PATH") || fileEnv.has("OPENCLAW_CRON_SECRET") || fileEnv.has("OPENCLAW_CRON_FIRE_BASE_URL");
17698
+ checks.push({
17699
+ id: "legacy_openclaw_env",
17700
+ ok: !legacyOpenclaw,
17701
+ summary: legacyOpenclaw ? "legacy OPENCLAW_CRON_* keys still present in ~/.kynver/.env" : "no legacy OPENCLAW_CRON_* keys in env file",
17702
+ remediation: "Re-run `kynver cron install` to retire legacy aliases."
17703
+ });
17704
+ const ok = checks.every((c) => c.ok);
17705
+ return { ok, checks };
17706
+ }
17707
+
17708
+ // src/cron/cron-install.ts
17709
+ init_run_store();
17710
+ function resolveDaemonRunId(config, explicit) {
17711
+ if (explicit?.trim()) return explicit.trim();
17712
+ if (config.defaultDaemonRunId?.trim()) return config.defaultDaemonRunId.trim();
17713
+ const runsDir = getPaths().runsDir;
17714
+ if (!existsSync51(runsDir)) return null;
17715
+ const runs = listRunRecords().sort(
17716
+ (a, b) => Date.parse(b.createdAt) - Date.parse(a.createdAt)
17717
+ );
17718
+ return runs[0]?.id ?? null;
17719
+ }
17720
+ async function runCronInstall(opts = {}) {
17721
+ const execute = opts.execute !== false;
17722
+ const existing = loadUserConfig();
17723
+ const apiBaseUrl = trimTrailingSlash(
17724
+ opts.apiBaseUrl?.trim() || existing.apiBaseUrl?.trim() || process.env.KYNVER_API_URL?.trim() || ""
17725
+ );
17726
+ const agentOsId = opts.agentOsId?.trim() || existing.agentOsId?.trim() || process.env.KYNVER_AGENT_OS_ID?.trim() || "";
17727
+ const secretResolution = resolveCronSecretForInstall();
17728
+ const plan = buildCronInstallPlan({
17729
+ config: existing,
17730
+ apiBaseUrl,
17731
+ agentOsId,
17732
+ cronSecret: secretResolution.secret,
17733
+ defaultDaemonRunId: opts.runId?.trim() || existing.defaultDaemonRunId,
17734
+ installSystemd: opts.installSystemd === true
17735
+ });
17736
+ const blockers = [...plan.prerequisites];
17737
+ const result = {
17738
+ ok: false,
17739
+ dryRun: !execute,
17740
+ plan,
17741
+ secretGenerated: secretResolution.generated,
17742
+ configPath: displayUserPath(plan.configPath),
17743
+ storeInitialized: false,
17744
+ blockers
17745
+ };
17746
+ if (blockers.length) return result;
17747
+ if (execute) {
17748
+ const envMerge = mergeEnvFile(plan.envUpdates, {
17749
+ filePath: plan.envFilePath,
17750
+ removeKeys: plan.envRemovals
17751
+ });
17752
+ result.envFile = {
17753
+ path: displayUserPath(envMerge.path),
17754
+ changed: envMerge.changed,
17755
+ keysWritten: envMerge.keysWritten
17756
+ };
17757
+ const nextConfig = { ...existing, ...plan.configUpdates };
17758
+ const runId = resolveDaemonRunId(nextConfig, opts.runId);
17759
+ if (runId) nextConfig.defaultDaemonRunId = runId;
17760
+ saveUserConfig(nextConfig);
17761
+ process.env.KYNVER_API_URL = apiBaseUrl;
17762
+ process.env.KYNVER_CRON_SECRET = secretResolution.secret;
17763
+ process.env.KYNVER_CRON_STORE_PATH = plan.storePath;
17764
+ process.env.KYNVER_CRON_TICK_ENABLED = "1";
17765
+ const storeInit = await ensureCronStoreInitialized(plan.storePath);
17766
+ result.storeInitialized = storeInit.created || existsSync51(plan.storePath);
17767
+ const apiKey = loadApiKey();
17768
+ if (apiKey) {
17769
+ try {
17770
+ const token = await fetchRunnerCredential(agentOsId, { baseUrl: apiBaseUrl, apiKey });
17771
+ saveRunnerToken(agentOsId, token);
17772
+ } catch {
17773
+ }
17774
+ }
17775
+ if (!opts.skipWatchdog) {
17776
+ if (!apiKey) {
17777
+ result.watchdog = { error: "KYNVER_API_KEY required to register watchdog on server" };
17778
+ blockers.push("login \u2014 run `kynver login --api-key \u2026` to register watchdog remotely");
17779
+ } else {
17780
+ try {
17781
+ const remote = await ensureWatchdogScheduleRemote(apiBaseUrl, agentOsId, apiKey, void 0, {
17782
+ requireProvider: "kynver-cron"
17783
+ });
17784
+ const spec = buildWatchdogCronSpec(agentOsId, remote.requestedCron, remote.dedupeKey);
17785
+ const entry = {
17786
+ providerScheduleId: deterministicCronProviderId(spec),
17787
+ spec,
17788
+ registeredAt: (/* @__PURE__ */ new Date()).toISOString(),
17789
+ paused: false
17790
+ };
17791
+ await saveCronJob(entry, plan.storePath);
17792
+ result.watchdog = {
17793
+ remoteJobId: remote.job.id,
17794
+ provider: remote.selectedProvider ?? remote.job.provider,
17795
+ localProviderScheduleId: entry.providerScheduleId
17796
+ };
17797
+ const allJobs = await listSchedulerJobs(apiBaseUrl, agentOsId, apiKey);
17798
+ const leftovers = findQstashWatchdogLeftovers(allJobs);
17799
+ const manualSteps = [
17800
+ "Phase 1 (local): `kynver cron install` writes box env/store and may fail remote watchdog until Vercel cutover.",
17801
+ "Phase 2 (hosted): set KYNVER_SCHEDULER_PROVIDER=kynver-cron + KYNVER_CRON_SECRET on Vercel, redeploy, re-run install.",
17802
+ "Phase 3 (cleanup): `kynver cron install --confirm-qstash-removal` after a kynver-cron watchdog row exists.",
17803
+ "Analyst/market QStash schedules are never touched by this installer."
17804
+ ];
17805
+ const removed = [];
17806
+ if (leftovers.length) {
17807
+ if (opts.confirmQstashRemoval) {
17808
+ const cancelGuard = canCancelQstashWatchdogLeftovers(allJobs);
17809
+ if (!cancelGuard.allowed) {
17810
+ blockers.push(cancelGuard.reason);
17811
+ } else {
17812
+ for (const job of leftovers) {
17813
+ await cancelSchedulerJob(apiBaseUrl, agentOsId, job.id, apiKey);
17814
+ removed.push(job.id);
17815
+ }
17816
+ }
17817
+ } else {
17818
+ blockers.push(
17819
+ `${leftovers.length} QStash watchdog job(s) still active \u2014 complete Vercel cutover (KYNVER_SCHEDULER_PROVIDER=kynver-cron + KYNVER_CRON_SECRET), re-run install, then --confirm-qstash-removal`
17820
+ );
17821
+ }
17822
+ }
17823
+ result.qstashWatchdog = {
17824
+ found: leftovers.map((j) => ({ id: j.id, provider: j.provider, status: j.status })),
17825
+ removed,
17826
+ manualSteps
17827
+ };
17828
+ } catch (err) {
17829
+ result.watchdog = { error: err.message };
17830
+ blockers.push(`watchdog registration failed: ${err.message}`);
17831
+ }
17832
+ }
17833
+ }
17834
+ if (opts.installSystemd) {
17835
+ const runId2 = resolveDaemonRunId({ ...existing, ...plan.configUpdates }, opts.runId);
17836
+ if (!runId2) {
17837
+ result.systemd = {
17838
+ supported: plan.systemdSupported,
17839
+ unitPath: null,
17840
+ written: false,
17841
+ enabled: false,
17842
+ started: false,
17843
+ note: "No harness run found \u2014 run `kynver run create` first or pass --run <runId>."
17844
+ };
17845
+ blockers.push("harness run \u2014 create a run before installing systemd unit");
17846
+ } else {
17847
+ try {
17848
+ loadRun(runId2);
17849
+ } catch {
17850
+ blockers.push(`harness run ${runId2} not found`);
17851
+ }
17852
+ result.systemd = installSystemdUserDaemon(
17853
+ { envFilePath: plan.envFilePath, agentOsId, runId: runId2 },
17854
+ execute
17855
+ );
17856
+ }
17857
+ }
17858
+ if (!opts.skipTestFire && blockers.length === 0) {
17859
+ const tick = await runKynverCronTick({ agentOsIdFilter: agentOsId });
17860
+ result.testFire = { fired: tick.fired, errors: tick.errors };
17861
+ if (tick.errors > 0) {
17862
+ blockers.push(`test fire reported ${tick.errors} error(s) \u2014 check server KYNVER_CRON_SECRET matches`);
17863
+ }
17864
+ } else if (opts.skipTestFire) {
17865
+ result.testFire = { fired: 0, errors: 0, skipped: true };
17866
+ }
17867
+ result.verify = await verifyCronInstall({
17868
+ config: loadUserConfig(),
17869
+ envFilePath: plan.envFilePath
17870
+ });
17871
+ result.blockers = blockers;
17872
+ result.ok = blockers.length === 0 && (result.verify?.ok ?? false);
17873
+ } else {
17874
+ result.ok = blockers.length === 0;
17875
+ }
17876
+ return result;
17877
+ }
17878
+
17879
+ // src/cron/cron-install-cli.ts
17880
+ function parseBoolArg(value, defaultValue) {
17881
+ if (value === void 0) return defaultValue;
17882
+ if (value === true) return true;
17883
+ if (value === false) return false;
17884
+ const v = String(value).trim().toLowerCase();
17885
+ if (v === "0" || v === "false" || v === "no" || v === "off") return false;
17886
+ if (v === "1" || v === "true" || v === "yes" || v === "on") return true;
17887
+ return defaultValue;
17888
+ }
17889
+ async function runCronInstallCli(args) {
17890
+ const dryRun = args.dryRun === true || args["dry-run"] === true;
17891
+ const result = await runCronInstall({
17892
+ execute: dryRun ? false : parseBoolArg(args.execute, true),
17893
+ json: args.json === true,
17894
+ installSystemd: args.installSystemd === true || args["install-systemd"] === true,
17895
+ confirmQstashRemoval: args.confirmQstashRemoval === true || args["confirm-qstash-removal"] === true,
17896
+ skipWatchdog: args.skipWatchdog === true || args["skip-watchdog"] === true,
17897
+ skipTestFire: args.skipTestFire === true || args["skip-test-fire"] === true,
17898
+ agentOsId: typeof args.agentOsId === "string" ? args.agentOsId : void 0,
17899
+ apiBaseUrl: typeof args.apiBaseUrl === "string" ? args.apiBaseUrl : void 0,
17900
+ runId: typeof args.run === "string" ? args.run : void 0
17901
+ });
17902
+ if (args.json === true) {
17903
+ console.log(JSON.stringify(result, null, 2));
17904
+ if (!result.ok) process.exitCode = 1;
17905
+ return;
17906
+ }
17907
+ console.log(result.dryRun ? "Kynver Cron install (dry-run)\n" : "Kynver Cron install\n");
17908
+ console.log(` config: ${result.configPath}`);
17909
+ console.log(` env file: ${result.plan.envFilePath}`);
17910
+ console.log(` store: ${result.plan.storePath}`);
17911
+ if (result.secretGenerated) console.log(" generated new KYNVER_CRON_SECRET");
17912
+ if (result.envFile) {
17913
+ console.log(
17914
+ ` env merge: ${result.envFile.changed ? "updated" : "unchanged"} (${result.envFile.keysWritten.join(", ") || "no key changes"})`
17915
+ );
17916
+ }
17917
+ if (result.watchdog?.remoteJobId) {
17918
+ console.log(
17919
+ ` watchdog: job ${result.watchdog.remoteJobId} provider=${result.watchdog.provider} local=${result.watchdog.localProviderScheduleId}`
17920
+ );
17921
+ } else if (result.watchdog?.error) {
17922
+ console.log(` watchdog: ERROR ${result.watchdog.error}`);
17923
+ }
17924
+ if (result.qstashWatchdog?.found.length) {
17925
+ console.log(` qstash watchdog leftovers: ${result.qstashWatchdog.found.length}`);
17926
+ for (const j of result.qstashWatchdog.found) {
17927
+ console.log(` - ${j.id} (${j.status})`);
17928
+ }
17929
+ if (result.qstashWatchdog.removed.length) {
17930
+ console.log(` removed: ${result.qstashWatchdog.removed.join(", ")}`);
17931
+ }
17932
+ }
17933
+ if (result.systemd) {
17934
+ console.log(` systemd: ${result.systemd.note ?? (result.systemd.started ? "running" : "not started")}`);
17935
+ }
17936
+ if (result.testFire && !result.testFire.skipped) {
17937
+ console.log(` test fire: fired=${result.testFire.fired} errors=${result.testFire.errors}`);
17938
+ }
17939
+ console.log("\nHosted deployment (manual):");
17940
+ for (const step of result.plan.deploymentSteps) console.log(` - ${step}`);
17941
+ if (result.blockers.length) {
17942
+ console.log("\nBlockers:");
17943
+ for (const b of result.blockers) console.log(` ! ${b}`);
17944
+ process.exitCode = 1;
17945
+ return;
17946
+ }
17947
+ console.log("\nInstall complete \u2014 run `kynver cron verify` to re-check.");
17948
+ }
17949
+ async function runCronVerifyCli(args) {
17950
+ const config = loadUserConfig();
17951
+ const report = await verifyCronInstall({ config });
17952
+ if (args.json === true) {
17953
+ console.log(JSON.stringify(report, null, 2));
17954
+ if (!report.ok) process.exitCode = 1;
17955
+ return;
17956
+ }
17957
+ console.log(`Kynver Cron verify: ${report.ok ? "PASS" : "FAIL"}
17958
+ `);
17959
+ for (const check3 of report.checks) {
17960
+ const mark = check3.ok ? "ok" : "FAIL";
17961
+ console.log(` [${mark}] ${check3.id}: ${check3.summary}`);
17962
+ if (!check3.ok && check3.remediation) console.log(` \u2192 ${check3.remediation}`);
17963
+ }
17964
+ if (!report.ok) process.exitCode = 1;
17965
+ }
17966
+
16822
17967
  // src/lane/landing-maintainer-tick.ts
16823
17968
  init_config();
16824
- import os11 from "node:os";
17969
+ import os13 from "node:os";
16825
17970
  init_config();
16826
17971
  init_box_identity();
16827
17972
  init_resource_gate();
@@ -16837,10 +17982,10 @@ var LANDING_MAINTAINER_LANE_SPEC = {
16837
17982
  };
16838
17983
 
16839
17984
  // src/lane/landing-maintainer-local.ts
16840
- import { spawnSync as spawnSync14 } from "node:child_process";
16841
- import path74 from "node:path";
17985
+ import { spawnSync as spawnSync15 } from "node:child_process";
17986
+ import path78 from "node:path";
16842
17987
  function runLandingWrapper(prNumber, repoRoot, execute) {
16843
- const script = path74.join(repoRoot, LANDING_MAINTAINER_LANE_SPEC.landScript);
17988
+ const script = path78.join(repoRoot, LANDING_MAINTAINER_LANE_SPEC.landScript);
16844
17989
  const args = [script, String(prNumber), ...LANDING_MAINTAINER_LANE_SPEC.landScriptArgs];
16845
17990
  if (!execute) {
16846
17991
  return {
@@ -16851,7 +17996,7 @@ function runLandingWrapper(prNumber, repoRoot, execute) {
16851
17996
  stderr: ""
16852
17997
  };
16853
17998
  }
16854
- const result = spawnSync14("node", args, {
17999
+ const result = spawnSync15("node", args, {
16855
18000
  cwd: repoRoot,
16856
18001
  encoding: "utf8",
16857
18002
  timeout: 10 * 60 * 1e3
@@ -16866,7 +18011,7 @@ function runLandingWrapper(prNumber, repoRoot, execute) {
16866
18011
  }
16867
18012
  function resolveLandingMaintainerRepoRoot(args) {
16868
18013
  const explicit = args.repoPath ? String(args.repoPath).trim() : "";
16869
- if (explicit) return path74.resolve(explicit);
18014
+ if (explicit) return path78.resolve(explicit);
16870
18015
  const resolved = resolveDefaultRepo();
16871
18016
  return resolved?.repo ?? process.cwd();
16872
18017
  }
@@ -16885,7 +18030,7 @@ async function runLandingMaintainerLaneTick(args) {
16885
18030
  ...buildBoxResourceSnapshotFromGate(resourceGate, {
16886
18031
  harnessRunId: runId,
16887
18032
  boxKind: resolveBoxKindFromConfig(loadUserConfig()),
16888
- hostLabel: os11.hostname()
18033
+ hostLabel: os13.hostname()
16889
18034
  }),
16890
18035
  providerHealthy: resourceGate.ok,
16891
18036
  authorizedForRepair: resourceGate.ok,
@@ -16991,6 +18136,7 @@ function usage(code = 0) {
16991
18136
  "Usage:",
16992
18137
  " kynver login [--api-key KEY] [--api-base-url URL] (omit --api-key to authorize in the browser)",
16993
18138
  " kynver bootstrap [--api-base-url URL] [--api-key KEY] [--repo PATH] (login + setup + runner credential in one shot)",
18139
+ " kynver start [--repo PATH] [--api-base-url URL] [--run RUN_ID] [--interval-ms MS] (bring your agent online: bootstrap if needed + run + daemon)",
16994
18140
  " kynver runner credential [--agent-os-id ID] [--base-url URL]",
16995
18141
  " 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]",
16996
18142
  " kynver daemon --run RUN_ID --agent-os-id AOS_ID [--execute] [--interval-ms MS] [--stall-ms MS] [--no-supervise]",
@@ -17031,6 +18177,8 @@ function usage(code = 0) {
17031
18177
  " kynver doctor runtime-takeover [--remediate-default-repo]",
17032
18178
  " kynver scheduler cutover-check [--json]",
17033
18179
  " kynver scheduler attest-cutover [--json]",
18180
+ " kynver cron install [--dry-run] [--json] [--install-systemd] [--confirm-qstash-removal] [--skip-watchdog] [--skip-test-fire] [--agent-os-id ID] [--api-base-url URL] [--run RUN_ID]",
18181
+ " kynver cron verify [--json]",
17034
18182
  " kynver cron status [--json]",
17035
18183
  " kynver cron tick [--agent-os-id AOS_ID] [--json]",
17036
18184
  " kynver lane tick landing-maintainer [--fleet] [--repo OWNER/NAME] [--agent-os-id AOS_ID] [--execute] [--json]",
@@ -17054,8 +18202,8 @@ async function main(argv = process.argv.slice(2)) {
17054
18202
  if (action && isHelpFlag(action) || rest.some(isHelpFlag)) return usage(0);
17055
18203
  const args = parseArgs(rest);
17056
18204
  const { runsDir, worktreesDir } = getPaths();
17057
- mkdirSync11(runsDir, { recursive: true });
17058
- mkdirSync11(worktreesDir, { recursive: true });
18205
+ mkdirSync13(runsDir, { recursive: true });
18206
+ mkdirSync13(worktreesDir, { recursive: true });
17059
18207
  if (scope === "daemon") {
17060
18208
  assertNativeDaemonAllowed();
17061
18209
  }
@@ -17076,6 +18224,7 @@ async function main(argv = process.argv.slice(2)) {
17076
18224
  }
17077
18225
  if (scope === "login") return void await runLogin(args);
17078
18226
  if (scope === "bootstrap") return void await runBootstrap(args);
18227
+ if (scope === "start") return void await runStart(args);
17079
18228
  if (scope === "status") return runStatus(args);
17080
18229
  if (scope === "runner" && action === "credential") return void await mintRunnerCredential(args);
17081
18230
  if (scope === "setup") return void await runSetup(args);
@@ -17106,6 +18255,12 @@ async function main(argv = process.argv.slice(2)) {
17106
18255
  if (scope === "scheduler" && action === "attest-cutover") {
17107
18256
  return runSchedulerAttestCutoverCli(args.json === true);
17108
18257
  }
18258
+ if (scope === "cron" && action === "install") {
18259
+ return void await runCronInstallCli(args);
18260
+ }
18261
+ if (scope === "cron" && action === "verify") {
18262
+ return void await runCronVerifyCli(args);
18263
+ }
17109
18264
  if (scope === "cron" && action === "status") {
17110
18265
  return void await runCronStatusCli(args.json === true);
17111
18266
  }
@@ -17119,7 +18274,7 @@ async function main(argv = process.argv.slice(2)) {
17119
18274
  if (scope === "board" && action === "contract") {
17120
18275
  return void await runCommandCenterContractCli(args);
17121
18276
  }
17122
- if (scope === "run" && action === "create") return createRun(args);
18277
+ if (scope === "run" && action === "create") return void createRun(args);
17123
18278
  if (scope === "run" && action === "list") return listRuns();
17124
18279
  if (scope === "run" && action === "resolve") return resolveHarnessRunCli(args);
17125
18280
  if (scope === "run" && action === "status") return runStatus(args);