@kynver-app/runtime 0.1.118 → 0.1.120

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;
@@ -12451,7 +12466,7 @@ function isPipelineCleanupEnabled() {
12451
12466
 
12452
12467
  // src/cli.ts
12453
12468
  init_config();
12454
- import { mkdirSync as mkdirSync11, realpathSync } from "node:fs";
12469
+ import { mkdirSync as mkdirSync13, realpathSync } from "node:fs";
12455
12470
  import { fileURLToPath as fileURLToPath5 } from "node:url";
12456
12471
 
12457
12472
  // src/bootstrap.ts
@@ -12513,68 +12528,13 @@ async function runBootstrap(args) {
12513
12528
  await runSetup(setupArgs);
12514
12529
  console.log("");
12515
12530
  console.log(` Bootstrap complete \u2014 ${os9.hostname()} is linked to workspace "${primary.slug}".`);
12516
- console.log(" Next: run autonomous work with `kynver daemon --run <RUN_ID> --agent-os-id <AOS_ID> --execute`");
12517
- 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`.)");
12518
12533
  }
12519
12534
 
12520
- // src/cli.ts
12521
- init_run_store();
12522
-
12523
- // src/discard-disposable.ts
12524
- init_run_store();
12525
- init_status();
12526
- import { existsSync as existsSync41, rmSync as rmSync4 } from "node:fs";
12527
- import path54 from "node:path";
12528
- function normalizeRelativePath2(value) {
12529
- const normalized = value.replace(/\\/g, "/").replace(/^\.\//, "").trim();
12530
- if (!normalized || normalized.startsWith("/") || normalized.includes("..")) {
12531
- throw new Error(`unsafe path: ${value}`);
12532
- }
12533
- return normalized;
12534
- }
12535
- function parsePathsArg(raw) {
12536
- if (typeof raw !== "string" || !raw.trim()) return [];
12537
- return raw.split(",").map((p) => p.trim()).filter(Boolean);
12538
- }
12539
- function discardDisposableArtifacts(args) {
12540
- const { runId, workerName } = resolveWorkerTargetArgs(args);
12541
- const worker = loadWorker(runId, workerName);
12542
- const paths = [
12543
- ...parsePathsArg(args.path),
12544
- ...Array.isArray(args.paths) ? args.paths : []
12545
- ];
12546
- if (paths.length === 0) {
12547
- return { ok: false, removed: [], reason: "requires at least one --path" };
12548
- }
12549
- const worktreeRoot = path54.resolve(worker.worktreePath);
12550
- const removed = [];
12551
- for (const raw of paths) {
12552
- const rel = normalizeRelativePath2(raw);
12553
- const abs = path54.resolve(worktreeRoot, rel);
12554
- if (!abs.startsWith(worktreeRoot + path54.sep) && abs !== worktreeRoot) {
12555
- return { ok: false, removed, reason: `path escapes worktree: ${raw}` };
12556
- }
12557
- if (!existsSync41(abs)) {
12558
- return { ok: false, removed, reason: `path not found: ${raw}` };
12559
- }
12560
- rmSync4(abs, { recursive: true, force: true });
12561
- removed.push(rel);
12562
- }
12563
- const prior = Array.isArray(worker.disposableArtifactsRemoved) ? worker.disposableArtifactsRemoved.filter((p) => typeof p === "string") : [];
12564
- worker.disposableArtifactsRemoved = [.../* @__PURE__ */ new Set([...prior, ...removed])];
12565
- saveWorker(worker.runId, worker);
12566
- const status = computeWorkerStatus(worker);
12567
- return {
12568
- ok: true,
12569
- removed,
12570
- ...status.changedFiles.length ? { reason: "worktree still has other changes" } : {}
12571
- };
12572
- }
12573
- function discardDisposableCli(args) {
12574
- const result = discardDisposableArtifacts(args);
12575
- console.log(JSON.stringify(result, null, 2));
12576
- if (!result.ok) process.exit(1);
12577
- }
12535
+ // src/start.ts
12536
+ init_config();
12537
+ import os12 from "node:os";
12578
12538
 
12579
12539
  // src/daemon.ts
12580
12540
  init_config();
@@ -12624,15 +12584,15 @@ function validateDaemonInstallIdentity(config = loadUserConfig(), env = process.
12624
12584
  // src/daemon-heartbeat.ts
12625
12585
  import { mkdirSync as mkdirSync7, readFileSync as readFileSync16, renameSync as renameSync3, writeFileSync as writeFileSync4 } from "node:fs";
12626
12586
  import { homedir as homedir14 } from "node:os";
12627
- import path55 from "node:path";
12587
+ import path54 from "node:path";
12628
12588
  function daemonHeartbeatPath(agentOsId) {
12629
12589
  const safe = agentOsId.replace(/[^A-Za-z0-9_-]/g, "_");
12630
- return path55.join(homedir14(), ".kynver", `daemon-heartbeat-${safe}.json`);
12590
+ return path54.join(homedir14(), ".kynver", `daemon-heartbeat-${safe}.json`);
12631
12591
  }
12632
12592
  function writeDaemonHeartbeat(input) {
12633
12593
  try {
12634
12594
  const file = daemonHeartbeatPath(input.agentOsId);
12635
- mkdirSync7(path55.dirname(file), { recursive: true });
12595
+ mkdirSync7(path54.dirname(file), { recursive: true });
12636
12596
  const beat = {
12637
12597
  observedAt: (input.now ?? /* @__PURE__ */ new Date()).toISOString(),
12638
12598
  pid: process.pid,
@@ -12682,9 +12642,9 @@ function assertNativeDaemonAllowed() {
12682
12642
 
12683
12643
  // src/cron/cron-env.ts
12684
12644
  init_config();
12685
- import { existsSync as existsSync42 } from "node:fs";
12645
+ import { existsSync as existsSync41 } from "node:fs";
12686
12646
  import { homedir as homedir15 } from "node:os";
12687
- import path56 from "node:path";
12647
+ import path55 from "node:path";
12688
12648
  function envFlag4(name, defaultValue) {
12689
12649
  const raw = process.env[name]?.trim().toLowerCase();
12690
12650
  if (!raw) return defaultValue;
@@ -12700,7 +12660,7 @@ function envInt(name, fallback, min = 1) {
12700
12660
  function defaultKynverCronStorePath() {
12701
12661
  const explicit = process.env.KYNVER_CRON_STORE_PATH?.trim() || process.env.OPENCLAW_CRON_STORE_PATH?.trim();
12702
12662
  if (explicit) return explicit;
12703
- return path56.join(homedir15(), ".kynver", "agent-os-cron.json");
12663
+ return path55.join(homedir15(), ".kynver", "agent-os-cron.json");
12704
12664
  }
12705
12665
  function defaultKynverCronStatePath(storePath = defaultKynverCronStorePath()) {
12706
12666
  const explicit = process.env.KYNVER_CRON_TICK_STATE_PATH?.trim();
@@ -12720,7 +12680,7 @@ function resolveKynverCronEnv() {
12720
12680
  const fireBaseUrl = resolveKynverCronFireBaseUrl();
12721
12681
  const secret = resolveKynverCronSecret();
12722
12682
  const credsReady = Boolean(fireBaseUrl && secret);
12723
- const storeExists = existsSync42(storePath);
12683
+ const storeExists = existsSync41(storePath);
12724
12684
  const defaultEnabled = credsReady && (storeExists || envFlag4("KYNVER_CRON_TICK_FORCE", false));
12725
12685
  return {
12726
12686
  storePath,
@@ -12774,10 +12734,10 @@ async function fireKynverCronJob(input) {
12774
12734
 
12775
12735
  // src/cron/cron-lock.ts
12776
12736
  init_util();
12777
- 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";
12778
12738
  var STALE_LOCK_MS = 10 * 6e4;
12779
12739
  function readLockInfo(lockPath) {
12780
- if (!existsSync43(lockPath)) return null;
12740
+ if (!existsSync42(lockPath)) return null;
12781
12741
  try {
12782
12742
  const parsed = JSON.parse(readFileSync17(lockPath, "utf8"));
12783
12743
  if (typeof parsed.pid === "number" && typeof parsed.at === "string") return parsed;
@@ -12795,14 +12755,14 @@ function lockIsStale(lockPath) {
12795
12755
  return Date.now() - atMs > STALE_LOCK_MS;
12796
12756
  }
12797
12757
  function tryAcquireCronTickLock(lockPath) {
12798
- if (existsSync43(lockPath) && !lockIsStale(lockPath)) {
12758
+ if (existsSync42(lockPath) && !lockIsStale(lockPath)) {
12799
12759
  const info = readLockInfo(lockPath);
12800
12760
  return {
12801
12761
  acquired: false,
12802
12762
  reason: info ? `held by pid ${info.pid}` : "held by another process"
12803
12763
  };
12804
12764
  }
12805
- if (existsSync43(lockPath)) {
12765
+ if (existsSync42(lockPath)) {
12806
12766
  try {
12807
12767
  unlinkSync3(lockPath);
12808
12768
  } catch {
@@ -12939,11 +12899,34 @@ async function loadCronJobs(storePath = defaultKynverCronStorePath()) {
12939
12899
  const raw = await readFileIfExists(storePath);
12940
12900
  return parseCronStore(raw);
12941
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
+ }
12942
12925
 
12943
12926
  // src/cron/cron-tick-state.ts
12944
12927
  import { randomBytes } from "node:crypto";
12945
12928
  import { promises as fs4 } from "node:fs";
12946
- import path57 from "node:path";
12929
+ import path56 from "node:path";
12947
12930
  var EMPTY = { version: 1, jobs: {} };
12948
12931
  async function readFileIfExists2(filePath) {
12949
12932
  try {
@@ -12970,7 +12953,7 @@ async function loadCronTickState(statePath) {
12970
12953
  return parseCronTickState(raw);
12971
12954
  }
12972
12955
  async function writeStateAtomic(statePath, state) {
12973
- await fs4.mkdir(path57.dirname(statePath), { recursive: true });
12956
+ await fs4.mkdir(path56.dirname(statePath), { recursive: true });
12974
12957
  const suffix = randomBytes(6).toString("hex");
12975
12958
  const tmp = `${statePath}.tmp-${process.pid}-${Date.now()}-${suffix}`;
12976
12959
  await fs4.writeFile(tmp, `${JSON.stringify(state, null, 2)}
@@ -13150,7 +13133,7 @@ async function runKynverCronTick(opts = {}) {
13150
13133
  init_util();
13151
13134
 
13152
13135
  // src/pipeline-tick.ts
13153
- import path60 from "node:path";
13136
+ import path59 from "node:path";
13154
13137
  init_config();
13155
13138
 
13156
13139
  // src/pipeline-dispatch.ts
@@ -13292,7 +13275,7 @@ init_util();
13292
13275
  init_status();
13293
13276
  init_run_store();
13294
13277
  init_util();
13295
- import path58 from "node:path";
13278
+ import path57 from "node:path";
13296
13279
 
13297
13280
  // src/plan-progress-sync.ts
13298
13281
  init_config();
@@ -13317,7 +13300,7 @@ async function syncActiveWorkerPlanProgress(runId, args) {
13317
13300
  const outcomes = [];
13318
13301
  for (const name of Object.keys(run.workers || {})) {
13319
13302
  const worker = readJson(
13320
- path58.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
13303
+ path57.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
13321
13304
  void 0
13322
13305
  );
13323
13306
  if (!worker?.dispatched || !worker.taskId) continue;
@@ -13685,10 +13668,10 @@ function collectProviderEvidence(wanted, opts = {}) {
13685
13668
  // src/provider-evidence/wanted-store.ts
13686
13669
  init_run_store();
13687
13670
  init_util();
13688
- import path59 from "node:path";
13671
+ import path58 from "node:path";
13689
13672
  var WANTED_FILE = "provider-evidence-wanted.json";
13690
13673
  function wantedFilePath(runId) {
13691
- return path59.join(runDirectory(runId), WANTED_FILE);
13674
+ return path58.join(runDirectory(runId), WANTED_FILE);
13692
13675
  }
13693
13676
  function parseWantedItems(value) {
13694
13677
  if (!Array.isArray(value)) return [];
@@ -13727,7 +13710,7 @@ async function completeFinishedWorkers(runId, args) {
13727
13710
  const outcomes = [];
13728
13711
  for (const name of Object.keys(run.workers || {})) {
13729
13712
  const worker = readJson(
13730
- path60.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
13713
+ path59.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
13731
13714
  void 0
13732
13715
  );
13733
13716
  if (!worker?.taskId || worker.localOnly) continue;
@@ -13895,6 +13878,389 @@ async function runPipelineTick(args) {
13895
13878
  };
13896
13879
  }
13897
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
+
13898
14264
  // src/daemon.ts
13899
14265
  var DEFAULT_INTERVAL_MS = 6e4;
13900
14266
  var IDLE_INTERVAL_MS = 5 * 6e4;
@@ -13947,6 +14313,14 @@ async function runDaemon(args) {
13947
14313
  })
13948
14314
  );
13949
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
+ });
13950
14324
  while (!stopping) {
13951
14325
  try {
13952
14326
  writeDaemonHeartbeat({ agentOsId, runId });
@@ -13973,40 +14347,143 @@ async function runDaemon(args) {
13973
14347
  await awaitDaemonBackoff(intervalMs, () => stopping);
13974
14348
  }
13975
14349
  }
14350
+ await chatLoop;
13976
14351
  console.error(JSON.stringify({ event: "daemon_stop", runId, agentOsId }));
13977
14352
  }
13978
14353
 
13979
- // src/daemon-keeper.ts
13980
- init_config();
13981
- import { spawn as spawn6 } from "node:child_process";
13982
- init_util();
13983
- var DEFAULT_STALL_MS = 15 * 6e4;
13984
- var STARTUP_GRACE_MS = 2 * 6e4;
13985
- var KILL_GRACE_MS = 1e4;
13986
- var BACKOFF_BASE_MS = 5e3;
13987
- var BACKOFF_CAP_MS = 5 * 6e4;
13988
- var HEALTHY_RESET_MS = 30 * 6e4;
13989
- var POLL_MS = 5e3;
13990
- function resolveKeeperStallMs(flag, env = process.env) {
13991
- const fromFlag = typeof flag === "string" ? Number.parseInt(flag, 10) : NaN;
13992
- if (Number.isFinite(fromFlag) && fromFlag > 0) return fromFlag;
13993
- const fromEnv = Number.parseInt(env.KYNVER_DAEMON_STALL_MS ?? "", 10);
13994
- if (Number.isFinite(fromEnv) && fromEnv > 0) return fromEnv;
13995
- 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;
13996
14359
  }
13997
- function shouldRunDaemonKeeper(args, env = process.env) {
13998
- if (args.keeperChild === true || args.keeperChild === "true") return false;
13999
- if (args.noSupervise === true || args.noSupervise === "true") return false;
14000
- if (args.supervised === "false") return false;
14001
- const envFlag5 = (env.KYNVER_DAEMON_SUPERVISED ?? "").trim().toLowerCase();
14002
- if (envFlag5 === "0" || envFlag5 === "false" || envFlag5 === "no" || envFlag5 === "off") {
14003
- 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();
14004
14367
  }
14005
- 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 });
14006
14395
  }
14007
- function nextKeeperBackoffMs(consecutiveFailures, base = BACKOFF_BASE_MS, cap = BACKOFF_CAP_MS) {
14008
- const exp = Math.min(Math.max(consecutiveFailures, 1) - 1, 10);
14009
- 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);
14010
14487
  }
14011
14488
  function keeperRunWasHealthy(startedAtMs, endedAtMs, healthyMs = HEALTHY_RESET_MS) {
14012
14489
  return endedAtMs - startedAtMs >= healthyMs;
@@ -14109,7 +14586,7 @@ async function runDaemonKeeper(args, rawArgv = process.argv.slice(2)) {
14109
14586
 
14110
14587
  // src/plan-progress.ts
14111
14588
  init_config();
14112
- import path64 from "node:path";
14589
+ import path65 from "node:path";
14113
14590
 
14114
14591
  // src/bounded-build/constants.ts
14115
14592
  var DEFAULT_BUILD_MEM_BUDGET_BYTES = 1536 * 1024 * 1024;
@@ -14254,20 +14731,20 @@ import {
14254
14731
  mkdirSync as mkdirSync9,
14255
14732
  openSync as openSync7,
14256
14733
  readdirSync as readdirSync15,
14257
- readFileSync as readFileSync18,
14734
+ readFileSync as readFileSync19,
14258
14735
  unlinkSync as unlinkSync4,
14259
14736
  writeFileSync as writeFileSync6
14260
14737
  } from "node:fs";
14261
- import path62 from "node:path";
14738
+ import path63 from "node:path";
14262
14739
 
14263
14740
  // src/heavy-verification/paths.ts
14264
14741
  import { mkdirSync as mkdirSync8 } from "node:fs";
14265
- import path61 from "node:path";
14742
+ import path62 from "node:path";
14266
14743
  function resolveHeavyVerificationRoot() {
14267
- return path61.join(resolveKynverStateRoot(), "heavy-verification");
14744
+ return path62.join(resolveKynverStateRoot(), "heavy-verification");
14268
14745
  }
14269
14746
  function heavyVerificationSlotsDir() {
14270
- return path61.join(resolveHeavyVerificationRoot(), "slots");
14747
+ return path62.join(resolveHeavyVerificationRoot(), "slots");
14271
14748
  }
14272
14749
  function ensureHeavyVerificationDirs() {
14273
14750
  const dir = heavyVerificationSlotsDir();
@@ -14296,12 +14773,12 @@ function indexedSlotId(index) {
14296
14773
  return `slot-${index}`;
14297
14774
  }
14298
14775
  function slotFilePath(slotId, slotsDir = heavyVerificationSlotsDir()) {
14299
- return path62.join(slotsDir, `${slotId}.json`);
14776
+ return path63.join(slotsDir, `${slotId}.json`);
14300
14777
  }
14301
14778
  function readSlotRecord(filePath) {
14302
14779
  if (!existsSync44(filePath)) return null;
14303
14780
  try {
14304
- const parsed = JSON.parse(readFileSync18(filePath, "utf8"));
14781
+ const parsed = JSON.parse(readFileSync19(filePath, "utf8"));
14305
14782
  if (typeof parsed.slotId === "string" && typeof parsed.pid === "number" && typeof parsed.acquiredAt === "string" && typeof parsed.command === "string") {
14306
14783
  return parsed;
14307
14784
  }
@@ -14335,7 +14812,7 @@ function reclaimStaleHeavyVerificationSlots(opts = {}) {
14335
14812
  let reclaimed = 0;
14336
14813
  for (const name of readdirSync15(slotsDir)) {
14337
14814
  if (!name.endsWith(".json")) continue;
14338
- const filePath = path62.join(slotsDir, name);
14815
+ const filePath = path63.join(slotsDir, name);
14339
14816
  const before = existsSync44(filePath);
14340
14817
  reclaimStaleSlot(filePath, staleMs);
14341
14818
  if (before && !existsSync44(filePath)) reclaimed += 1;
@@ -14349,7 +14826,7 @@ function listActiveHeavyVerificationSlots(opts = {}) {
14349
14826
  const active = [];
14350
14827
  for (const name of readdirSync15(slotsDir)) {
14351
14828
  if (!name.endsWith(".json")) continue;
14352
- const record3 = readSlotRecord(path62.join(slotsDir, name));
14829
+ const record3 = readSlotRecord(path63.join(slotsDir, name));
14353
14830
  if (record3 && !slotIsStale(record3, staleMs)) active.push(record3);
14354
14831
  }
14355
14832
  return active;
@@ -14470,11 +14947,11 @@ function waitForHeavyVerificationSlot(command, timeoutMs, pollMs = 2e3, opts = {
14470
14947
 
14471
14948
  // src/harness-worktree-build-guard.ts
14472
14949
  init_paths();
14473
- import path63 from "node:path";
14950
+ import path64 from "node:path";
14474
14951
  function isPathUnderHarnessWorktree(cwd) {
14475
14952
  const worktreesDir = harnessWorktreesDir(resolveHarnessRoot());
14476
- const rel = path63.relative(worktreesDir, path63.resolve(cwd));
14477
- 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);
14478
14955
  }
14479
14956
  function assessHarnessWorktreeBuildGuard(cwd) {
14480
14957
  if (!isPathUnderHarnessWorktree(cwd)) return { ok: true };
@@ -14687,7 +15164,7 @@ async function emitPlanProgress(args) {
14687
15164
  }
14688
15165
  function verifyPlanLocal(args) {
14689
15166
  const worktree = required(args.worktree ? String(args.worktree) : void 0, "worktree");
14690
- const cwd = path64.resolve(worktree);
15167
+ const cwd = path65.resolve(worktree);
14691
15168
  const summary = runHarnessVerifyCommands(cwd);
14692
15169
  const emitJson = args.json === true || args.json === "true";
14693
15170
  const payload = { passed: summary.passed, worktree: cwd, steps: summary.steps };
@@ -14736,10 +15213,10 @@ async function verifyPlan(args) {
14736
15213
  }
14737
15214
 
14738
15215
  // src/harness-verify-cli.ts
14739
- import path65 from "node:path";
15216
+ import path66 from "node:path";
14740
15217
  init_util();
14741
15218
  function runHarnessVerifyCli(args) {
14742
- 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"));
14743
15220
  const emitJson = args.json === true || args.json === "true" || args.emitJson === true || args.emitJson === "true";
14744
15221
  const commands = [];
14745
15222
  const rawCmd = args.command;
@@ -14785,7 +15262,7 @@ function runHarnessVerifyCli(args) {
14785
15262
 
14786
15263
  // src/plan-persist-cli.ts
14787
15264
  init_config();
14788
- import { readFileSync as readFileSync19 } from "node:fs";
15265
+ import { readFileSync as readFileSync20 } from "node:fs";
14789
15266
  init_util();
14790
15267
  var OPERATIONS = ["create", "add_version", "update_metadata"];
14791
15268
  var FAILURE_KINDS = [
@@ -14798,7 +15275,7 @@ var FAILURE_KINDS = [
14798
15275
  function readBodyArg(args) {
14799
15276
  const bodyFile = args.bodyFile ? String(args.bodyFile) : void 0;
14800
15277
  if (bodyFile) {
14801
- return { body: readFileSync19(bodyFile, "utf8"), bodyPathHint: bodyFile };
15278
+ return { body: readFileSync20(bodyFile, "utf8"), bodyPathHint: bodyFile };
14802
15279
  }
14803
15280
  const inline = args.body ? String(args.body) : void 0;
14804
15281
  if (inline) return { body: inline };
@@ -15178,7 +15655,7 @@ ${text.slice(0, 800)}`,
15178
15655
  }
15179
15656
 
15180
15657
  // src/monitor/monitor.service.ts
15181
- import path67 from "node:path";
15658
+ import path68 from "node:path";
15182
15659
  init_run_store();
15183
15660
  init_status();
15184
15661
  init_util();
@@ -15238,10 +15715,10 @@ function classifyWorkerHealth(input) {
15238
15715
  init_paths();
15239
15716
  init_util();
15240
15717
  import { existsSync as existsSync45, mkdirSync as mkdirSync10, readdirSync as readdirSync16, unlinkSync as unlinkSync5 } from "node:fs";
15241
- import path66 from "node:path";
15718
+ import path67 from "node:path";
15242
15719
  function monitorsDir() {
15243
15720
  const { harnessRoot } = getHarnessPaths();
15244
- const dir = path66.join(harnessRoot, "monitors");
15721
+ const dir = path67.join(harnessRoot, "monitors");
15245
15722
  mkdirSync10(dir, { recursive: true });
15246
15723
  return dir;
15247
15724
  }
@@ -15249,7 +15726,7 @@ function monitorIdFor(runId, workerName) {
15249
15726
  return workerName ? `${safeSlug(runId)}--${safeSlug(workerName)}` : safeSlug(runId);
15250
15727
  }
15251
15728
  function monitorPath(monitorId) {
15252
- return path66.join(monitorsDir(), `${monitorId}.json`);
15729
+ return path67.join(monitorsDir(), `${monitorId}.json`);
15253
15730
  }
15254
15731
  function loadMonitorSession(monitorId) {
15255
15732
  return readJson(monitorPath(monitorId), void 0);
@@ -15270,7 +15747,7 @@ function listMonitorSessions() {
15270
15747
  for (const name of readdirSync16(dir)) {
15271
15748
  if (!name.endsWith(".json")) continue;
15272
15749
  const session = readJson(
15273
- path66.join(dir, name),
15750
+ path67.join(dir, name),
15274
15751
  void 0
15275
15752
  );
15276
15753
  if (!session?.monitorId) continue;
@@ -15363,7 +15840,7 @@ async function fetchTaskLeasesForWorkers(input) {
15363
15840
  // src/monitor/monitor.service.ts
15364
15841
  function workerRecord2(runId, name) {
15365
15842
  return readJson(
15366
- path67.join(runDirectory(runId), "workers", safeSlug(name), "worker.json"),
15843
+ path68.join(runDirectory(runId), "workers", safeSlug(name), "worker.json"),
15367
15844
  void 0
15368
15845
  );
15369
15846
  }
@@ -15573,17 +16050,17 @@ init_util();
15573
16050
  init_paths();
15574
16051
  import { spawn as spawn7 } from "node:child_process";
15575
16052
  import { closeSync as closeSync8, existsSync as existsSync46, openSync as openSync8 } from "node:fs";
15576
- import path68 from "node:path";
16053
+ import path69 from "node:path";
15577
16054
  import { fileURLToPath as fileURLToPath4 } from "node:url";
15578
16055
  function resolveDefaultCliPath2() {
15579
- return path68.join(fileURLToPath4(new URL(".", import.meta.url)), "cli.js");
16056
+ return path69.join(fileURLToPath4(new URL(".", import.meta.url)), "cli.js");
15580
16057
  }
15581
16058
  function spawnMonitorSidecar(opts) {
15582
16059
  const cliPath = opts.cliPath ?? resolveDefaultCliPath2();
15583
16060
  if (!existsSync46(cliPath)) return void 0;
15584
16061
  const monitorId = monitorIdFor(opts.runId, opts.workerName);
15585
16062
  const { harnessRoot } = getHarnessPaths();
15586
- const logPath = path68.join(harnessRoot, "monitors", `${monitorId}.log`);
16063
+ const logPath = path69.join(harnessRoot, "monitors", `${monitorId}.log`);
15587
16064
  let logFd;
15588
16065
  try {
15589
16066
  logFd = openSync8(logPath, "a");
@@ -15708,7 +16185,7 @@ init_run_store();
15708
16185
  init_status();
15709
16186
  init_util();
15710
16187
  init_config();
15711
- import path69 from "node:path";
16188
+ import path70 from "node:path";
15712
16189
  function skip(runId, worker, taskId, agentOsId, leaseOwner, reason) {
15713
16190
  return { runId, worker, taskId, agentOsId, leaseOwner, action: "skipped", reason };
15714
16191
  }
@@ -15721,7 +16198,7 @@ async function postRestartUnblock(args) {
15721
16198
  const errors = [];
15722
16199
  for (const run of listRunRecords()) {
15723
16200
  for (const name of Object.keys(run.workers ?? {})) {
15724
- 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");
15725
16202
  const worker = readJson(workerPath, void 0);
15726
16203
  if (!worker) {
15727
16204
  skipped.push(skip(run.id, name, "", "", "", "worker.json missing"));
@@ -15835,9 +16312,9 @@ async function postRestartUnblockCli(args) {
15835
16312
  // src/default-repo-cli.ts
15836
16313
  init_path_values();
15837
16314
  init_config();
15838
- import path70 from "node:path";
15839
- import { homedir as homedir16 } from "node:os";
15840
- 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");
15841
16318
  function ensureDefaultRepo(opts) {
15842
16319
  const existing = loadUserConfig();
15843
16320
  const resolved = resolveDefaultRepo({ ...opts, config: existing });
@@ -15918,14 +16395,14 @@ function summarizeResolvedDefaultRepo(resolved) {
15918
16395
  }
15919
16396
 
15920
16397
  // src/doctor/runtime-takeover.ts
15921
- import path72 from "node:path";
16398
+ import path73 from "node:path";
15922
16399
  init_path_values();
15923
16400
 
15924
16401
  // src/doctor/runtime-takeover.probes.ts
15925
16402
  init_config();
15926
- import { accessSync, constants, existsSync as existsSync47, readFileSync as readFileSync20 } from "node:fs";
15927
- import { homedir as homedir17 } from "node:os";
15928
- 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";
15929
16406
  import { spawnSync as spawnSync13 } from "node:child_process";
15930
16407
  init_paths();
15931
16408
  function captureCommand(bin, args) {
@@ -15968,15 +16445,15 @@ var defaultRuntimeTakeoverProbes = {
15968
16445
  commandOnPath: (bin) => captureCommand(process.platform === "win32" ? "where" : "which", [bin]),
15969
16446
  kynverVersion: (bin) => captureCommand(bin, ["--version"]),
15970
16447
  loadConfig: () => loadUserConfig(),
15971
- configFilePath: () => path71.join(homedir17(), ".kynver", "config.json"),
15972
- credentialsFilePath: () => path71.join(homedir17(), ".kynver", "credentials"),
16448
+ configFilePath: () => path72.join(homedir18(), ".kynver", "config.json"),
16449
+ credentialsFilePath: () => path72.join(homedir18(), ".kynver", "credentials"),
15973
16450
  readCredentials: () => {
15974
- const credPath = path71.join(homedir17(), ".kynver", "credentials");
16451
+ const credPath = path72.join(homedir18(), ".kynver", "credentials");
15975
16452
  if (!existsSync47(credPath)) {
15976
16453
  return { hasApiKey: false };
15977
16454
  }
15978
16455
  try {
15979
- const parsed = JSON.parse(readFileSync20(credPath, "utf8"));
16456
+ const parsed = JSON.parse(readFileSync21(credPath, "utf8"));
15980
16457
  return {
15981
16458
  hasApiKey: Boolean(parsed.apiKey?.trim()),
15982
16459
  runnerTokenPrefix: tokenPrefix(parsed.runnerToken),
@@ -16006,7 +16483,7 @@ var defaultRuntimeTakeoverProbes = {
16006
16483
  })()
16007
16484
  }),
16008
16485
  harnessRoot: () => resolveHarnessRoot(),
16009
- legacyOpenclawHarnessRoot: () => path71.join(homedir17(), ".openclaw", "harness"),
16486
+ legacyOpenclawHarnessRoot: () => path72.join(homedir18(), ".openclaw", "harness"),
16010
16487
  pathExists: (target) => existsSync47(target),
16011
16488
  pathWritable: (target) => isWritable(target)
16012
16489
  };
@@ -16413,8 +16890,8 @@ function assessVercelDeployEvidence(probes) {
16413
16890
  }
16414
16891
  function assessHarnessDirs(probes) {
16415
16892
  const harnessRoot = probes.harnessRoot();
16416
- const runsDir = path72.join(harnessRoot, "runs");
16417
- const worktreesDir = path72.join(harnessRoot, "worktrees");
16893
+ const runsDir = path73.join(harnessRoot, "runs");
16894
+ const worktreesDir = path73.join(harnessRoot, "worktrees");
16418
16895
  const displayHarnessRoot = redactHomePath(harnessRoot);
16419
16896
  const displayRunsDir = redactHomePath(runsDir);
16420
16897
  const displayWorktreesDir = redactHomePath(worktreesDir);
@@ -16683,9 +17160,9 @@ function applySchedulerCutoverAttestation(config) {
16683
17160
 
16684
17161
  // src/scheduler-cutover-cli.ts
16685
17162
  init_config();
16686
- import path73 from "node:path";
16687
- import { homedir as homedir18 } from "node:os";
16688
- 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");
16689
17166
  function runSchedulerCutoverCheckCli(json = false) {
16690
17167
  const config = loadUserConfig();
16691
17168
  const report = assessSchedulerCutover(config);
@@ -16822,9 +17299,674 @@ async function runCronTickCli(args) {
16822
17299
  );
16823
17300
  }
16824
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
+
16825
17967
  // src/lane/landing-maintainer-tick.ts
16826
17968
  init_config();
16827
- import os11 from "node:os";
17969
+ import os13 from "node:os";
16828
17970
  init_config();
16829
17971
  init_box_identity();
16830
17972
  init_resource_gate();
@@ -16840,10 +17982,10 @@ var LANDING_MAINTAINER_LANE_SPEC = {
16840
17982
  };
16841
17983
 
16842
17984
  // src/lane/landing-maintainer-local.ts
16843
- import { spawnSync as spawnSync14 } from "node:child_process";
16844
- import path74 from "node:path";
17985
+ import { spawnSync as spawnSync15 } from "node:child_process";
17986
+ import path78 from "node:path";
16845
17987
  function runLandingWrapper(prNumber, repoRoot, execute) {
16846
- const script = path74.join(repoRoot, LANDING_MAINTAINER_LANE_SPEC.landScript);
17988
+ const script = path78.join(repoRoot, LANDING_MAINTAINER_LANE_SPEC.landScript);
16847
17989
  const args = [script, String(prNumber), ...LANDING_MAINTAINER_LANE_SPEC.landScriptArgs];
16848
17990
  if (!execute) {
16849
17991
  return {
@@ -16854,7 +17996,7 @@ function runLandingWrapper(prNumber, repoRoot, execute) {
16854
17996
  stderr: ""
16855
17997
  };
16856
17998
  }
16857
- const result = spawnSync14("node", args, {
17999
+ const result = spawnSync15("node", args, {
16858
18000
  cwd: repoRoot,
16859
18001
  encoding: "utf8",
16860
18002
  timeout: 10 * 60 * 1e3
@@ -16869,7 +18011,7 @@ function runLandingWrapper(prNumber, repoRoot, execute) {
16869
18011
  }
16870
18012
  function resolveLandingMaintainerRepoRoot(args) {
16871
18013
  const explicit = args.repoPath ? String(args.repoPath).trim() : "";
16872
- if (explicit) return path74.resolve(explicit);
18014
+ if (explicit) return path78.resolve(explicit);
16873
18015
  const resolved = resolveDefaultRepo();
16874
18016
  return resolved?.repo ?? process.cwd();
16875
18017
  }
@@ -16888,7 +18030,7 @@ async function runLandingMaintainerLaneTick(args) {
16888
18030
  ...buildBoxResourceSnapshotFromGate(resourceGate, {
16889
18031
  harnessRunId: runId,
16890
18032
  boxKind: resolveBoxKindFromConfig(loadUserConfig()),
16891
- hostLabel: os11.hostname()
18033
+ hostLabel: os13.hostname()
16892
18034
  }),
16893
18035
  providerHealthy: resourceGate.ok,
16894
18036
  authorizedForRepair: resourceGate.ok,
@@ -16994,6 +18136,7 @@ function usage(code = 0) {
16994
18136
  "Usage:",
16995
18137
  " kynver login [--api-key KEY] [--api-base-url URL] (omit --api-key to authorize in the browser)",
16996
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)",
16997
18140
  " kynver runner credential [--agent-os-id ID] [--base-url URL]",
16998
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]",
16999
18142
  " kynver daemon --run RUN_ID --agent-os-id AOS_ID [--execute] [--interval-ms MS] [--stall-ms MS] [--no-supervise]",
@@ -17034,6 +18177,8 @@ function usage(code = 0) {
17034
18177
  " kynver doctor runtime-takeover [--remediate-default-repo]",
17035
18178
  " kynver scheduler cutover-check [--json]",
17036
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]",
17037
18182
  " kynver cron status [--json]",
17038
18183
  " kynver cron tick [--agent-os-id AOS_ID] [--json]",
17039
18184
  " kynver lane tick landing-maintainer [--fleet] [--repo OWNER/NAME] [--agent-os-id AOS_ID] [--execute] [--json]",
@@ -17057,8 +18202,8 @@ async function main(argv = process.argv.slice(2)) {
17057
18202
  if (action && isHelpFlag(action) || rest.some(isHelpFlag)) return usage(0);
17058
18203
  const args = parseArgs(rest);
17059
18204
  const { runsDir, worktreesDir } = getPaths();
17060
- mkdirSync11(runsDir, { recursive: true });
17061
- mkdirSync11(worktreesDir, { recursive: true });
18205
+ mkdirSync13(runsDir, { recursive: true });
18206
+ mkdirSync13(worktreesDir, { recursive: true });
17062
18207
  if (scope === "daemon") {
17063
18208
  assertNativeDaemonAllowed();
17064
18209
  }
@@ -17079,6 +18224,7 @@ async function main(argv = process.argv.slice(2)) {
17079
18224
  }
17080
18225
  if (scope === "login") return void await runLogin(args);
17081
18226
  if (scope === "bootstrap") return void await runBootstrap(args);
18227
+ if (scope === "start") return void await runStart(args);
17082
18228
  if (scope === "status") return runStatus(args);
17083
18229
  if (scope === "runner" && action === "credential") return void await mintRunnerCredential(args);
17084
18230
  if (scope === "setup") return void await runSetup(args);
@@ -17109,6 +18255,12 @@ async function main(argv = process.argv.slice(2)) {
17109
18255
  if (scope === "scheduler" && action === "attest-cutover") {
17110
18256
  return runSchedulerAttestCutoverCli(args.json === true);
17111
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
+ }
17112
18264
  if (scope === "cron" && action === "status") {
17113
18265
  return void await runCronStatusCli(args.json === true);
17114
18266
  }
@@ -17122,7 +18274,7 @@ async function main(argv = process.argv.slice(2)) {
17122
18274
  if (scope === "board" && action === "contract") {
17123
18275
  return void await runCommandCenterContractCli(args);
17124
18276
  }
17125
- if (scope === "run" && action === "create") return createRun(args);
18277
+ if (scope === "run" && action === "create") return void createRun(args);
17126
18278
  if (scope === "run" && action === "list") return listRuns();
17127
18279
  if (scope === "run" && action === "resolve") return resolveHarnessRunCli(args);
17128
18280
  if (scope === "run" && action === "status") return runStatus(args);