@kynver-app/runtime 0.1.51 → 0.1.59
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/callbacks.d.ts +17 -0
- package/dist/cleanup-guards-helpers.d.ts +2 -0
- package/dist/cleanup-guards.d.ts +15 -4
- package/dist/cleanup-orphan-safety.d.ts +17 -0
- package/dist/cleanup-types.d.ts +28 -1
- package/dist/cli.js +712 -183
- package/dist/cli.js.map +4 -4
- package/dist/config.d.ts +5 -0
- package/dist/db-credential-resolver.d.ts +37 -0
- package/dist/db-url-hint.d.ts +4 -0
- package/dist/discard-disposable.d.ts +6 -0
- package/dist/disk-gate.d.ts +5 -0
- package/dist/dispatch.d.ts +2 -1
- package/dist/disposable-artifacts.d.ts +2 -0
- package/dist/doctor/runtime-takeover-scheduler.d.ts +5 -0
- package/dist/doctor/runtime-takeover.probes.d.ts +2 -0
- package/dist/harness-storage-snapshot.d.ts +26 -0
- package/dist/index.d.ts +8 -1
- package/dist/index.js +1299 -588
- package/dist/index.js.map +4 -4
- package/dist/material-worktree-changes.d.ts +2 -0
- package/dist/paths.d.ts +1 -0
- package/dist/status.d.ts +13 -0
- package/dist/supervisor.d.ts +2 -1
- package/dist/worker-env.d.ts +1 -1
- package/dist/worktree-completion-handoff.d.ts +17 -0
- package/dist/wsl-host.d.ts +71 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -169,12 +169,14 @@ var FORBIDDEN_WORKER_ENV_KEYS = [
|
|
|
169
169
|
"NEXTAUTH_SECRET",
|
|
170
170
|
"DATABASE_URL",
|
|
171
171
|
"PRODUCTION_DATABASE_URL",
|
|
172
|
+
"KYNVER_PRODUCTION_DATABASE_URL",
|
|
172
173
|
"REDIS_URL",
|
|
173
174
|
"GOOGLE_CLIENT_SECRET",
|
|
174
175
|
"GITHUB_CLIENT_SECRET",
|
|
175
176
|
"KYNVER_API_KEY",
|
|
176
177
|
"KYNVER_SERVICE_SECRET",
|
|
177
178
|
"KYNVER_RUNTIME_SECRET",
|
|
179
|
+
"KYNVER_CRON_SECRET",
|
|
178
180
|
"OPENCLAW_CRON_SECRET",
|
|
179
181
|
"QSTASH_TOKEN",
|
|
180
182
|
"QSTASH_CURRENT_SIGNING_KEY",
|
|
@@ -455,7 +457,7 @@ function presentUserConfig(config) {
|
|
|
455
457
|
}
|
|
456
458
|
function inferSetupFields(existing, args) {
|
|
457
459
|
const creds = loadCredentialsFile();
|
|
458
|
-
const apiBaseUrl = (typeof args.apiBaseUrl === "string" ? args.apiBaseUrl : void 0) || existing.apiBaseUrl?.trim() || process.env.KYNVER_API_URL?.trim() || process.env.OPENCLAW_CRON_FIRE_BASE_URL?.trim();
|
|
460
|
+
const apiBaseUrl = (typeof args.apiBaseUrl === "string" ? args.apiBaseUrl : void 0) || existing.apiBaseUrl?.trim() || process.env.KYNVER_API_URL?.trim() || process.env.KYNVER_CRON_FIRE_BASE_URL?.trim() || process.env.OPENCLAW_CRON_FIRE_BASE_URL?.trim();
|
|
459
461
|
const agentOsId = (typeof args.agentOsId === "string" ? args.agentOsId : void 0) || existing.agentOsId?.trim() || process.env.KYNVER_AGENT_OS_ID?.trim() || (creds.runnerToken?.trim().startsWith("krc1.") ? creds.runnerTokenAgentOsId?.trim() : void 0);
|
|
460
462
|
const explicitRepo = typeof args.repo === "string" ? args.repo : args.discoverRepo === true || args.discoverRepo === "true" ? discoverDefaultRepo()?.repo : void 0;
|
|
461
463
|
const defaultRepo = explicitRepo || existing.defaultRepo?.trim() || process.env.KYNVER_DEFAULT_REPO?.trim() || process.env.KYNVER_HARNESS_REPO?.trim() || discoverDefaultRepo()?.repo;
|
|
@@ -507,17 +509,17 @@ function saveRunnerToken(agentOsId, token) {
|
|
|
507
509
|
}
|
|
508
510
|
function resolveBaseUrl(argsBaseUrl) {
|
|
509
511
|
const baseUrl = resolveConfiguredBaseUrl(argsBaseUrl);
|
|
510
|
-
if (!baseUrl) failConfig("requires --base-url, KYNVER_API_URL,
|
|
512
|
+
if (!baseUrl) failConfig("requires --base-url, KYNVER_API_URL, KYNVER_CRON_FIRE_BASE_URL, or ~/.kynver/config.json apiBaseUrl");
|
|
511
513
|
return baseUrl;
|
|
512
514
|
}
|
|
513
515
|
function resolveConfiguredBaseUrl(argsBaseUrl) {
|
|
514
|
-
const baseUrl = argsBaseUrl || process.env.KYNVER_API_URL || process.env.OPENCLAW_CRON_FIRE_BASE_URL || loadUserConfig().apiBaseUrl;
|
|
516
|
+
const baseUrl = argsBaseUrl || process.env.KYNVER_API_URL || process.env.KYNVER_CRON_FIRE_BASE_URL || process.env.OPENCLAW_CRON_FIRE_BASE_URL || loadUserConfig().apiBaseUrl;
|
|
515
517
|
return baseUrl ? trimTrailingSlash(String(baseUrl)) : void 0;
|
|
516
518
|
}
|
|
517
519
|
function resolveConfiguredCallbackSecret(argsSecret, agentOsId) {
|
|
518
520
|
const scoped = argsSecret || loadRunnerToken(agentOsId) || (agentOsId ? void 0 : loadRunnerToken(loadUserConfig().agentOsId));
|
|
519
521
|
if (scoped) return String(scoped);
|
|
520
|
-
const globalSecret = process.env.KYNVER_RUNTIME_SECRET || process.env.OPENCLAW_CRON_SECRET;
|
|
522
|
+
const globalSecret = process.env.KYNVER_RUNTIME_SECRET || process.env.KYNVER_CRON_SECRET || process.env.OPENCLAW_CRON_SECRET;
|
|
521
523
|
if (globalSecret) {
|
|
522
524
|
console.warn(
|
|
523
525
|
"[kynver] using deployment-level callback secret; run `kynver runner credential --agent-os-id <id>` for a scoped token"
|
|
@@ -530,7 +532,7 @@ function resolveCallbackSecret(argsSecret, agentOsId) {
|
|
|
530
532
|
const configured = resolveConfiguredCallbackSecret(argsSecret, agentOsId);
|
|
531
533
|
if (configured) return configured;
|
|
532
534
|
failConfig(
|
|
533
|
-
"requires --secret, KYNVER_RUNNER_TOKEN, a scoped runner token (`kynver runner credential`), ~/.kynver/credentials runnerToken, KYNVER_API_KEY with an API base URL to mint one, or (legacy) KYNVER_RUNTIME_SECRET / OPENCLAW_CRON_SECRET"
|
|
535
|
+
"requires --secret, KYNVER_RUNNER_TOKEN, a scoped runner token (`kynver runner credential`), ~/.kynver/credentials runnerToken, KYNVER_API_KEY with an API base URL to mint one, or (legacy) KYNVER_RUNTIME_SECRET / KYNVER_CRON_SECRET / OPENCLAW_CRON_SECRET"
|
|
534
536
|
);
|
|
535
537
|
}
|
|
536
538
|
async function resolveCallbackSecretWithMint(argsSecret, agentOsId, opts) {
|
|
@@ -548,7 +550,7 @@ async function resolveCallbackSecretWithMint(argsSecret, agentOsId, opts) {
|
|
|
548
550
|
}
|
|
549
551
|
}
|
|
550
552
|
failConfig(
|
|
551
|
-
"requires --secret, KYNVER_RUNNER_TOKEN, a scoped runner token (`kynver runner credential`), ~/.kynver/credentials runnerToken, KYNVER_API_KEY with an API base URL to mint one, or (legacy) KYNVER_RUNTIME_SECRET / OPENCLAW_CRON_SECRET"
|
|
553
|
+
"requires --secret, KYNVER_RUNNER_TOKEN, a scoped runner token (`kynver runner credential`), ~/.kynver/credentials runnerToken, KYNVER_API_KEY with an API base URL to mint one, or (legacy) KYNVER_RUNTIME_SECRET / KYNVER_CRON_SECRET / OPENCLAW_CRON_SECRET"
|
|
552
554
|
);
|
|
553
555
|
}
|
|
554
556
|
async function refreshRunnerToken(agentOsId, opts) {
|
|
@@ -708,6 +710,11 @@ function buildHarnessCallbackHeaders(secret) {
|
|
|
708
710
|
}
|
|
709
711
|
return {
|
|
710
712
|
"Content-Type": "application/json",
|
|
713
|
+
// Canonical header. We keep sending the legacy `X-OpenClaw-Cron-Secret`
|
|
714
|
+
// (and `X-Kynver-Runtime-Secret`) so an un-upgraded Kynver server that
|
|
715
|
+
// only reads the old header still authenticates this runner during the
|
|
716
|
+
// rename compat window.
|
|
717
|
+
"X-Kynver-Cron-Secret": trimmed,
|
|
711
718
|
"X-OpenClaw-Cron-Secret": trimmed,
|
|
712
719
|
"X-Kynver-Runtime-Secret": trimmed
|
|
713
720
|
};
|
|
@@ -751,18 +758,88 @@ async function getJson(url, secret) {
|
|
|
751
758
|
}
|
|
752
759
|
|
|
753
760
|
// src/disk-gate.ts
|
|
754
|
-
import { statfsSync } from "node:fs";
|
|
761
|
+
import { statfsSync as statfsSync2 } from "node:fs";
|
|
762
|
+
|
|
763
|
+
// src/wsl-host.ts
|
|
764
|
+
import { existsSync as existsSync5, readFileSync as readFileSync5, statfsSync } from "node:fs";
|
|
765
|
+
var DEFAULT_WSL_HOST_WARN_FREE_BYTES = 25 * 1024 * 1024 * 1024;
|
|
766
|
+
var DEFAULT_WSL_HOST_CRITICAL_FREE_BYTES = 12 * 1024 * 1024 * 1024;
|
|
767
|
+
var DEFAULT_WSL_HOST_MOUNT = "/mnt/c";
|
|
768
|
+
function isWslHost() {
|
|
769
|
+
if (process.platform !== "linux") return false;
|
|
770
|
+
for (const probe of ["/proc/sys/kernel/osrelease", "/proc/version"]) {
|
|
771
|
+
try {
|
|
772
|
+
if (!existsSync5(probe)) continue;
|
|
773
|
+
const text = readFileSync5(probe, "utf8");
|
|
774
|
+
if (/microsoft|wsl/i.test(text)) return true;
|
|
775
|
+
} catch {
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
return false;
|
|
779
|
+
}
|
|
780
|
+
function observeWslHostDisk(options = {}) {
|
|
781
|
+
const wsl = options.forceWsl === void 0 ? isWslHost() : options.forceWsl;
|
|
782
|
+
if (!wsl) return null;
|
|
783
|
+
const path44 = options.wslHostMount?.trim() || process.env.KYNVER_WSL_HOST_MOUNT?.trim() || DEFAULT_WSL_HOST_MOUNT;
|
|
784
|
+
const warnBelowBytes = options.wslHostFreeWarnBytes ?? DEFAULT_WSL_HOST_WARN_FREE_BYTES;
|
|
785
|
+
const criticalBelowBytes = options.wslHostFreeCriticalBytes ?? DEFAULT_WSL_HOST_CRITICAL_FREE_BYTES;
|
|
786
|
+
const statfs = options.statfs ?? statfsSync;
|
|
787
|
+
let stats;
|
|
788
|
+
try {
|
|
789
|
+
stats = statfs(path44);
|
|
790
|
+
} catch (error) {
|
|
791
|
+
return {
|
|
792
|
+
ok: false,
|
|
793
|
+
path: path44,
|
|
794
|
+
freeBytes: 0,
|
|
795
|
+
totalBytes: 0,
|
|
796
|
+
usedPercent: 100,
|
|
797
|
+
warnBelowBytes,
|
|
798
|
+
criticalBelowBytes,
|
|
799
|
+
reason: `Windows host disk probe failed at ${path44}: ${error.message}`,
|
|
800
|
+
probeError: error.message
|
|
801
|
+
};
|
|
802
|
+
}
|
|
803
|
+
const freeBytes = Number(stats.bavail) * Number(stats.bsize);
|
|
804
|
+
const totalBytes = Number(stats.blocks) * Number(stats.bsize);
|
|
805
|
+
const usedPercent = totalBytes > 0 ? (totalBytes - freeBytes) / totalBytes * 100 : 100;
|
|
806
|
+
const lowFree = freeBytes < warnBelowBytes;
|
|
807
|
+
const criticalFree = freeBytes < criticalBelowBytes;
|
|
808
|
+
const ok = !lowFree && !criticalFree;
|
|
809
|
+
const freeGiB = (freeBytes / (1024 * 1024 * 1024)).toFixed(1);
|
|
810
|
+
let reason = null;
|
|
811
|
+
if (!ok) {
|
|
812
|
+
const tag = criticalFree ? "critical" : "warning";
|
|
813
|
+
reason = `Windows host disk ${path44} at ${tag}: ${freeGiB} GiB free (<${(criticalFree ? criticalBelowBytes : warnBelowBytes) / 1024 / 1024 / 1024} GiB); WSL VHDX cannot grow safely. ${summarizeWslRecoverySteps()}`;
|
|
814
|
+
}
|
|
815
|
+
return {
|
|
816
|
+
ok,
|
|
817
|
+
path: path44,
|
|
818
|
+
freeBytes,
|
|
819
|
+
totalBytes,
|
|
820
|
+
usedPercent,
|
|
821
|
+
warnBelowBytes,
|
|
822
|
+
criticalBelowBytes,
|
|
823
|
+
reason,
|
|
824
|
+
probeError: null
|
|
825
|
+
};
|
|
826
|
+
}
|
|
827
|
+
function summarizeWslRecoverySteps() {
|
|
828
|
+
return "Recovery: 1) free Windows C: (empty Recycle Bin / Storage Sense / clear %TEMP%); 2) shut down WSL (`wsl --shutdown`) then compact the VHDX (`Optimize-VHD` or `diskpart compact vdisk`); 3) clear local node_modules / .next / harness worktrees before restarting workers. Full runbook: docs/runbooks/wsl-disk-pressure.md.";
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
// src/disk-gate.ts
|
|
755
832
|
var DEFAULT_WARN_FREE_BYTES = 30 * 1024 * 1024 * 1024;
|
|
756
833
|
var DEFAULT_CRITICAL_FREE_BYTES = 15 * 1024 * 1024 * 1024;
|
|
757
834
|
var DEFAULT_MAX_USED_PERCENT = 80;
|
|
758
835
|
var DEFAULT_HARD_MAX_USED_PERCENT = 90;
|
|
759
836
|
function observeRunnerDiskGate(input = {}) {
|
|
760
|
-
const
|
|
837
|
+
const path44 = input.diskPath?.trim() || "/";
|
|
761
838
|
const warnBelowBytes = input.diskFreeWarnBytes ?? DEFAULT_WARN_FREE_BYTES;
|
|
762
839
|
const criticalBelowBytes = input.diskFreeCriticalBytes ?? DEFAULT_CRITICAL_FREE_BYTES;
|
|
763
840
|
const maxUsedPercent = input.diskMaxUsedPercent ?? DEFAULT_MAX_USED_PERCENT;
|
|
764
841
|
const hardMaxUsedPercent = input.diskHardMaxUsedPercent ?? DEFAULT_HARD_MAX_USED_PERCENT;
|
|
765
|
-
const stats =
|
|
842
|
+
const stats = statfsSync2(path44);
|
|
766
843
|
const freeBytes = Number(stats.bavail) * Number(stats.bsize);
|
|
767
844
|
const totalBytes = Number(stats.blocks) * Number(stats.bsize);
|
|
768
845
|
const usedPercent = totalBytes > 0 ? (totalBytes - freeBytes) / totalBytes * 100 : 100;
|
|
@@ -770,19 +847,22 @@ function observeRunnerDiskGate(input = {}) {
|
|
|
770
847
|
const criticalFree = freeBytes < criticalBelowBytes;
|
|
771
848
|
const highUse = usedPercent > maxUsedPercent;
|
|
772
849
|
const hardHighUse = usedPercent > hardMaxUsedPercent;
|
|
773
|
-
const
|
|
850
|
+
const localOk = !lowFree && !criticalFree && !highUse && !hardHighUse;
|
|
851
|
+
const wslHost = input.skipWslHostCheck ? null : observeWslHostDisk(input.wslHost);
|
|
852
|
+
const ok = localOk && (wslHost ? wslHost.ok : true);
|
|
774
853
|
let reason = null;
|
|
775
854
|
if (!ok) {
|
|
776
855
|
reason = [
|
|
777
856
|
criticalFree ? `free space below critical ${criticalBelowBytes} bytes` : null,
|
|
778
857
|
lowFree ? `free space below warning ${warnBelowBytes} bytes` : null,
|
|
779
858
|
hardHighUse ? `used percent above hard cap ${hardMaxUsedPercent}%` : null,
|
|
780
|
-
highUse ? `used percent above cap ${maxUsedPercent}%` : null
|
|
859
|
+
highUse ? `used percent above cap ${maxUsedPercent}%` : null,
|
|
860
|
+
wslHost && !wslHost.ok ? wslHost.reason : null
|
|
781
861
|
].filter(Boolean).join("; ");
|
|
782
862
|
}
|
|
783
863
|
return {
|
|
784
864
|
ok,
|
|
785
|
-
path:
|
|
865
|
+
path: path44,
|
|
786
866
|
freeBytes,
|
|
787
867
|
totalBytes,
|
|
788
868
|
usedPercent,
|
|
@@ -790,7 +870,8 @@ function observeRunnerDiskGate(input = {}) {
|
|
|
790
870
|
criticalBelowBytes,
|
|
791
871
|
maxUsedPercent,
|
|
792
872
|
hardMaxUsedPercent,
|
|
793
|
-
reason
|
|
873
|
+
reason,
|
|
874
|
+
wslHost
|
|
794
875
|
};
|
|
795
876
|
}
|
|
796
877
|
|
|
@@ -798,7 +879,7 @@ function observeRunnerDiskGate(input = {}) {
|
|
|
798
879
|
import os2 from "node:os";
|
|
799
880
|
|
|
800
881
|
// src/bounded-build/meminfo.ts
|
|
801
|
-
import { readFileSync as
|
|
882
|
+
import { readFileSync as readFileSync6 } from "node:fs";
|
|
802
883
|
import os from "node:os";
|
|
803
884
|
function readMemAvailableBytes(meminfoText) {
|
|
804
885
|
if (meminfoText !== void 0) {
|
|
@@ -808,7 +889,7 @@ function readMemAvailableBytes(meminfoText) {
|
|
|
808
889
|
}
|
|
809
890
|
if (process.platform === "linux") {
|
|
810
891
|
try {
|
|
811
|
-
const meminfo =
|
|
892
|
+
const meminfo = readFileSync6("/proc/meminfo", "utf8");
|
|
812
893
|
const match = meminfo.match(/^MemAvailable:\s+(\d+)\s*kB/m);
|
|
813
894
|
if (match) return Number(match[1]) * 1024;
|
|
814
895
|
} catch {
|
|
@@ -821,11 +902,11 @@ function readMemAvailableBytes(meminfoText) {
|
|
|
821
902
|
import path7 from "node:path";
|
|
822
903
|
|
|
823
904
|
// src/run-store.ts
|
|
824
|
-
import { existsSync as
|
|
905
|
+
import { existsSync as existsSync7, readdirSync as readdirSync2 } from "node:fs";
|
|
825
906
|
import path6 from "node:path";
|
|
826
907
|
|
|
827
908
|
// src/paths.ts
|
|
828
|
-
import { existsSync as
|
|
909
|
+
import { existsSync as existsSync6 } from "node:fs";
|
|
829
910
|
import { homedir as homedir4 } from "node:os";
|
|
830
911
|
import path5 from "node:path";
|
|
831
912
|
var LEGACY_ROOT = path5.join(homedir4(), ".openclaw", "harness");
|
|
@@ -835,8 +916,8 @@ function resolveHarnessRoot() {
|
|
|
835
916
|
const configured = loadUserConfig().harnessRoot?.trim();
|
|
836
917
|
if (configured) return resolveUserPath(configured);
|
|
837
918
|
const kynverRoot = path5.join(homedir4(), ".kynver", "harness");
|
|
838
|
-
if (
|
|
839
|
-
if (
|
|
919
|
+
if (existsSync6(kynverRoot)) return kynverRoot;
|
|
920
|
+
if (existsSync6(LEGACY_ROOT)) return LEGACY_ROOT;
|
|
840
921
|
return kynverRoot;
|
|
841
922
|
}
|
|
842
923
|
function getHarnessPaths() {
|
|
@@ -861,7 +942,7 @@ function loadRun(id) {
|
|
|
861
942
|
}
|
|
862
943
|
function listRunRecords() {
|
|
863
944
|
const { runsDir } = getPaths();
|
|
864
|
-
if (!
|
|
945
|
+
if (!existsSync7(runsDir)) return [];
|
|
865
946
|
const runs = [];
|
|
866
947
|
for (const entry of readdirSync2(runsDir, { withFileTypes: true })) {
|
|
867
948
|
if (!entry.isDirectory()) continue;
|
|
@@ -893,7 +974,7 @@ function runDirectory(id) {
|
|
|
893
974
|
}
|
|
894
975
|
|
|
895
976
|
// src/heartbeat.ts
|
|
896
|
-
import { existsSync as
|
|
977
|
+
import { existsSync as existsSync8, readFileSync as readFileSync7 } from "node:fs";
|
|
897
978
|
var HEARTBEAT_FUTURE_SKEW_MS = 6e4;
|
|
898
979
|
function isTerminalHeartbeatPhase(phase) {
|
|
899
980
|
return phase === "complete";
|
|
@@ -912,10 +993,10 @@ function parseHeartbeat(file) {
|
|
|
912
993
|
heartbeatBlocker: null,
|
|
913
994
|
timestampAnomalies: []
|
|
914
995
|
};
|
|
915
|
-
if (!
|
|
996
|
+
if (!existsSync8(file)) return result;
|
|
916
997
|
const maxFutureMs = Date.now() + HEARTBEAT_FUTURE_SKEW_MS;
|
|
917
998
|
const clampedTo = new Date(maxFutureMs).toISOString();
|
|
918
|
-
const lines =
|
|
999
|
+
const lines = readFileSync7(file, "utf8").split("\n").filter(Boolean);
|
|
919
1000
|
for (const line of lines) {
|
|
920
1001
|
const entry = safeJson(line);
|
|
921
1002
|
if (!entry || typeof entry !== "object" || Array.isArray(entry)) continue;
|
|
@@ -942,7 +1023,7 @@ function parseHeartbeat(file) {
|
|
|
942
1023
|
}
|
|
943
1024
|
|
|
944
1025
|
// src/stream.ts
|
|
945
|
-
import { existsSync as
|
|
1026
|
+
import { existsSync as existsSync9, readFileSync as readFileSync8 } from "node:fs";
|
|
946
1027
|
|
|
947
1028
|
// src/shell-command-outcome.ts
|
|
948
1029
|
var NPM_AUDIT_RE = /\bnpm\s+audit\b/i;
|
|
@@ -1146,8 +1227,8 @@ function parseHarnessStream(file) {
|
|
|
1146
1227
|
error: null,
|
|
1147
1228
|
lastShellOutcome: null
|
|
1148
1229
|
};
|
|
1149
|
-
if (!
|
|
1150
|
-
const lines =
|
|
1230
|
+
if (!existsSync9(file)) return result;
|
|
1231
|
+
const lines = readFileSync8(file, "utf8").split("\n").filter(Boolean);
|
|
1151
1232
|
for (const line of lines) {
|
|
1152
1233
|
const event = safeJson(line);
|
|
1153
1234
|
if (!event) continue;
|
|
@@ -2173,7 +2254,7 @@ function hasLiveWorkerForTask(runId, taskId) {
|
|
|
2173
2254
|
}
|
|
2174
2255
|
|
|
2175
2256
|
// src/supervisor.ts
|
|
2176
|
-
import { existsSync as
|
|
2257
|
+
import { existsSync as existsSync13, mkdirSync as mkdirSync3 } from "node:fs";
|
|
2177
2258
|
import path14 from "node:path";
|
|
2178
2259
|
|
|
2179
2260
|
// src/prompt.ts
|
|
@@ -2194,7 +2275,7 @@ function buildPrompt(input) {
|
|
|
2194
2275
|
input.planId ? `Active planId: ${input.planId}${input.taskId ? ` \xB7 taskId: ${input.taskId}` : ""}` : "No planId on this worker \u2014 still emit progress when you touch plan-scoped work."
|
|
2195
2276
|
];
|
|
2196
2277
|
const mergeGateLines = compact ? [
|
|
2197
|
-
"Merge-gate cost control: run `node scripts/verify-pr-local.mjs --
|
|
2278
|
+
"Merge-gate cost control: run `node scripts/agent-os-pr-merge-gate.mjs --pr <url> --agent-os-id <id>` (or `verify-pr-local.mjs --from-pr` + `collect-pr-vercel-evidence.mjs` + POST pr-merge-gate/refresh) before any GitHub Actions run; request merge-gate only via refresh then POST pr-merge-gate/request-run (one Actions run per PR head unless human approves extra)."
|
|
2198
2279
|
] : [
|
|
2199
2280
|
"GitHub Actions merge-gate cost control (Kynver/Hermes PRs):",
|
|
2200
2281
|
"- Prefer local cached package verification (`node scripts/verify-pr-local.mjs --emit-json`) and Vercel preview evidence before GitHub Actions.",
|
|
@@ -2221,7 +2302,7 @@ function buildPrompt(input) {
|
|
|
2221
2302
|
"After each major step, append one JSON line to the heartbeat file with fields: ts, phase, summary, changedFiles, blocker.",
|
|
2222
2303
|
"Final response must include files changed, verification commands, and unresolved risks.",
|
|
2223
2304
|
"Structured final result (recommended): record completion as JSON with summary, laneExpertise { whatChanged, why, files, prUrls, verification, risks, blockers, lessonsLearned, laneGuidance }, and targetPrReconciliation [{ prUrl, outcome: merged|skipped|blocked, mergeCommit?, reason? }] for every target PR on landing-only tasks.",
|
|
2224
|
-
"Completion handoff (required): before you stop, ensure the harness records a final result \u2014 summarize outcome in your last message and append a heartbeat line with phase `complete`. If you leave uncommitted changes or committed work without a PR, the orchestrator blocks completion until a GitHub PR exists (or you discard/commit cleanly). Exiting with only dirty files and no PR routes to salvage review, not production review.",
|
|
2305
|
+
"Completion handoff (required): before you stop, ensure the harness records a final result \u2014 summarize outcome in your last message and append a heartbeat line with phase `complete`. If you leave uncommitted changes or committed work without a PR, the orchestrator blocks completion until a GitHub PR exists (or you discard/commit cleanly). One-off helper scripts must be removed (`kynver worker discard-disposable --path <file>`) or committed before completion \u2014 maintenance/board-drain workers are not exempt. Exiting with only dirty files and no PR routes to salvage review, not production review.",
|
|
2225
2306
|
"PR-ready handoff: for substantial implementation work, commit, push, and open a GitHub PR (draft OK) on your branch before finishing \u2014 or rely on the harness to run `gh pr create` at completion when `gh` is authenticated.",
|
|
2226
2307
|
"Expert review / production-review workers (Dalton/Lorentz, plan-review-task, scheduledJob reviewer children): do NOT open new implementation PRs \u2014 review the parent task's existing PR and record reviewVerdict in finalResult; landing-contract targetPrReconciliation does not apply.",
|
|
2227
2308
|
"Worker resource guard: do not run full monorepo verification (`npm run typecheck`, `npm run build`, or equivalent) from this worker lane unless an operator explicitly requests it. Use targeted checks for touched paths and rely on CI/operator lanes for heavy gates.",
|
|
@@ -2243,12 +2324,12 @@ function buildPrompt(input) {
|
|
|
2243
2324
|
}
|
|
2244
2325
|
|
|
2245
2326
|
// src/providers/cursor.ts
|
|
2246
|
-
import { closeSync as closeSync2, existsSync as
|
|
2327
|
+
import { closeSync as closeSync2, existsSync as existsSync11, openSync as openSync2 } from "node:fs";
|
|
2247
2328
|
import { spawn as spawn2 } from "node:child_process";
|
|
2248
2329
|
import path10 from "node:path";
|
|
2249
2330
|
|
|
2250
2331
|
// src/providers/cursor-windows.ts
|
|
2251
|
-
import { existsSync as
|
|
2332
|
+
import { existsSync as existsSync10, readdirSync as readdirSync3 } from "node:fs";
|
|
2252
2333
|
import path9 from "node:path";
|
|
2253
2334
|
var CURSOR_VERSION_DIR = /^\d{4}\.\d{1,2}\.\d{1,2}-[a-f0-9]+$/i;
|
|
2254
2335
|
function parseCursorVersionSortKey(versionName) {
|
|
@@ -2261,7 +2342,7 @@ function parseCursorVersionSortKey(versionName) {
|
|
|
2261
2342
|
}
|
|
2262
2343
|
function pickLatestCursorVersionDir(agentRoot) {
|
|
2263
2344
|
const versionsRoot = path9.join(agentRoot, "versions");
|
|
2264
|
-
if (!
|
|
2345
|
+
if (!existsSync10(versionsRoot)) return null;
|
|
2265
2346
|
let bestDir = null;
|
|
2266
2347
|
let bestKey = -1;
|
|
2267
2348
|
for (const entry of readdirSync3(versionsRoot, { withFileTypes: true })) {
|
|
@@ -2277,14 +2358,14 @@ function resolveWindowsCursorBundled(agentRoot) {
|
|
|
2277
2358
|
const root = agentRoot?.trim() || path9.join(process.env.LOCALAPPDATA || "", "cursor-agent");
|
|
2278
2359
|
const directNode = path9.join(root, "node.exe");
|
|
2279
2360
|
const directIndex = path9.join(root, "index.js");
|
|
2280
|
-
if (
|
|
2361
|
+
if (existsSync10(directNode) && existsSync10(directIndex)) {
|
|
2281
2362
|
return { nodeExe: directNode, indexJs: directIndex, versionDir: root };
|
|
2282
2363
|
}
|
|
2283
2364
|
const versionDir = pickLatestCursorVersionDir(root);
|
|
2284
2365
|
if (!versionDir) return null;
|
|
2285
2366
|
const nodeExe = path9.join(versionDir, "node.exe");
|
|
2286
2367
|
const indexJs = path9.join(versionDir, "index.js");
|
|
2287
|
-
if (!
|
|
2368
|
+
if (!existsSync10(nodeExe) || !existsSync10(indexJs)) return null;
|
|
2288
2369
|
return { nodeExe, indexJs, versionDir };
|
|
2289
2370
|
}
|
|
2290
2371
|
|
|
@@ -2302,7 +2383,7 @@ function bundledSpawnTarget(nodeExe, indexJs, versionDir) {
|
|
|
2302
2383
|
function resolveCursorSpawn(agentBin) {
|
|
2303
2384
|
if (process.platform === "win32") {
|
|
2304
2385
|
const isCursorWrapper = /\.(cmd|bat)$/i.test(agentBin);
|
|
2305
|
-
const isBundledNode = /node\.exe$/i.test(agentBin) &&
|
|
2386
|
+
const isBundledNode = /node\.exe$/i.test(agentBin) && existsSync11(path10.join(path10.dirname(agentBin), "index.js"));
|
|
2306
2387
|
const isDefaultShim = agentBin === "agent";
|
|
2307
2388
|
if (isCursorWrapper || isBundledNode || isDefaultShim) {
|
|
2308
2389
|
const bundled = isCursorWrapper ? resolveWindowsCursorBundled(path10.dirname(agentBin)) : isBundledNode ? {
|
|
@@ -2329,7 +2410,7 @@ function resolveAgentBin() {
|
|
|
2329
2410
|
);
|
|
2330
2411
|
if (bundled) return bundled.nodeExe;
|
|
2331
2412
|
const localAgent = path10.join(process.env.LOCALAPPDATA || "", "cursor-agent", "agent.cmd");
|
|
2332
|
-
if (
|
|
2413
|
+
if (existsSync11(localAgent)) return localAgent;
|
|
2333
2414
|
}
|
|
2334
2415
|
return "agent";
|
|
2335
2416
|
}
|
|
@@ -2412,7 +2493,7 @@ function resolveWorkerProvider(name) {
|
|
|
2412
2493
|
|
|
2413
2494
|
// src/auto-complete.ts
|
|
2414
2495
|
import { spawn as spawn3 } from "node:child_process";
|
|
2415
|
-
import { existsSync as
|
|
2496
|
+
import { existsSync as existsSync12, openSync as openSync3, closeSync as closeSync3 } from "node:fs";
|
|
2416
2497
|
import path13 from "node:path";
|
|
2417
2498
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
2418
2499
|
|
|
@@ -2911,6 +2992,133 @@ function ensurePrReadyHandoff(input, exec = defaultPrHandoffExec) {
|
|
|
2911
2992
|
};
|
|
2912
2993
|
}
|
|
2913
2994
|
|
|
2995
|
+
// src/material-worktree-changes.ts
|
|
2996
|
+
function materialWorktreeChanges(changedFiles) {
|
|
2997
|
+
return changedFiles.filter((line) => {
|
|
2998
|
+
const trimmed = line.trim();
|
|
2999
|
+
const pathPart = trimmed.startsWith("??") ? trimmed.slice(2).trim() : trimmed.length > 3 ? trimmed.slice(3).trim() : trimmed;
|
|
3000
|
+
return pathPart !== "node_modules" && !pathPart.startsWith("node_modules/");
|
|
3001
|
+
});
|
|
3002
|
+
}
|
|
3003
|
+
function pathFromGitStatusLine(line) {
|
|
3004
|
+
const trimmed = line.trim();
|
|
3005
|
+
if (trimmed.startsWith("??")) return trimmed.slice(2).trim();
|
|
3006
|
+
if (trimmed.length > 3 && /^[ MADRCU?!]{2} /.test(trimmed.slice(0, 3))) {
|
|
3007
|
+
return trimmed.slice(3).trim();
|
|
3008
|
+
}
|
|
3009
|
+
return trimmed;
|
|
3010
|
+
}
|
|
3011
|
+
|
|
3012
|
+
// src/disposable-artifacts.ts
|
|
3013
|
+
function stringList(value) {
|
|
3014
|
+
if (!Array.isArray(value)) return [];
|
|
3015
|
+
return value.map((item) => typeof item === "string" ? item.trim() : "").filter(Boolean);
|
|
3016
|
+
}
|
|
3017
|
+
function asRecord2(value) {
|
|
3018
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
3019
|
+
}
|
|
3020
|
+
function extractDisposableArtifactsRemoved(finalResult) {
|
|
3021
|
+
const record = asRecord2(finalResult);
|
|
3022
|
+
if (!record) return [];
|
|
3023
|
+
const nested = asRecord2(record.worktreeHandoff);
|
|
3024
|
+
const fromNested = stringList(nested?.disposableArtifactsRemoved);
|
|
3025
|
+
if (fromNested.length) return fromNested;
|
|
3026
|
+
return stringList(record.disposableArtifactsRemoved);
|
|
3027
|
+
}
|
|
3028
|
+
function normalizeRelativePath(value) {
|
|
3029
|
+
return value.replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/+$/, "");
|
|
3030
|
+
}
|
|
3031
|
+
function dirtyPathsCoveredByDisposableRemoval(changedFiles, removed) {
|
|
3032
|
+
const material = materialWorktreeChanges(changedFiles);
|
|
3033
|
+
if (material.length === 0) return true;
|
|
3034
|
+
if (removed.length === 0) return false;
|
|
3035
|
+
const removedSet = new Set(removed.map((p) => normalizeRelativePath(p)));
|
|
3036
|
+
return material.every((line) => {
|
|
3037
|
+
const path44 = normalizeRelativePath(pathFromGitStatusLine(line));
|
|
3038
|
+
return removedSet.has(path44);
|
|
3039
|
+
});
|
|
3040
|
+
}
|
|
3041
|
+
|
|
3042
|
+
// src/worktree-completion-handoff.ts
|
|
3043
|
+
function trimOrNull5(value) {
|
|
3044
|
+
if (typeof value !== "string") return null;
|
|
3045
|
+
const t = value.trim();
|
|
3046
|
+
return t.length ? t : null;
|
|
3047
|
+
}
|
|
3048
|
+
function stringList2(value) {
|
|
3049
|
+
if (!Array.isArray(value)) return [];
|
|
3050
|
+
return value.map((item) => typeof item === "string" ? item.trim() : "").filter(Boolean);
|
|
3051
|
+
}
|
|
3052
|
+
function hasFinalResult4(value) {
|
|
3053
|
+
if (value === void 0 || value === null) return false;
|
|
3054
|
+
if (typeof value === "string") return value.trim().length > 0;
|
|
3055
|
+
if (typeof value === "boolean") return value;
|
|
3056
|
+
if (Array.isArray(value)) return value.length > 0;
|
|
3057
|
+
if (typeof value === "object") return Object.keys(value).length > 0;
|
|
3058
|
+
return true;
|
|
3059
|
+
}
|
|
3060
|
+
function mergedDisposableRemoved(input) {
|
|
3061
|
+
const fromWorker = stringList2(input.disposableArtifactsRemoved);
|
|
3062
|
+
const fromResult = extractDisposableArtifactsRemoved(input.finalResult);
|
|
3063
|
+
return [.../* @__PURE__ */ new Set([...fromWorker, ...fromResult])];
|
|
3064
|
+
}
|
|
3065
|
+
function assessWorktreeCompletionHandoff(input) {
|
|
3066
|
+
const rawDirty = stringList2(input.changedFiles);
|
|
3067
|
+
const materialDirty = materialWorktreeChanges(rawDirty);
|
|
3068
|
+
const removed = mergedDisposableRemoved(input);
|
|
3069
|
+
const effectivelyClean = materialDirty.length === 0 || dirtyPathsCoveredByDisposableRemoval(rawDirty, removed);
|
|
3070
|
+
if (trimOrNull5(input.prUrl)) {
|
|
3071
|
+
if (!effectivelyClean) {
|
|
3072
|
+
return {
|
|
3073
|
+
allowed: false,
|
|
3074
|
+
state: "dirty_worktree",
|
|
3075
|
+
materialDirtyCount: materialDirty.length,
|
|
3076
|
+
detail: `Worktree has ${materialDirty.length} uncommitted change(s) with a PR attached; commit or discard before completing`
|
|
3077
|
+
};
|
|
3078
|
+
}
|
|
3079
|
+
return { allowed: true, state: "pr_handoff", materialDirtyCount: 0 };
|
|
3080
|
+
}
|
|
3081
|
+
if (trimOrNull5(input.headCommit)) {
|
|
3082
|
+
if (!effectivelyClean) {
|
|
3083
|
+
return {
|
|
3084
|
+
allowed: false,
|
|
3085
|
+
state: "dirty_worktree",
|
|
3086
|
+
materialDirtyCount: materialDirty.length,
|
|
3087
|
+
detail: `Worktree has ${materialDirty.length} uncommitted change(s) on top of a commit; commit or discard before completing`
|
|
3088
|
+
};
|
|
3089
|
+
}
|
|
3090
|
+
return { allowed: true, state: "commit_handoff", materialDirtyCount: 0 };
|
|
3091
|
+
}
|
|
3092
|
+
if (trimOrNull5(input.artifactBundlePath) || trimOrNull5(input.patchPath)) {
|
|
3093
|
+
if (!effectivelyClean) {
|
|
3094
|
+
return {
|
|
3095
|
+
allowed: false,
|
|
3096
|
+
state: "dirty_worktree",
|
|
3097
|
+
materialDirtyCount: materialDirty.length,
|
|
3098
|
+
detail: `Worktree has ${materialDirty.length} uncommitted change(s) with a patch/bundle handoff; clean the tree before completing`
|
|
3099
|
+
};
|
|
3100
|
+
}
|
|
3101
|
+
return { allowed: true, state: "commit_handoff", materialDirtyCount: 0 };
|
|
3102
|
+
}
|
|
3103
|
+
if (effectivelyClean) {
|
|
3104
|
+
return { allowed: true, state: "clean", materialDirtyCount: 0 };
|
|
3105
|
+
}
|
|
3106
|
+
if (hasFinalResult4(input.finalResult)) {
|
|
3107
|
+
return {
|
|
3108
|
+
allowed: false,
|
|
3109
|
+
state: "dirty_worktree_no_pr",
|
|
3110
|
+
materialDirtyCount: materialDirty.length,
|
|
3111
|
+
detail: `Worktree has ${materialDirty.length} uncommitted change(s) with no commit or PR; commit, open a PR, discard, or remove one-off files via \`kynver worker discard-disposable\` before completing`
|
|
3112
|
+
};
|
|
3113
|
+
}
|
|
3114
|
+
return {
|
|
3115
|
+
allowed: false,
|
|
3116
|
+
state: "dirty_worktree",
|
|
3117
|
+
materialDirtyCount: materialDirty.length,
|
|
3118
|
+
detail: `Worktree has ${materialDirty.length} uncommitted change(s) with no final result`
|
|
3119
|
+
};
|
|
3120
|
+
}
|
|
3121
|
+
|
|
2914
3122
|
// src/worker-lifecycle.ts
|
|
2915
3123
|
import path11 from "node:path";
|
|
2916
3124
|
var TASK_LEFT_RUNNING = /* @__PURE__ */ new Set([
|
|
@@ -3007,7 +3215,7 @@ function completionErrorText(parsed) {
|
|
|
3007
3215
|
}
|
|
3008
3216
|
return void 0;
|
|
3009
3217
|
}
|
|
3010
|
-
function
|
|
3218
|
+
function asRecord3(value) {
|
|
3011
3219
|
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
3012
3220
|
}
|
|
3013
3221
|
function asString2(value) {
|
|
@@ -3084,26 +3292,48 @@ async function tryCompleteWorker(args) {
|
|
|
3084
3292
|
if (worker.localOnly) {
|
|
3085
3293
|
return { ok: true, skipped: true, reason: "local-only-worker" };
|
|
3086
3294
|
}
|
|
3295
|
+
const headCommit = status.gitAncestry.headIsAncestorOfBase === false && status.gitAncestry.head ? status.gitAncestry.head : status.headCommit;
|
|
3296
|
+
const handoff = assessWorktreeCompletionHandoff({
|
|
3297
|
+
changedFiles: status.changedFiles,
|
|
3298
|
+
finalResult: status.finalResult,
|
|
3299
|
+
prUrl: status.prUrl,
|
|
3300
|
+
headCommit,
|
|
3301
|
+
disposableArtifactsRemoved: worker.disposableArtifactsRemoved
|
|
3302
|
+
});
|
|
3303
|
+
if (!handoff.allowed) {
|
|
3304
|
+
const reason2 = handoff.detail ?? `worktree completion blocked (${handoff.state})`;
|
|
3305
|
+
persistCompletionBlocker(worker, reason2);
|
|
3306
|
+
return {
|
|
3307
|
+
ok: false,
|
|
3308
|
+
reason: reason2,
|
|
3309
|
+
nextAction: "Clean the worktree (commit, open a PR, or `kynver worker discard-disposable --path <file>`), then rerun `kynver worker complete`.",
|
|
3310
|
+
completionBlocked: true
|
|
3311
|
+
};
|
|
3312
|
+
}
|
|
3087
3313
|
const skipPrHandoff = args.skipPrHandoff === true || args.skipPrHandoff === "true";
|
|
3088
3314
|
if (!skipPrHandoff && worker.dispatched && taskId) {
|
|
3089
|
-
const
|
|
3090
|
-
if (!
|
|
3091
|
-
persistCompletionBlocker(worker,
|
|
3315
|
+
const handoff2 = ensurePrReadyHandoff({ worker, run, status });
|
|
3316
|
+
if (!handoff2.ok) {
|
|
3317
|
+
persistCompletionBlocker(worker, handoff2.reason);
|
|
3092
3318
|
return {
|
|
3093
3319
|
ok: false,
|
|
3094
|
-
reason:
|
|
3095
|
-
nextAction:
|
|
3320
|
+
reason: handoff2.reason,
|
|
3321
|
+
nextAction: handoff2.nextAction,
|
|
3096
3322
|
completionBlocked: true
|
|
3097
3323
|
};
|
|
3098
3324
|
}
|
|
3099
|
-
if (
|
|
3100
|
-
status = applyPrHandoffToStatus(status,
|
|
3325
|
+
if (handoff2.prUrl || handoff2.headCommit) {
|
|
3326
|
+
status = applyPrHandoffToStatus(status, handoff2);
|
|
3101
3327
|
}
|
|
3102
3328
|
}
|
|
3103
3329
|
const base = resolveBaseUrl(args.baseUrl ? String(args.baseUrl) : void 0);
|
|
3104
3330
|
const explicitSecret = args.secret ? String(args.secret) : void 0;
|
|
3105
3331
|
let secret = await resolveCallbackSecretWithMint(explicitSecret, agentOsId, { baseUrl: base });
|
|
3106
3332
|
const url = `${base}/api/agent-os/by-id/${encodeURIComponent(agentOsId)}/harness/completion`;
|
|
3333
|
+
const statusPayload = { ...status };
|
|
3334
|
+
if (worker.disposableArtifactsRemoved?.length) {
|
|
3335
|
+
statusPayload.disposableArtifactsRemoved = worker.disposableArtifactsRemoved;
|
|
3336
|
+
}
|
|
3107
3337
|
const body = {
|
|
3108
3338
|
source: "kynver-harness",
|
|
3109
3339
|
agentOsId,
|
|
@@ -3112,10 +3342,11 @@ async function tryCompleteWorker(args) {
|
|
|
3112
3342
|
taskId,
|
|
3113
3343
|
startedAt: worker.startedAt,
|
|
3114
3344
|
finishedAt: status.lastActivityAt || (/* @__PURE__ */ new Date()).toISOString(),
|
|
3115
|
-
status,
|
|
3345
|
+
status: statusPayload,
|
|
3116
3346
|
workerInjection: {
|
|
3117
3347
|
instructionPolicyFingerprint: worker.instructionPolicyFingerprint ?? null,
|
|
3118
3348
|
instructionPolicyEvidence: worker.instructionPolicyEvidence ?? null,
|
|
3349
|
+
memoryQualityCapture: worker.memoryQualityCapture ?? null,
|
|
3119
3350
|
policyAt: worker.instructionPolicyEvidence && typeof worker.instructionPolicyEvidence === "object" && "policyAt" in worker.instructionPolicyEvidence && typeof worker.instructionPolicyEvidence.policyAt === "string" ? worker.instructionPolicyEvidence.policyAt : null,
|
|
3120
3351
|
personaSlug: worker.personaSlug ?? null,
|
|
3121
3352
|
personaEvidence: worker.personaEvidence ?? null
|
|
@@ -3256,8 +3487,8 @@ function buildRunBoard(runId) {
|
|
|
3256
3487
|
const completionBlocker = typeof rawBlocker === "string" && rawBlocker ? rawBlocker : void 0;
|
|
3257
3488
|
const boardStatus = completionBlocker ? "blocked" : status.status;
|
|
3258
3489
|
const boardAttention = completionBlocker ? "blocked" : status.attention.state;
|
|
3259
|
-
const completionResponse =
|
|
3260
|
-
const completionTask =
|
|
3490
|
+
const completionResponse = asRecord3(worker.completionResponse);
|
|
3491
|
+
const completionTask = asRecord3(completionResponse?.task);
|
|
3261
3492
|
const completionOutcome = asString2(completionResponse?.outcome);
|
|
3262
3493
|
const completionRouteStatus = asString2(completionResponse?.status);
|
|
3263
3494
|
const completionWarnings = Array.isArray(completionResponse?.warnings) ? completionResponse.warnings.filter((w) => typeof w === "string" && w.trim().length > 0) : [];
|
|
@@ -3538,7 +3769,7 @@ function resolveDefaultCliPath() {
|
|
|
3538
3769
|
}
|
|
3539
3770
|
function spawnCompletionSidecar(opts) {
|
|
3540
3771
|
const cliPath = opts.cliPath ?? resolveDefaultCliPath();
|
|
3541
|
-
if (!
|
|
3772
|
+
if (!existsSync12(cliPath)) return void 0;
|
|
3542
3773
|
const logPath = path13.join(opts.workerDir, "auto-complete.log");
|
|
3543
3774
|
let logFd;
|
|
3544
3775
|
try {
|
|
@@ -3625,7 +3856,7 @@ function spawnWorkerProcess(run, opts) {
|
|
|
3625
3856
|
mkdirSync3(workerDir, { recursive: true });
|
|
3626
3857
|
const worktreePath = path14.join(worktreesDir, run.id, name);
|
|
3627
3858
|
const branch = opts.branch || `agent/${run.id}/${name}`;
|
|
3628
|
-
if (
|
|
3859
|
+
if (existsSync13(worktreePath)) throw new Error(`worktree path already exists: ${worktreePath}`);
|
|
3629
3860
|
git(run.repo, ["fetch", "origin", "--prune"], { allowFailure: true });
|
|
3630
3861
|
git(run.repo, ["worktree", "add", "-b", branch, worktreePath, run.baseCommit], { throwError: true });
|
|
3631
3862
|
const stdoutPath = path14.join(workerDir, "stdout.jsonl");
|
|
@@ -3681,6 +3912,7 @@ function spawnWorkerProcess(run, opts) {
|
|
|
3681
3912
|
...opts.planId ? { planId: String(opts.planId) } : {},
|
|
3682
3913
|
...opts.instructionPolicyFingerprint ? { instructionPolicyFingerprint: String(opts.instructionPolicyFingerprint) } : {},
|
|
3683
3914
|
...opts.instructionPolicyEvidence ? { instructionPolicyEvidence: opts.instructionPolicyEvidence } : {},
|
|
3915
|
+
...opts.memoryQualityCapture ? { memoryQualityCapture: opts.memoryQualityCapture } : {},
|
|
3684
3916
|
...opts.personaSlug ? { personaSlug: String(opts.personaSlug) } : {},
|
|
3685
3917
|
...opts.personaEvidence ? { personaEvidence: opts.personaEvidence } : {},
|
|
3686
3918
|
...opts.leaseOwner ? { leaseOwner: String(opts.leaseOwner) } : {},
|
|
@@ -4015,8 +4247,8 @@ function isTmpOnlyPath(filePath) {
|
|
|
4015
4247
|
|
|
4016
4248
|
// src/plan-persist/outbox-store.ts
|
|
4017
4249
|
import {
|
|
4018
|
-
existsSync as
|
|
4019
|
-
readFileSync as
|
|
4250
|
+
existsSync as existsSync15,
|
|
4251
|
+
readFileSync as readFileSync9,
|
|
4020
4252
|
renameSync,
|
|
4021
4253
|
readdirSync as readdirSync4,
|
|
4022
4254
|
writeFileSync as writeFileSync3,
|
|
@@ -4042,9 +4274,9 @@ function findOutboxByIdempotencyKey(key) {
|
|
|
4042
4274
|
return null;
|
|
4043
4275
|
}
|
|
4044
4276
|
function readOutboxItem(jsonPath) {
|
|
4045
|
-
if (!
|
|
4277
|
+
if (!existsSync15(jsonPath)) return null;
|
|
4046
4278
|
try {
|
|
4047
|
-
return JSON.parse(
|
|
4279
|
+
return JSON.parse(readFileSync9(jsonPath, "utf8"));
|
|
4048
4280
|
} catch {
|
|
4049
4281
|
return null;
|
|
4050
4282
|
}
|
|
@@ -4052,7 +4284,7 @@ function readOutboxItem(jsonPath) {
|
|
|
4052
4284
|
function readOutboxBody(item) {
|
|
4053
4285
|
const { outboxDir } = ensurePlanOutboxDirs();
|
|
4054
4286
|
const bodyFile = path16.join(outboxDir, item.bodyPath);
|
|
4055
|
-
return
|
|
4287
|
+
return readFileSync9(bodyFile, "utf8");
|
|
4056
4288
|
}
|
|
4057
4289
|
function writeOutboxItem(input, opts) {
|
|
4058
4290
|
const { outboxDir } = ensurePlanOutboxDirs();
|
|
@@ -4106,8 +4338,8 @@ function archiveOutboxItem(item) {
|
|
|
4106
4338
|
const bodySrc = path16.join(outboxDir, item.bodyPath);
|
|
4107
4339
|
const jsonDst = path16.join(archiveDir, `${item.id}.json`);
|
|
4108
4340
|
const bodyDst = path16.join(archiveDir, item.bodyPath);
|
|
4109
|
-
if (
|
|
4110
|
-
if (
|
|
4341
|
+
if (existsSync15(jsonSrc)) renameSync(jsonSrc, jsonDst);
|
|
4342
|
+
if (existsSync15(bodySrc)) renameSync(bodySrc, bodyDst);
|
|
4111
4343
|
}
|
|
4112
4344
|
function outboxItemPaths(item) {
|
|
4113
4345
|
const { outboxDir } = ensurePlanOutboxDirs();
|
|
@@ -4376,10 +4608,12 @@ function readHarnessWorkerContext(decision) {
|
|
|
4376
4608
|
const personaSlug = typeof ctx.personaEvidence === "object" && ctx.personaEvidence && typeof ctx.personaEvidence.injectedPersonaSlug === "string" ? ctx.personaEvidence.injectedPersonaSlug : null;
|
|
4377
4609
|
const personaEvidence = ctx.personaEvidence && typeof ctx.personaEvidence === "object" ? ctx.personaEvidence : null;
|
|
4378
4610
|
const personaInjectionReady = ctx.personaInjectionReady === true;
|
|
4611
|
+
const memoryQualityCapture = ctx.memoryQualityCapture && typeof ctx.memoryQualityCapture === "object" ? ctx.memoryQualityCapture : null;
|
|
4379
4612
|
return {
|
|
4380
4613
|
instructionPolicyMarkdown: markdown,
|
|
4381
4614
|
instructionPolicyFingerprint: fingerprint,
|
|
4382
4615
|
instructionPolicyEvidence: evidence,
|
|
4616
|
+
memoryQualityCapture,
|
|
4383
4617
|
personaMarkdown,
|
|
4384
4618
|
personaSlug,
|
|
4385
4619
|
personaEvidence,
|
|
@@ -4568,6 +4802,7 @@ async function dispatchRun(args) {
|
|
|
4568
4802
|
instructionPolicyMarkdown: harnessContext?.instructionPolicyMarkdown ?? null,
|
|
4569
4803
|
instructionPolicyFingerprint: harnessContext?.instructionPolicyFingerprint ?? null,
|
|
4570
4804
|
instructionPolicyEvidence: harnessContext?.instructionPolicyEvidence ?? null,
|
|
4805
|
+
memoryQualityCapture: harnessContext?.memoryQualityCapture ?? null,
|
|
4571
4806
|
personaMarkdown: harnessContext?.personaMarkdown ?? null,
|
|
4572
4807
|
personaSlug: harnessContext?.personaSlug ?? expectedPersona,
|
|
4573
4808
|
personaEvidence: harnessContext?.personaEvidence ?? null,
|
|
@@ -4657,8 +4892,175 @@ function redactHarness(text, secret) {
|
|
|
4657
4892
|
return out;
|
|
4658
4893
|
}
|
|
4659
4894
|
|
|
4895
|
+
// src/db-credential-resolver.ts
|
|
4896
|
+
import { execFileSync, spawnSync as spawnSync3 } from "node:child_process";
|
|
4897
|
+
import * as fs from "node:fs";
|
|
4898
|
+
import * as path19 from "node:path";
|
|
4899
|
+
|
|
4900
|
+
// src/db-url-hint.ts
|
|
4901
|
+
function safeDatabaseUrlHint(url) {
|
|
4902
|
+
if (!url?.trim()) return "(not set)";
|
|
4903
|
+
try {
|
|
4904
|
+
const u = new URL(url);
|
|
4905
|
+
const name = (u.pathname || "").replace(/^\//, "").split("?")[0] || "(no database name)";
|
|
4906
|
+
return `${u.hostname} / ${name}`;
|
|
4907
|
+
} catch {
|
|
4908
|
+
return "(invalid URL)";
|
|
4909
|
+
}
|
|
4910
|
+
}
|
|
4911
|
+
|
|
4912
|
+
// src/db-credential-resolver.ts
|
|
4913
|
+
var PRODUCTION_ENV_KEYS = [
|
|
4914
|
+
"KYNVER_PRODUCTION_DATABASE_URL",
|
|
4915
|
+
"PRODUCTION_DATABASE_URL"
|
|
4916
|
+
];
|
|
4917
|
+
var ENV_FILE_KEYS = [...PRODUCTION_ENV_KEYS];
|
|
4918
|
+
function readEnvValue(env, key) {
|
|
4919
|
+
const value = env[key]?.trim();
|
|
4920
|
+
return value || void 0;
|
|
4921
|
+
}
|
|
4922
|
+
function readProductionDbKeysFromEnvFile(envPath) {
|
|
4923
|
+
if (!fs.existsSync(envPath)) return {};
|
|
4924
|
+
const out = {};
|
|
4925
|
+
const allowed = new Set(ENV_FILE_KEYS);
|
|
4926
|
+
for (const line of fs.readFileSync(envPath, "utf8").split("\n")) {
|
|
4927
|
+
const m = line.match(/^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/);
|
|
4928
|
+
if (!m || !allowed.has(m[1])) continue;
|
|
4929
|
+
const key = m[1];
|
|
4930
|
+
out[key] = m[2].replace(/^["']|["']$/g, "").trim();
|
|
4931
|
+
}
|
|
4932
|
+
return out;
|
|
4933
|
+
}
|
|
4934
|
+
function resolveFromProcessEnv(env) {
|
|
4935
|
+
for (const key of PRODUCTION_ENV_KEYS) {
|
|
4936
|
+
const value = readEnvValue(env, key);
|
|
4937
|
+
if (value) {
|
|
4938
|
+
return {
|
|
4939
|
+
ok: true,
|
|
4940
|
+
databaseUrl: value,
|
|
4941
|
+
source: `env:${key}`,
|
|
4942
|
+
hint: safeDatabaseUrlHint(value)
|
|
4943
|
+
};
|
|
4944
|
+
}
|
|
4945
|
+
}
|
|
4946
|
+
return null;
|
|
4947
|
+
}
|
|
4948
|
+
function resolveFromEnvFile(cwd) {
|
|
4949
|
+
const envPath = path19.join(cwd, ".env");
|
|
4950
|
+
const parsed = readProductionDbKeysFromEnvFile(envPath);
|
|
4951
|
+
for (const key of ENV_FILE_KEYS) {
|
|
4952
|
+
const value = parsed[key]?.trim();
|
|
4953
|
+
if (value) {
|
|
4954
|
+
return {
|
|
4955
|
+
ok: true,
|
|
4956
|
+
databaseUrl: value,
|
|
4957
|
+
source: `env-file:${key}`,
|
|
4958
|
+
hint: safeDatabaseUrlHint(value)
|
|
4959
|
+
};
|
|
4960
|
+
}
|
|
4961
|
+
}
|
|
4962
|
+
return null;
|
|
4963
|
+
}
|
|
4964
|
+
function defaultNeonConnectionString(args) {
|
|
4965
|
+
const neonBin = process.platform === "win32" ? "neon.cmd" : "neon";
|
|
4966
|
+
const argv = [
|
|
4967
|
+
"connection-string",
|
|
4968
|
+
args.branch,
|
|
4969
|
+
"--prisma",
|
|
4970
|
+
"-o",
|
|
4971
|
+
"json",
|
|
4972
|
+
"--no-color",
|
|
4973
|
+
"--no-analytics"
|
|
4974
|
+
];
|
|
4975
|
+
if (args.projectId) argv.push("--project-id", args.projectId);
|
|
4976
|
+
if (args.databaseName) argv.push("--database-name", args.databaseName);
|
|
4977
|
+
if (args.roleName) argv.push("--role-name", args.roleName);
|
|
4978
|
+
if (args.pooled) argv.push("--pooled");
|
|
4979
|
+
let stdout;
|
|
4980
|
+
try {
|
|
4981
|
+
const result = spawnSync3(neonBin, argv, {
|
|
4982
|
+
encoding: "utf8",
|
|
4983
|
+
env: process.env,
|
|
4984
|
+
timeout: 3e4
|
|
4985
|
+
});
|
|
4986
|
+
if (result.status !== 0) return null;
|
|
4987
|
+
stdout = (result.stdout || "").trim();
|
|
4988
|
+
} catch {
|
|
4989
|
+
try {
|
|
4990
|
+
stdout = execFileSync("neonctl", argv, {
|
|
4991
|
+
encoding: "utf8",
|
|
4992
|
+
env: process.env,
|
|
4993
|
+
timeout: 3e4
|
|
4994
|
+
}).trim();
|
|
4995
|
+
} catch {
|
|
4996
|
+
return null;
|
|
4997
|
+
}
|
|
4998
|
+
}
|
|
4999
|
+
if (!stdout) return null;
|
|
5000
|
+
try {
|
|
5001
|
+
const parsed = JSON.parse(stdout);
|
|
5002
|
+
const fromField = typeof parsed.connection_uri === "string" && parsed.connection_uri || typeof parsed.connection_string === "string" && parsed.connection_string || typeof parsed.connectionString === "string" && parsed.connectionString;
|
|
5003
|
+
if (fromField) return fromField;
|
|
5004
|
+
} catch {
|
|
5005
|
+
}
|
|
5006
|
+
if (/^postgres(ql)?:\/\//i.test(stdout)) return stdout;
|
|
5007
|
+
return null;
|
|
5008
|
+
}
|
|
5009
|
+
function resolveFromNeonCli(env, neonFn) {
|
|
5010
|
+
const branch = readEnvValue(env, "KYNVER_NEON_BRANCH") || readEnvValue(env, "NEON_BRANCH") || "production";
|
|
5011
|
+
const url = neonFn({
|
|
5012
|
+
branch,
|
|
5013
|
+
projectId: readEnvValue(env, "KYNVER_NEON_PROJECT_ID") || readEnvValue(env, "NEON_PROJECT_ID"),
|
|
5014
|
+
databaseName: readEnvValue(env, "KYNVER_NEON_DATABASE_NAME") || readEnvValue(env, "NEON_DATABASE_NAME"),
|
|
5015
|
+
roleName: readEnvValue(env, "KYNVER_NEON_ROLE_NAME") || readEnvValue(env, "NEON_ROLE_NAME"),
|
|
5016
|
+
pooled: readEnvValue(env, "KYNVER_NEON_POOLED") === "1" || readEnvValue(env, "NEON_POOLED") === "1"
|
|
5017
|
+
});
|
|
5018
|
+
if (!url) return null;
|
|
5019
|
+
return {
|
|
5020
|
+
ok: true,
|
|
5021
|
+
databaseUrl: url,
|
|
5022
|
+
source: "neon-cli",
|
|
5023
|
+
hint: safeDatabaseUrlHint(url)
|
|
5024
|
+
};
|
|
5025
|
+
}
|
|
5026
|
+
function resolveProductionDatabaseUrl(options = {}) {
|
|
5027
|
+
const env = options.env ?? process.env;
|
|
5028
|
+
const cwd = options.cwd ?? process.cwd();
|
|
5029
|
+
const neonFn = options.neonConnectionString ?? defaultNeonConnectionString;
|
|
5030
|
+
const attempted = [];
|
|
5031
|
+
const fromNeon = resolveFromNeonCli(env, neonFn);
|
|
5032
|
+
attempted.push("neon-cli");
|
|
5033
|
+
if (fromNeon) return fromNeon;
|
|
5034
|
+
for (const key of PRODUCTION_ENV_KEYS) {
|
|
5035
|
+
attempted.push(`env:${key}`);
|
|
5036
|
+
}
|
|
5037
|
+
const fromEnv = resolveFromProcessEnv(env);
|
|
5038
|
+
if (fromEnv) return fromEnv;
|
|
5039
|
+
for (const key of ENV_FILE_KEYS) {
|
|
5040
|
+
attempted.push(`env-file:${key}`);
|
|
5041
|
+
}
|
|
5042
|
+
const fromFile = resolveFromEnvFile(cwd);
|
|
5043
|
+
if (fromFile) return fromFile;
|
|
5044
|
+
return {
|
|
5045
|
+
ok: false,
|
|
5046
|
+
reason: "No production database credentials found. Authenticate Neon CLI (`neon auth`), set KYNVER_NEON_* / NEON_* project hints, or provide PRODUCTION_DATABASE_URL / KYNVER_PRODUCTION_DATABASE_URL (process env or selective .env keys only).",
|
|
5047
|
+
attempted
|
|
5048
|
+
};
|
|
5049
|
+
}
|
|
5050
|
+
function applyProductionDatabaseToProcess(options = {}) {
|
|
5051
|
+
const resolved = resolveProductionDatabaseUrl(options);
|
|
5052
|
+
if (!resolved.ok) {
|
|
5053
|
+
throw new Error(resolved.reason);
|
|
5054
|
+
}
|
|
5055
|
+
process.env.DATABASE_URL = resolved.databaseUrl;
|
|
5056
|
+
console.error(
|
|
5057
|
+
`[kynver-cli] database target: ${resolved.hint} (source: ${resolved.source})`
|
|
5058
|
+
);
|
|
5059
|
+
return resolved;
|
|
5060
|
+
}
|
|
5061
|
+
|
|
4660
5062
|
// src/validate.ts
|
|
4661
|
-
import
|
|
5063
|
+
import path20 from "node:path";
|
|
4662
5064
|
var RUN_ID_RE = /^[a-z0-9][a-z0-9._-]{0,127}$/i;
|
|
4663
5065
|
var WORKER_NAME_RE = /^[a-z0-9][a-z0-9._-]{0,63}$/i;
|
|
4664
5066
|
function validateRunId(runId) {
|
|
@@ -4672,15 +5074,15 @@ function validateWorkerName(name) {
|
|
|
4672
5074
|
return trimmed;
|
|
4673
5075
|
}
|
|
4674
5076
|
function validateRepo(repo) {
|
|
4675
|
-
const resolved =
|
|
5077
|
+
const resolved = path20.resolve(repo);
|
|
4676
5078
|
if (resolved.includes("..")) throw new Error("repo path must not contain .. segments");
|
|
4677
5079
|
return resolved;
|
|
4678
5080
|
}
|
|
4679
5081
|
function validateOwnedPaths(repoRoot, ownedPaths) {
|
|
4680
5082
|
return ownedPaths.map((owned) => {
|
|
4681
|
-
const resolved =
|
|
4682
|
-
const rel =
|
|
4683
|
-
if (rel.startsWith("..") ||
|
|
5083
|
+
const resolved = path20.resolve(repoRoot, owned);
|
|
5084
|
+
const rel = path20.relative(repoRoot, resolved);
|
|
5085
|
+
if (rel.startsWith("..") || path20.isAbsolute(rel)) {
|
|
4684
5086
|
throw new Error(`owned path escapes repo: ${owned}`);
|
|
4685
5087
|
}
|
|
4686
5088
|
return resolved;
|
|
@@ -4692,13 +5094,13 @@ function validateTailLines(lines) {
|
|
|
4692
5094
|
}
|
|
4693
5095
|
|
|
4694
5096
|
// src/worktree.ts
|
|
4695
|
-
import { existsSync as
|
|
4696
|
-
import
|
|
5097
|
+
import { existsSync as existsSync17, mkdirSync as mkdirSync5 } from "node:fs";
|
|
5098
|
+
import path22 from "node:path";
|
|
4697
5099
|
|
|
4698
5100
|
// src/default-repo.ts
|
|
4699
|
-
import
|
|
5101
|
+
import path21 from "node:path";
|
|
4700
5102
|
function expandConfiguredRepo(value) {
|
|
4701
|
-
return
|
|
5103
|
+
return path21.resolve(resolveUserPath(value.trim()));
|
|
4702
5104
|
}
|
|
4703
5105
|
function fromConfigured(value, source, persistedInConfig) {
|
|
4704
5106
|
const trimmed = value?.trim();
|
|
@@ -4732,7 +5134,7 @@ function resolveDefaultRepo(opts = {}) {
|
|
|
4732
5134
|
function persistDefaultRepo(repo, existing) {
|
|
4733
5135
|
const config = {
|
|
4734
5136
|
...existing ?? loadUserConfig(),
|
|
4735
|
-
defaultRepo: redactHomePath(
|
|
5137
|
+
defaultRepo: redactHomePath(path21.resolve(repo))
|
|
4736
5138
|
};
|
|
4737
5139
|
saveUserConfig(config);
|
|
4738
5140
|
return config;
|
|
@@ -4778,7 +5180,7 @@ function createRun(args) {
|
|
|
4778
5180
|
ensureGitRepo(repo);
|
|
4779
5181
|
const id = args.id ? validateRunId(String(args.id)) : timestampSlug(String(args.name || "run"));
|
|
4780
5182
|
const dir = runDirectory(id);
|
|
4781
|
-
if (
|
|
5183
|
+
if (existsSync17(dir)) failExists(`run already exists: ${id}`);
|
|
4782
5184
|
mkdirSync5(dir, { recursive: true });
|
|
4783
5185
|
const base = String(args.base || "origin/main");
|
|
4784
5186
|
const baseCommit = git(repo, ["rev-parse", base]).trim();
|
|
@@ -4792,12 +5194,12 @@ function createRun(args) {
|
|
|
4792
5194
|
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4793
5195
|
workers: {}
|
|
4794
5196
|
};
|
|
4795
|
-
writeJson(
|
|
5197
|
+
writeJson(path22.join(dir, "run.json"), run);
|
|
4796
5198
|
console.log(JSON.stringify({ runId: id, runDir: dir, repo, base, baseCommit }, null, 2));
|
|
4797
5199
|
}
|
|
4798
5200
|
function listRuns() {
|
|
4799
5201
|
const { runsDir } = getPaths();
|
|
4800
|
-
const rows = listRunIds(runsDir).map((id) => readJson(
|
|
5202
|
+
const rows = listRunIds(runsDir).map((id) => readJson(path22.join(runDirectory(id), "run.json"), void 0)).filter(Boolean).map((run) => ({
|
|
4801
5203
|
id: run.id,
|
|
4802
5204
|
name: run.name,
|
|
4803
5205
|
status: run.status,
|
|
@@ -4812,7 +5214,7 @@ function failExists(message) {
|
|
|
4812
5214
|
}
|
|
4813
5215
|
|
|
4814
5216
|
// src/sweep.ts
|
|
4815
|
-
import
|
|
5217
|
+
import path23 from "node:path";
|
|
4816
5218
|
async function sweepRun(args) {
|
|
4817
5219
|
const pipeline = args.pipeline === true || args.pipeline === "true";
|
|
4818
5220
|
try {
|
|
@@ -4825,7 +5227,7 @@ async function sweepRun(args) {
|
|
|
4825
5227
|
const releasedLocalOrphans = [];
|
|
4826
5228
|
for (const name of Object.keys(run.workers || {})) {
|
|
4827
5229
|
const worker = readJson(
|
|
4828
|
-
|
|
5230
|
+
path23.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
|
|
4829
5231
|
void 0
|
|
4830
5232
|
);
|
|
4831
5233
|
if (!worker || !worker.dispatched || !worker.taskId) continue;
|
|
@@ -4867,69 +5269,182 @@ async function sweepRun(args) {
|
|
|
4867
5269
|
}
|
|
4868
5270
|
}
|
|
4869
5271
|
|
|
4870
|
-
// src/
|
|
4871
|
-
import {
|
|
4872
|
-
import
|
|
4873
|
-
|
|
4874
|
-
// src/pipeline-tick.ts
|
|
4875
|
-
import path32 from "node:path";
|
|
5272
|
+
// src/harness-storage-snapshot.ts
|
|
5273
|
+
import { existsSync as existsSync19, readdirSync as readdirSync6, statSync as statSync3 } from "node:fs";
|
|
5274
|
+
import path25 from "node:path";
|
|
4876
5275
|
|
|
4877
|
-
// src/
|
|
4878
|
-
|
|
4879
|
-
|
|
4880
|
-
|
|
4881
|
-
|
|
4882
|
-
|
|
4883
|
-
|
|
4884
|
-
|
|
4885
|
-
|
|
4886
|
-
|
|
4887
|
-
|
|
4888
|
-
|
|
4889
|
-
|
|
5276
|
+
// src/cleanup-dir-size.ts
|
|
5277
|
+
import { existsSync as existsSync18, readdirSync as readdirSync5, statSync as statSync2 } from "node:fs";
|
|
5278
|
+
import path24 from "node:path";
|
|
5279
|
+
function directorySizeBytes(root, maxEntries = 5e4) {
|
|
5280
|
+
if (!existsSync18(root)) return 0;
|
|
5281
|
+
let total = 0;
|
|
5282
|
+
let seen = 0;
|
|
5283
|
+
const stack = [root];
|
|
5284
|
+
while (stack.length > 0) {
|
|
5285
|
+
const current = stack.pop();
|
|
5286
|
+
let entries;
|
|
5287
|
+
try {
|
|
5288
|
+
entries = readdirSync5(current);
|
|
5289
|
+
} catch {
|
|
5290
|
+
continue;
|
|
5291
|
+
}
|
|
5292
|
+
for (const name of entries) {
|
|
5293
|
+
if (seen++ > maxEntries) return null;
|
|
5294
|
+
const full = path24.join(current, name);
|
|
5295
|
+
let st;
|
|
5296
|
+
try {
|
|
5297
|
+
st = statSync2(full);
|
|
5298
|
+
} catch {
|
|
5299
|
+
continue;
|
|
5300
|
+
}
|
|
5301
|
+
if (st.isDirectory()) stack.push(full);
|
|
5302
|
+
else total += st.size;
|
|
5303
|
+
}
|
|
5304
|
+
}
|
|
5305
|
+
return total;
|
|
4890
5306
|
}
|
|
4891
|
-
|
|
4892
|
-
|
|
4893
|
-
|
|
5307
|
+
|
|
5308
|
+
// src/harness-storage-snapshot.ts
|
|
5309
|
+
function harnessStorageSnapshot(opts = {}) {
|
|
5310
|
+
const harnessRoot = opts.harnessRoot ?? resolveHarnessRoot();
|
|
5311
|
+
const worktreesDir = path25.join(harnessRoot, "worktrees");
|
|
5312
|
+
const now = opts.now ?? Date.now();
|
|
5313
|
+
const scannedAt = new Date(now).toISOString();
|
|
5314
|
+
if (!existsSync19(worktreesDir)) {
|
|
5315
|
+
return {
|
|
5316
|
+
harnessRoot,
|
|
5317
|
+
worktreesDir,
|
|
5318
|
+
worktreesBytes: 0,
|
|
5319
|
+
runCount: 0,
|
|
5320
|
+
workerCount: 0,
|
|
5321
|
+
oldestRunAt: null,
|
|
5322
|
+
scannedAt
|
|
5323
|
+
};
|
|
4894
5324
|
}
|
|
4895
|
-
|
|
4896
|
-
|
|
4897
|
-
|
|
4898
|
-
|
|
4899
|
-
|
|
4900
|
-
|
|
4901
|
-
|
|
4902
|
-
|
|
4903
|
-
maxStarts: String(reviewBudget)
|
|
4904
|
-
});
|
|
4905
|
-
const reviewStarted = countDispatchStarts(review);
|
|
4906
|
-
const workSlots = workBudget + (reviewBudget - reviewStarted);
|
|
4907
|
-
if (workSlots <= 0) {
|
|
5325
|
+
let totalBytes = 0;
|
|
5326
|
+
let runCount = 0;
|
|
5327
|
+
let workerCount = 0;
|
|
5328
|
+
let oldestMs = null;
|
|
5329
|
+
let entries;
|
|
5330
|
+
try {
|
|
5331
|
+
entries = readdirSync6(worktreesDir, { withFileTypes: true });
|
|
5332
|
+
} catch {
|
|
4908
5333
|
return {
|
|
4909
|
-
|
|
4910
|
-
|
|
4911
|
-
|
|
5334
|
+
harnessRoot,
|
|
5335
|
+
worktreesDir,
|
|
5336
|
+
worktreesBytes: null,
|
|
5337
|
+
runCount: 0,
|
|
5338
|
+
workerCount: 0,
|
|
5339
|
+
oldestRunAt: null,
|
|
5340
|
+
scannedAt
|
|
4912
5341
|
};
|
|
4913
5342
|
}
|
|
4914
|
-
const
|
|
4915
|
-
|
|
4916
|
-
|
|
4917
|
-
|
|
4918
|
-
|
|
4919
|
-
|
|
4920
|
-
|
|
5343
|
+
for (const runEntry of entries) {
|
|
5344
|
+
if (!runEntry.isDirectory()) continue;
|
|
5345
|
+
runCount += 1;
|
|
5346
|
+
const runPath = path25.join(worktreesDir, runEntry.name);
|
|
5347
|
+
try {
|
|
5348
|
+
const st = statSync3(runPath);
|
|
5349
|
+
oldestMs = oldestMs === null ? st.mtimeMs : Math.min(oldestMs, st.mtimeMs);
|
|
5350
|
+
} catch {
|
|
5351
|
+
}
|
|
5352
|
+
try {
|
|
5353
|
+
for (const workerEntry of readdirSync6(runPath, { withFileTypes: true })) {
|
|
5354
|
+
if (workerEntry.isDirectory()) workerCount += 1;
|
|
5355
|
+
}
|
|
5356
|
+
} catch {
|
|
5357
|
+
}
|
|
5358
|
+
if (totalBytes !== null && opts.perRunEntryCap !== null) {
|
|
5359
|
+
const cap = opts.perRunEntryCap ?? 5e4;
|
|
5360
|
+
if (cap <= 0) {
|
|
5361
|
+
totalBytes = null;
|
|
5362
|
+
} else {
|
|
5363
|
+
const runBytes = directorySizeBytes(runPath, cap);
|
|
5364
|
+
if (runBytes === null) totalBytes = null;
|
|
5365
|
+
else totalBytes += runBytes;
|
|
5366
|
+
}
|
|
5367
|
+
}
|
|
5368
|
+
}
|
|
4921
5369
|
return {
|
|
4922
|
-
|
|
4923
|
-
|
|
4924
|
-
|
|
5370
|
+
harnessRoot,
|
|
5371
|
+
worktreesDir,
|
|
5372
|
+
worktreesBytes: totalBytes,
|
|
5373
|
+
runCount,
|
|
5374
|
+
workerCount,
|
|
5375
|
+
oldestRunAt: oldestMs === null ? null : new Date(oldestMs).toISOString(),
|
|
5376
|
+
scannedAt
|
|
4925
5377
|
};
|
|
4926
5378
|
}
|
|
4927
5379
|
|
|
4928
|
-
// src/
|
|
4929
|
-
import
|
|
5380
|
+
// src/cleanup-orphan-safety.ts
|
|
5381
|
+
import { existsSync as existsSync20, statSync as statSync4 } from "node:fs";
|
|
5382
|
+
import path26 from "node:path";
|
|
5383
|
+
|
|
5384
|
+
// src/cleanup-guards-helpers.ts
|
|
5385
|
+
function materialWorktreeChanges2(changedFiles) {
|
|
5386
|
+
return changedFiles.filter((line) => {
|
|
5387
|
+
const trimmed = line.trim();
|
|
5388
|
+
const pathPart = trimmed.startsWith("??") ? trimmed.slice(2).trim() : trimmed.length > 3 ? trimmed.slice(3).trim() : trimmed;
|
|
5389
|
+
return pathPart !== "node_modules" && !pathPart.startsWith("node_modules/");
|
|
5390
|
+
});
|
|
5391
|
+
}
|
|
5392
|
+
|
|
5393
|
+
// src/cleanup-orphan-safety.ts
|
|
5394
|
+
var DEFAULT_HEARTBEAT_FRESH_MS = 30 * 60 * 1e3;
|
|
5395
|
+
function assessOrphanWorktreeSafety(input) {
|
|
5396
|
+
const now = input.now ?? Date.now();
|
|
5397
|
+
const heartbeatFreshMs = input.heartbeatFreshMs ?? DEFAULT_HEARTBEAT_FRESH_MS;
|
|
5398
|
+
if (!existsSync20(input.worktreePath)) return null;
|
|
5399
|
+
if (input.runId && input.workerName) {
|
|
5400
|
+
const heartbeatPath = path26.join(
|
|
5401
|
+
input.harnessRoot,
|
|
5402
|
+
"runs",
|
|
5403
|
+
input.runId,
|
|
5404
|
+
"workers",
|
|
5405
|
+
input.workerName,
|
|
5406
|
+
"heartbeat.jsonl"
|
|
5407
|
+
);
|
|
5408
|
+
try {
|
|
5409
|
+
const mtime = statSync4(heartbeatPath).mtimeMs;
|
|
5410
|
+
if (now - mtime < heartbeatFreshMs) return "active_worker";
|
|
5411
|
+
} catch {
|
|
5412
|
+
}
|
|
5413
|
+
}
|
|
5414
|
+
const gitDir = path26.join(input.worktreePath, ".git");
|
|
5415
|
+
if (!existsSync20(gitDir)) return null;
|
|
5416
|
+
const porcelain = gitCapture(input.worktreePath, ["status", "--porcelain"]);
|
|
5417
|
+
if (porcelain.status !== 0) return "pr_or_unmerged_commits";
|
|
5418
|
+
const dirtyLines = porcelain.stdout.split("\n").map((line) => line.trim()).filter((line) => line.length > 0);
|
|
5419
|
+
if (materialWorktreeChanges2(dirtyLines).length > 0) return "dirty_worktree";
|
|
5420
|
+
const upstreamAhead = gitCapture(input.worktreePath, [
|
|
5421
|
+
"rev-list",
|
|
5422
|
+
"--count",
|
|
5423
|
+
"@{u}..HEAD"
|
|
5424
|
+
]);
|
|
5425
|
+
if (upstreamAhead.status === 0) {
|
|
5426
|
+
const count = Number(upstreamAhead.stdout.trim());
|
|
5427
|
+
if (Number.isFinite(count) && count > 0) return "pr_or_unmerged_commits";
|
|
5428
|
+
}
|
|
5429
|
+
const mainAhead = gitCapture(input.worktreePath, [
|
|
5430
|
+
"rev-list",
|
|
5431
|
+
"--count",
|
|
5432
|
+
"origin/main..HEAD"
|
|
5433
|
+
]);
|
|
5434
|
+
if (mainAhead.status !== 0) {
|
|
5435
|
+
if (upstreamAhead.status !== 0) return "pr_or_unmerged_commits";
|
|
5436
|
+
return null;
|
|
5437
|
+
}
|
|
5438
|
+
const mainCount = Number(mainAhead.stdout.trim());
|
|
5439
|
+
if (Number.isFinite(mainCount) && mainCount > 0) return "pr_or_unmerged_commits";
|
|
5440
|
+
return null;
|
|
5441
|
+
}
|
|
5442
|
+
|
|
5443
|
+
// src/cleanup.ts
|
|
5444
|
+
import path31 from "node:path";
|
|
4930
5445
|
|
|
4931
5446
|
// src/finalize.ts
|
|
4932
|
-
import
|
|
5447
|
+
import path27 from "node:path";
|
|
4933
5448
|
var ACTIVE_RUN_STATUSES = /* @__PURE__ */ new Set([
|
|
4934
5449
|
"running",
|
|
4935
5450
|
"dispatching",
|
|
@@ -4947,7 +5462,7 @@ function deriveTerminalRunStatus(run) {
|
|
|
4947
5462
|
let anyLandingBlocked = false;
|
|
4948
5463
|
for (const name of names) {
|
|
4949
5464
|
const worker = readJson(
|
|
4950
|
-
|
|
5465
|
+
path27.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
|
|
4951
5466
|
void 0
|
|
4952
5467
|
);
|
|
4953
5468
|
if (!worker) continue;
|
|
@@ -4986,188 +5501,6 @@ function finalizeStaleRuns() {
|
|
|
4986
5501
|
return finalized;
|
|
4987
5502
|
}
|
|
4988
5503
|
|
|
4989
|
-
// src/stale-reconcile.ts
|
|
4990
|
-
var STALE_RECONCILE_HEARTBEAT_MS = 15 * 60 * 1e3;
|
|
4991
|
-
function staleReconcileDisabled() {
|
|
4992
|
-
return process.env.KYNVER_NO_STALE_CLEANUP === "1";
|
|
4993
|
-
}
|
|
4994
|
-
function reconcileStaleWorkers() {
|
|
4995
|
-
if (staleReconcileDisabled()) {
|
|
4996
|
-
return { workers: [], finalizedRuns: finalizeStaleRuns() };
|
|
4997
|
-
}
|
|
4998
|
-
const outcomes = [];
|
|
4999
|
-
const now = Date.now();
|
|
5000
|
-
for (const run of listRunRecords()) {
|
|
5001
|
-
for (const name of Object.keys(run.workers || {})) {
|
|
5002
|
-
const workerPath = path24.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json");
|
|
5003
|
-
const worker = readJson(workerPath, void 0);
|
|
5004
|
-
if (!worker || worker.status !== "running") {
|
|
5005
|
-
outcomes.push({
|
|
5006
|
-
runId: run.id,
|
|
5007
|
-
worker: name,
|
|
5008
|
-
action: "skipped",
|
|
5009
|
-
reason: worker ? `worker status is ${worker.status}` : "worker.json missing"
|
|
5010
|
-
});
|
|
5011
|
-
continue;
|
|
5012
|
-
}
|
|
5013
|
-
const status = computeWorkerStatus(worker, { base: run.base, baseCommit: run.baseCommit });
|
|
5014
|
-
if (status.finalResult) {
|
|
5015
|
-
if (worker.status === "running") {
|
|
5016
|
-
const nextStatus = status.attention.state === "blocked" ? "blocked" : status.attention.state === "done" || status.status === "done" ? "done" : "exited";
|
|
5017
|
-
worker.status = nextStatus;
|
|
5018
|
-
worker.reconciledAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
5019
|
-
worker.reconcileReason = "synced finished worker record after terminal stdout/heartbeat";
|
|
5020
|
-
saveWorker(run.id, worker);
|
|
5021
|
-
outcomes.push({
|
|
5022
|
-
runId: run.id,
|
|
5023
|
-
worker: name,
|
|
5024
|
-
action: "marked_exited",
|
|
5025
|
-
reason: worker.reconcileReason
|
|
5026
|
-
});
|
|
5027
|
-
} else {
|
|
5028
|
-
outcomes.push({ runId: run.id, worker: name, action: "skipped", reason: "final result present" });
|
|
5029
|
-
}
|
|
5030
|
-
continue;
|
|
5031
|
-
}
|
|
5032
|
-
if (!status.alive) {
|
|
5033
|
-
const nextStatus = status.attention.state === "blocked" ? "blocked" : status.status === "done" ? "done" : "exited";
|
|
5034
|
-
worker.status = nextStatus;
|
|
5035
|
-
worker.reconciledAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
5036
|
-
worker.reconcileReason = status.attention.reason;
|
|
5037
|
-
saveWorker(run.id, worker);
|
|
5038
|
-
outcomes.push({
|
|
5039
|
-
runId: run.id,
|
|
5040
|
-
worker: name,
|
|
5041
|
-
action: "marked_exited",
|
|
5042
|
-
reason: status.attention.reason
|
|
5043
|
-
});
|
|
5044
|
-
continue;
|
|
5045
|
-
}
|
|
5046
|
-
if (status.attention.state === "stale" && worker.pid && isPidAlive(worker.pid)) {
|
|
5047
|
-
const hbMs = status.lastHeartbeatAt ? Date.parse(status.lastHeartbeatAt) : NaN;
|
|
5048
|
-
const actMs = status.lastActivityAt ? Date.parse(status.lastActivityAt) : NaN;
|
|
5049
|
-
const hbStale = !Number.isFinite(hbMs) || now - hbMs > STALE_RECONCILE_HEARTBEAT_MS;
|
|
5050
|
-
const actStale = Number.isFinite(actMs) && now - actMs > STALE_MS;
|
|
5051
|
-
if (hbStale && actStale) {
|
|
5052
|
-
killWorkerProcess(worker.pid, "SIGTERM");
|
|
5053
|
-
worker.status = "exited";
|
|
5054
|
-
worker.reconciledAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
5055
|
-
worker.reconcileReason = `reconciled stale worker: ${status.attention.reason}`;
|
|
5056
|
-
saveWorker(run.id, worker);
|
|
5057
|
-
outcomes.push({
|
|
5058
|
-
runId: run.id,
|
|
5059
|
-
worker: name,
|
|
5060
|
-
action: "killed_stale",
|
|
5061
|
-
reason: status.attention.reason
|
|
5062
|
-
});
|
|
5063
|
-
continue;
|
|
5064
|
-
}
|
|
5065
|
-
}
|
|
5066
|
-
outcomes.push({
|
|
5067
|
-
runId: run.id,
|
|
5068
|
-
worker: name,
|
|
5069
|
-
action: "skipped",
|
|
5070
|
-
reason: status.attention.reason
|
|
5071
|
-
});
|
|
5072
|
-
}
|
|
5073
|
-
}
|
|
5074
|
-
return { workers: outcomes, finalizedRuns: finalizeStaleRuns() };
|
|
5075
|
-
}
|
|
5076
|
-
function reconcileRunsCli() {
|
|
5077
|
-
const result = reconcileStaleWorkers();
|
|
5078
|
-
const markedExited = result.workers.filter((w) => w.action === "marked_exited").length;
|
|
5079
|
-
const killedStale = result.workers.filter((w) => w.action === "killed_stale").length;
|
|
5080
|
-
const skipped = result.workers.filter((w) => w.action === "skipped").length;
|
|
5081
|
-
console.log(
|
|
5082
|
-
JSON.stringify(
|
|
5083
|
-
{
|
|
5084
|
-
ok: true,
|
|
5085
|
-
workers: { markedExited, killedStale, skipped, total: result.workers.length },
|
|
5086
|
-
finalizedRuns: result.finalizedRuns.length,
|
|
5087
|
-
details: { workers: result.workers, finalizedRuns: result.finalizedRuns }
|
|
5088
|
-
},
|
|
5089
|
-
null,
|
|
5090
|
-
2
|
|
5091
|
-
)
|
|
5092
|
-
);
|
|
5093
|
-
}
|
|
5094
|
-
|
|
5095
|
-
// src/plan-progress-daemon-sync.ts
|
|
5096
|
-
import path25 from "node:path";
|
|
5097
|
-
|
|
5098
|
-
// src/plan-progress-sync.ts
|
|
5099
|
-
async function syncPlanProgress(args) {
|
|
5100
|
-
const base = resolveBaseUrl(args.baseUrl);
|
|
5101
|
-
const secret = await resolveCallbackSecretWithMint(args.secret, args.agentOsId, { baseUrl: base });
|
|
5102
|
-
const url = `${base}/api/agent-os/by-id/${encodeURIComponent(args.agentOsId)}/tasks/${encodeURIComponent(args.taskId)}/plan-progress-sync`;
|
|
5103
|
-
const res = await postJsonWithCredentialRefresh(url, secret, {
|
|
5104
|
-
phase: args.phase,
|
|
5105
|
-
taskId: args.taskId,
|
|
5106
|
-
blocker: args.blocker,
|
|
5107
|
-
artifact: args.artifact
|
|
5108
|
-
}, { agentOsId: args.agentOsId, baseUrl: base });
|
|
5109
|
-
return { ok: res.ok, status: res.status, response: res.response };
|
|
5110
|
-
}
|
|
5111
|
-
|
|
5112
|
-
// src/plan-progress-daemon-sync.ts
|
|
5113
|
-
async function syncActiveWorkerPlanProgress(runId, args) {
|
|
5114
|
-
const run = loadRun(runId);
|
|
5115
|
-
const agentOsId = String(args.agentOsId || "");
|
|
5116
|
-
if (!agentOsId) return [];
|
|
5117
|
-
const outcomes = [];
|
|
5118
|
-
for (const name of Object.keys(run.workers || {})) {
|
|
5119
|
-
const worker = readJson(
|
|
5120
|
-
path25.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
|
|
5121
|
-
void 0
|
|
5122
|
-
);
|
|
5123
|
-
if (!worker?.dispatched || !worker.taskId) continue;
|
|
5124
|
-
const status = computeWorkerStatus(worker);
|
|
5125
|
-
if (!status.heartbeatBlocker) continue;
|
|
5126
|
-
if (worker.lastSyncedHeartbeatBlocker === status.heartbeatBlocker) {
|
|
5127
|
-
outcomes.push({ worker: name, phase: "heartbeat_blocker", ok: true, skipped: true });
|
|
5128
|
-
continue;
|
|
5129
|
-
}
|
|
5130
|
-
const res = await syncPlanProgress({
|
|
5131
|
-
agentOsId,
|
|
5132
|
-
taskId: worker.taskId,
|
|
5133
|
-
phase: "heartbeat_blocker",
|
|
5134
|
-
blocker: status.heartbeatBlocker,
|
|
5135
|
-
baseUrl: args.baseUrl ? String(args.baseUrl) : void 0,
|
|
5136
|
-
secret: args.secret ? String(args.secret) : void 0
|
|
5137
|
-
});
|
|
5138
|
-
if (res.ok) {
|
|
5139
|
-
worker.lastSyncedHeartbeatBlocker = status.heartbeatBlocker;
|
|
5140
|
-
saveWorker(run.id, worker);
|
|
5141
|
-
}
|
|
5142
|
-
outcomes.push({ worker: name, phase: "heartbeat_blocker", ok: res.ok });
|
|
5143
|
-
}
|
|
5144
|
-
return outcomes;
|
|
5145
|
-
}
|
|
5146
|
-
|
|
5147
|
-
// src/workspace-runtime-config.ts
|
|
5148
|
-
async function fetchWorkspaceRuntimePreferences(agentOsId, args) {
|
|
5149
|
-
const base = resolveBaseUrl(args.baseUrl ? String(args.baseUrl) : void 0);
|
|
5150
|
-
const secret = await resolveCallbackSecretWithMint(args.secret ? String(args.secret) : void 0, agentOsId, { baseUrl: base });
|
|
5151
|
-
const url = `${base}/api/agent-os/by-id/${encodeURIComponent(agentOsId)}/runtime`;
|
|
5152
|
-
try {
|
|
5153
|
-
const res = await getJson(url, secret);
|
|
5154
|
-
if (!res.ok) return null;
|
|
5155
|
-
const body = res.response;
|
|
5156
|
-
const raw = body?.preferences?.maxConcurrentWorkers;
|
|
5157
|
-
if (raw === null || raw === void 0) {
|
|
5158
|
-
return { maxConcurrentWorkers: null };
|
|
5159
|
-
}
|
|
5160
|
-
const n = Number(raw);
|
|
5161
|
-
if (!Number.isFinite(n) || n <= 0) return { maxConcurrentWorkers: null };
|
|
5162
|
-
return { maxConcurrentWorkers: Math.floor(n) };
|
|
5163
|
-
} catch {
|
|
5164
|
-
return null;
|
|
5165
|
-
}
|
|
5166
|
-
}
|
|
5167
|
-
|
|
5168
|
-
// src/cleanup.ts
|
|
5169
|
-
import path30 from "node:path";
|
|
5170
|
-
|
|
5171
5504
|
// src/cleanup-run-liveness.ts
|
|
5172
5505
|
function isWorkerProcessLive(indexed) {
|
|
5173
5506
|
if (indexed.status.alive) return true;
|
|
@@ -5209,29 +5542,25 @@ function isPrOrUnmergedWork(status) {
|
|
|
5209
5542
|
if (status.changedFiles.length > 0 && status.finalResult) return true;
|
|
5210
5543
|
return false;
|
|
5211
5544
|
}
|
|
5212
|
-
function materialWorktreeChanges(changedFiles) {
|
|
5213
|
-
return changedFiles.filter((line) => {
|
|
5214
|
-
const trimmed = line.trim();
|
|
5215
|
-
const pathPart = trimmed.startsWith("??") ? trimmed.slice(2).trim() : trimmed.length > 3 ? trimmed.slice(3).trim() : trimmed;
|
|
5216
|
-
return pathPart !== "node_modules" && !pathPart.startsWith("node_modules/");
|
|
5217
|
-
});
|
|
5218
|
-
}
|
|
5219
5545
|
function hasUnrestorableWorktreeChanges(status) {
|
|
5220
|
-
if (
|
|
5546
|
+
if (materialWorktreeChanges2(status.changedFiles).length > 0) return true;
|
|
5221
5547
|
if (status.gitAncestry?.relation === "diverged") return true;
|
|
5222
5548
|
return false;
|
|
5223
5549
|
}
|
|
5224
5550
|
function skipWorktreeRemoval(input) {
|
|
5225
|
-
const { indexed, includeOrphans, worktreesAgeMs, ageMs } = input;
|
|
5226
|
-
if (
|
|
5227
|
-
|
|
5228
|
-
|
|
5551
|
+
const { indexed, includeOrphans, worktreesAgeMs, ageMs, orphanSafety, worktreeRemovalGuard } = input;
|
|
5552
|
+
if (!indexed) {
|
|
5553
|
+
if (!includeOrphans) return "orphan_without_flag";
|
|
5554
|
+
return orphanSafety ?? null;
|
|
5555
|
+
}
|
|
5556
|
+
if (worktreesAgeMs <= 0 && !includeOrphans) return "worktrees_disabled";
|
|
5557
|
+
if (worktreesAgeMs > 0 && ageMs < worktreesAgeMs) return "below_age_threshold";
|
|
5229
5558
|
if (isWorkerProcessLive(indexed)) return "active_worker";
|
|
5230
5559
|
if (indexed.worker.completionBlocker) return "completion_blocked";
|
|
5231
5560
|
if (runBlocksWorktreeRemoval(indexed)) return "run_still_active";
|
|
5232
5561
|
if (!isFinishedWorkerStatus(indexed.status)) return "run_still_active";
|
|
5233
5562
|
if (isPrOrUnmergedWork(indexed.status)) return "pr_or_unmerged_commits";
|
|
5234
|
-
if (
|
|
5563
|
+
if (materialWorktreeChanges2(indexed.status.changedFiles).length > 0) return "dirty_worktree";
|
|
5235
5564
|
const landing = assessWorkerLanding({
|
|
5236
5565
|
finalResult: indexed.status.finalResult,
|
|
5237
5566
|
changedFiles: indexed.status.changedFiles,
|
|
@@ -5239,6 +5568,17 @@ function skipWorktreeRemoval(input) {
|
|
|
5239
5568
|
prUrl: prUrlFromFinalResult(indexed.status.finalResult)
|
|
5240
5569
|
});
|
|
5241
5570
|
if (landing.blocked) return "landing_blocked";
|
|
5571
|
+
if (worktreeRemovalGuard && input.worktreePath) {
|
|
5572
|
+
const overlay = worktreeRemovalGuard({
|
|
5573
|
+
worktreePath: input.worktreePath,
|
|
5574
|
+
indexed: Boolean(indexed),
|
|
5575
|
+
runId: indexed?.runId,
|
|
5576
|
+
worker: indexed?.workerName
|
|
5577
|
+
});
|
|
5578
|
+
if (overlay) {
|
|
5579
|
+
return overlay.detail ? { reason: overlay.reason, detail: overlay.detail } : overlay.reason;
|
|
5580
|
+
}
|
|
5581
|
+
}
|
|
5242
5582
|
return null;
|
|
5243
5583
|
}
|
|
5244
5584
|
function skipNodeModulesRemoval(input) {
|
|
@@ -5255,51 +5595,17 @@ function skipNodeModulesRemoval(input) {
|
|
|
5255
5595
|
gitAncestry: indexed.status.gitAncestry,
|
|
5256
5596
|
prUrl: prUrlFromFinalResult(indexed.status.finalResult)
|
|
5257
5597
|
});
|
|
5258
|
-
if (landing.blocked &&
|
|
5598
|
+
if (landing.blocked && materialWorktreeChanges2(indexed.status.changedFiles).length > 0) {
|
|
5259
5599
|
return "landing_blocked";
|
|
5260
5600
|
}
|
|
5261
5601
|
return null;
|
|
5262
5602
|
}
|
|
5263
5603
|
|
|
5264
5604
|
// src/cleanup-execute.ts
|
|
5265
|
-
import { existsSync as
|
|
5266
|
-
import
|
|
5267
|
-
|
|
5268
|
-
// src/cleanup-dir-size.ts
|
|
5269
|
-
import { existsSync as existsSync16, readdirSync as readdirSync5, statSync as statSync2 } from "node:fs";
|
|
5270
|
-
import path26 from "node:path";
|
|
5271
|
-
function directorySizeBytes(root, maxEntries = 5e4) {
|
|
5272
|
-
if (!existsSync16(root)) return 0;
|
|
5273
|
-
let total = 0;
|
|
5274
|
-
let seen = 0;
|
|
5275
|
-
const stack = [root];
|
|
5276
|
-
while (stack.length > 0) {
|
|
5277
|
-
const current = stack.pop();
|
|
5278
|
-
let entries;
|
|
5279
|
-
try {
|
|
5280
|
-
entries = readdirSync5(current);
|
|
5281
|
-
} catch {
|
|
5282
|
-
continue;
|
|
5283
|
-
}
|
|
5284
|
-
for (const name of entries) {
|
|
5285
|
-
if (seen++ > maxEntries) return null;
|
|
5286
|
-
const full = path26.join(current, name);
|
|
5287
|
-
let st;
|
|
5288
|
-
try {
|
|
5289
|
-
st = statSync2(full);
|
|
5290
|
-
} catch {
|
|
5291
|
-
continue;
|
|
5292
|
-
}
|
|
5293
|
-
if (st.isDirectory()) stack.push(full);
|
|
5294
|
-
else total += st.size;
|
|
5295
|
-
}
|
|
5296
|
-
}
|
|
5297
|
-
return total;
|
|
5298
|
-
}
|
|
5299
|
-
|
|
5300
|
-
// src/cleanup-execute.ts
|
|
5605
|
+
import { existsSync as existsSync21, rmSync } from "node:fs";
|
|
5606
|
+
import path28 from "node:path";
|
|
5301
5607
|
function removeNodeModules(candidate, execute) {
|
|
5302
|
-
if (!
|
|
5608
|
+
if (!existsSync21(candidate.path)) {
|
|
5303
5609
|
return {
|
|
5304
5610
|
...candidate,
|
|
5305
5611
|
executed: false,
|
|
@@ -5330,7 +5636,7 @@ function removeNodeModules(candidate, execute) {
|
|
|
5330
5636
|
}
|
|
5331
5637
|
}
|
|
5332
5638
|
function removeWorktree(candidate, execute) {
|
|
5333
|
-
if (!
|
|
5639
|
+
if (!existsSync21(candidate.path)) {
|
|
5334
5640
|
return {
|
|
5335
5641
|
...candidate,
|
|
5336
5642
|
executed: false,
|
|
@@ -5347,7 +5653,7 @@ function removeWorktree(candidate, execute) {
|
|
|
5347
5653
|
if (repo) {
|
|
5348
5654
|
git(repo, ["worktree", "remove", "--force", candidate.path], { allowFailure: true });
|
|
5349
5655
|
}
|
|
5350
|
-
if (
|
|
5656
|
+
if (existsSync21(candidate.path)) {
|
|
5351
5657
|
rmSync(candidate.path, { recursive: true, force: true });
|
|
5352
5658
|
}
|
|
5353
5659
|
return {
|
|
@@ -5367,40 +5673,40 @@ function removeWorktree(candidate, execute) {
|
|
|
5367
5673
|
}
|
|
5368
5674
|
}
|
|
5369
5675
|
function isHarnessNodeModulesPath(targetPath, harnessRoot, worktreesDir) {
|
|
5370
|
-
const resolved =
|
|
5371
|
-
const nm = resolved.endsWith(`${
|
|
5676
|
+
const resolved = path28.resolve(targetPath);
|
|
5677
|
+
const nm = resolved.endsWith(`${path28.sep}node_modules`) ? resolved : null;
|
|
5372
5678
|
if (!nm) return "path_outside_harness";
|
|
5373
|
-
const rel =
|
|
5374
|
-
if (rel.startsWith("..") ||
|
|
5375
|
-
const parts = rel.split(
|
|
5679
|
+
const rel = path28.relative(worktreesDir, nm);
|
|
5680
|
+
if (rel.startsWith("..") || path28.isAbsolute(rel)) return "path_outside_harness";
|
|
5681
|
+
const parts = rel.split(path28.sep);
|
|
5376
5682
|
if (parts.length < 3 || parts[parts.length - 1] !== "node_modules") return "path_outside_harness";
|
|
5377
|
-
if (!resolved.startsWith(
|
|
5683
|
+
if (!resolved.startsWith(path28.resolve(harnessRoot))) return "path_outside_harness";
|
|
5378
5684
|
return null;
|
|
5379
5685
|
}
|
|
5380
5686
|
|
|
5381
5687
|
// src/cleanup-scan.ts
|
|
5382
|
-
import { existsSync as
|
|
5383
|
-
import
|
|
5688
|
+
import { existsSync as existsSync22, readdirSync as readdirSync7, statSync as statSync5 } from "node:fs";
|
|
5689
|
+
import path29 from "node:path";
|
|
5384
5690
|
function pathAgeMs(target, now) {
|
|
5385
5691
|
try {
|
|
5386
|
-
const mtime =
|
|
5692
|
+
const mtime = statSync5(target).mtimeMs;
|
|
5387
5693
|
return Math.max(0, now - mtime);
|
|
5388
5694
|
} catch {
|
|
5389
5695
|
return 0;
|
|
5390
5696
|
}
|
|
5391
5697
|
}
|
|
5392
5698
|
function isPathInside(child, parent) {
|
|
5393
|
-
const rel =
|
|
5394
|
-
return rel === "" || !rel.startsWith("..") && !
|
|
5699
|
+
const rel = path29.relative(parent, child);
|
|
5700
|
+
return rel === "" || !rel.startsWith("..") && !path29.isAbsolute(rel);
|
|
5395
5701
|
}
|
|
5396
5702
|
function scanNodeModulesCandidates(opts) {
|
|
5397
5703
|
const candidates = [];
|
|
5398
5704
|
const seen = /* @__PURE__ */ new Set();
|
|
5399
5705
|
for (const entry of opts.index.values()) {
|
|
5400
5706
|
if (opts.runIdFilter && entry.runId !== opts.runIdFilter) continue;
|
|
5401
|
-
const nm =
|
|
5402
|
-
if (!
|
|
5403
|
-
const resolved =
|
|
5707
|
+
const nm = path29.join(entry.worktreePath, "node_modules");
|
|
5708
|
+
if (!existsSync22(nm)) continue;
|
|
5709
|
+
const resolved = path29.resolve(nm);
|
|
5404
5710
|
if (seen.has(resolved)) continue;
|
|
5405
5711
|
seen.add(resolved);
|
|
5406
5712
|
candidates.push({
|
|
@@ -5413,16 +5719,16 @@ function scanNodeModulesCandidates(opts) {
|
|
|
5413
5719
|
ageMs: pathAgeMs(resolved, opts.now)
|
|
5414
5720
|
});
|
|
5415
5721
|
}
|
|
5416
|
-
if (!opts.includeOrphans || !
|
|
5417
|
-
for (const runEntry of
|
|
5722
|
+
if (!opts.includeOrphans || !existsSync22(opts.worktreesDir)) return candidates;
|
|
5723
|
+
for (const runEntry of readdirSync7(opts.worktreesDir, { withFileTypes: true })) {
|
|
5418
5724
|
if (!runEntry.isDirectory()) continue;
|
|
5419
|
-
const runPath =
|
|
5420
|
-
for (const workerEntry of
|
|
5725
|
+
const runPath = path29.join(opts.worktreesDir, runEntry.name);
|
|
5726
|
+
for (const workerEntry of readdirSync7(runPath, { withFileTypes: true })) {
|
|
5421
5727
|
if (!workerEntry.isDirectory()) continue;
|
|
5422
|
-
const worktreePath =
|
|
5423
|
-
const nm =
|
|
5424
|
-
if (!
|
|
5425
|
-
const resolved =
|
|
5728
|
+
const worktreePath = path29.join(runPath, workerEntry.name);
|
|
5729
|
+
const nm = path29.join(worktreePath, "node_modules");
|
|
5730
|
+
if (!existsSync22(nm)) continue;
|
|
5731
|
+
const resolved = path29.resolve(nm);
|
|
5426
5732
|
if (seen.has(resolved)) continue;
|
|
5427
5733
|
if (!isPathInside(resolved, opts.harnessRoot)) continue;
|
|
5428
5734
|
seen.add(resolved);
|
|
@@ -5439,40 +5745,76 @@ function scanNodeModulesCandidates(opts) {
|
|
|
5439
5745
|
return candidates;
|
|
5440
5746
|
}
|
|
5441
5747
|
function scanWorktreeCandidates(opts) {
|
|
5442
|
-
|
|
5748
|
+
const indexedEnabled = opts.worktreesAgeMs > 0 || opts.includeOrphans;
|
|
5749
|
+
const orphanEnabled = opts.includeOrphans;
|
|
5750
|
+
if (!indexedEnabled && !orphanEnabled) return [];
|
|
5443
5751
|
const candidates = [];
|
|
5444
5752
|
const seen = /* @__PURE__ */ new Set();
|
|
5753
|
+
if (indexedEnabled) {
|
|
5754
|
+
for (const entry of opts.index.values()) {
|
|
5755
|
+
if (opts.runIdFilter && entry.runId !== opts.runIdFilter) continue;
|
|
5756
|
+
const resolved = entry.worktreePath;
|
|
5757
|
+
if (!existsSync22(resolved)) continue;
|
|
5758
|
+
if (seen.has(resolved)) continue;
|
|
5759
|
+
seen.add(resolved);
|
|
5760
|
+
candidates.push({
|
|
5761
|
+
kind: "remove_worktree",
|
|
5762
|
+
path: resolved,
|
|
5763
|
+
bytes: null,
|
|
5764
|
+
runId: entry.runId,
|
|
5765
|
+
worker: entry.workerName,
|
|
5766
|
+
repo: entry.run.repo,
|
|
5767
|
+
ageMs: pathAgeMs(resolved, opts.now)
|
|
5768
|
+
});
|
|
5769
|
+
}
|
|
5770
|
+
}
|
|
5771
|
+
if (!orphanEnabled || !existsSync22(opts.worktreesDir)) return candidates;
|
|
5772
|
+
const indexedPaths = /* @__PURE__ */ new Set();
|
|
5445
5773
|
for (const entry of opts.index.values()) {
|
|
5446
|
-
|
|
5447
|
-
|
|
5448
|
-
|
|
5449
|
-
if (
|
|
5450
|
-
|
|
5451
|
-
|
|
5452
|
-
|
|
5453
|
-
|
|
5454
|
-
|
|
5455
|
-
|
|
5456
|
-
|
|
5457
|
-
|
|
5458
|
-
|
|
5459
|
-
|
|
5774
|
+
indexedPaths.add(path29.resolve(entry.worktreePath));
|
|
5775
|
+
}
|
|
5776
|
+
for (const runEntry of readdirSync7(opts.worktreesDir, { withFileTypes: true })) {
|
|
5777
|
+
if (!runEntry.isDirectory()) continue;
|
|
5778
|
+
if (opts.runIdFilter && runEntry.name !== opts.runIdFilter) continue;
|
|
5779
|
+
const runPath = path29.join(opts.worktreesDir, runEntry.name);
|
|
5780
|
+
let workerEntries;
|
|
5781
|
+
try {
|
|
5782
|
+
workerEntries = readdirSync7(runPath, { withFileTypes: true });
|
|
5783
|
+
} catch {
|
|
5784
|
+
continue;
|
|
5785
|
+
}
|
|
5786
|
+
for (const workerEntry of workerEntries) {
|
|
5787
|
+
if (!workerEntry.isDirectory()) continue;
|
|
5788
|
+
const worktreePath = path29.resolve(path29.join(runPath, workerEntry.name));
|
|
5789
|
+
if (seen.has(worktreePath)) continue;
|
|
5790
|
+
if (indexedPaths.has(worktreePath)) continue;
|
|
5791
|
+
if (!isPathInside(worktreePath, opts.harnessRoot)) continue;
|
|
5792
|
+
seen.add(worktreePath);
|
|
5793
|
+
candidates.push({
|
|
5794
|
+
kind: "remove_worktree",
|
|
5795
|
+
path: worktreePath,
|
|
5796
|
+
bytes: null,
|
|
5797
|
+
runId: runEntry.name,
|
|
5798
|
+
worker: workerEntry.name,
|
|
5799
|
+
ageMs: pathAgeMs(worktreePath, opts.now)
|
|
5800
|
+
});
|
|
5801
|
+
}
|
|
5460
5802
|
}
|
|
5461
5803
|
return candidates;
|
|
5462
5804
|
}
|
|
5463
5805
|
|
|
5464
5806
|
// src/cleanup-worktree-index.ts
|
|
5465
|
-
import
|
|
5807
|
+
import path30 from "node:path";
|
|
5466
5808
|
function buildWorktreeIndex() {
|
|
5467
5809
|
const index = /* @__PURE__ */ new Map();
|
|
5468
5810
|
for (const run of listRunRecords()) {
|
|
5469
5811
|
for (const name of Object.keys(run.workers || {})) {
|
|
5470
|
-
const workerPath =
|
|
5812
|
+
const workerPath = path30.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json");
|
|
5471
5813
|
const worker = readJson(workerPath, void 0);
|
|
5472
5814
|
if (!worker?.worktreePath) continue;
|
|
5473
5815
|
const status = computeWorkerStatus(worker, { base: run.base, baseCommit: run.baseCommit });
|
|
5474
|
-
index.set(
|
|
5475
|
-
worktreePath:
|
|
5816
|
+
index.set(path30.resolve(worker.worktreePath), {
|
|
5817
|
+
worktreePath: path30.resolve(worker.worktreePath),
|
|
5476
5818
|
runId: run.id,
|
|
5477
5819
|
workerName: name,
|
|
5478
5820
|
run,
|
|
@@ -5518,157 +5860,465 @@ function resolveHarnessRetention(options = {}) {
|
|
|
5518
5860
|
accountBytes
|
|
5519
5861
|
};
|
|
5520
5862
|
}
|
|
5521
|
-
function resolvePipelineHarnessRetention(runId) {
|
|
5522
|
-
const scopeAll = process.env.KYNVER_CLEANUP_SCOPE === "all";
|
|
5523
|
-
const envWorktrees = Number(process.env.KYNVER_CLEANUP_WORKTREES_AGE_MS);
|
|
5524
|
-
const worktreesAgeMs = scopeAll ? Number.isFinite(envWorktrees) && envWorktrees > 0 ? envWorktrees : DEFAULT_WORKTREES_AGE_MS : Number.isFinite(envWorktrees) && envWorktrees > 0 ? envWorktrees : 0;
|
|
5525
|
-
return resolveHarnessRetention({
|
|
5526
|
-
runIdFilter: scopeAll ? void 0 : runId,
|
|
5527
|
-
worktreesAgeMs,
|
|
5528
|
-
finalizeStaleRuns: true,
|
|
5529
|
-
accountBytes: true
|
|
5530
|
-
});
|
|
5863
|
+
function resolvePipelineHarnessRetention(runId) {
|
|
5864
|
+
const scopeAll = process.env.KYNVER_CLEANUP_SCOPE === "all";
|
|
5865
|
+
const envWorktrees = Number(process.env.KYNVER_CLEANUP_WORKTREES_AGE_MS);
|
|
5866
|
+
const worktreesAgeMs = scopeAll ? Number.isFinite(envWorktrees) && envWorktrees > 0 ? envWorktrees : DEFAULT_WORKTREES_AGE_MS : Number.isFinite(envWorktrees) && envWorktrees > 0 ? envWorktrees : 0;
|
|
5867
|
+
return resolveHarnessRetention({
|
|
5868
|
+
runIdFilter: scopeAll ? void 0 : runId,
|
|
5869
|
+
worktreesAgeMs,
|
|
5870
|
+
finalizeStaleRuns: true,
|
|
5871
|
+
accountBytes: true
|
|
5872
|
+
});
|
|
5873
|
+
}
|
|
5874
|
+
|
|
5875
|
+
// src/cleanup.ts
|
|
5876
|
+
function resolvePaths(options = {}) {
|
|
5877
|
+
const harnessRoot = options.harnessRoot ? resolveUserPath(options.harnessRoot) : resolveHarnessRoot();
|
|
5878
|
+
const { worktreesDir } = options.harnessRoot ? { worktreesDir: path31.join(harnessRoot, "worktrees") } : getHarnessPaths();
|
|
5879
|
+
const now = options.now ?? Date.now();
|
|
5880
|
+
return { harnessRoot, worktreesDir, now };
|
|
5881
|
+
}
|
|
5882
|
+
function normalizeGuardSkip(skip) {
|
|
5883
|
+
if (typeof skip === "string") return { reason: skip };
|
|
5884
|
+
return skip;
|
|
5885
|
+
}
|
|
5886
|
+
function recordSkip(skips, pathValue, reason, detail) {
|
|
5887
|
+
skips.push({ path: pathValue, reason, ...detail ? { detail } : {} });
|
|
5888
|
+
}
|
|
5889
|
+
function attachCandidateBytes(candidate, accountBytes) {
|
|
5890
|
+
if (!accountBytes || candidate.bytes != null) return candidate;
|
|
5891
|
+
return { ...candidate, bytes: directorySizeBytes(candidate.path) };
|
|
5892
|
+
}
|
|
5893
|
+
function tallySkipReasons(actions, skips) {
|
|
5894
|
+
const counts = {};
|
|
5895
|
+
for (const skip of skips) {
|
|
5896
|
+
counts[skip.reason] = (counts[skip.reason] ?? 0) + 1;
|
|
5897
|
+
}
|
|
5898
|
+
for (const action of actions) {
|
|
5899
|
+
if (action.skipReason) {
|
|
5900
|
+
counts[action.skipReason] = (counts[action.skipReason] ?? 0) + 1;
|
|
5901
|
+
}
|
|
5902
|
+
}
|
|
5903
|
+
return counts;
|
|
5904
|
+
}
|
|
5905
|
+
function runHarnessCleanup(options = {}) {
|
|
5906
|
+
const retention = resolveHarnessRetention(options);
|
|
5907
|
+
const paths = resolvePaths(options);
|
|
5908
|
+
const finalizedRuns = retention.finalizeStaleRuns ? finalizeStaleRuns().map((f) => ({ runId: f.runId, from: f.from, to: f.to })) : [];
|
|
5909
|
+
const index = buildWorktreeIndex();
|
|
5910
|
+
const scanOpts = {
|
|
5911
|
+
harnessRoot: paths.harnessRoot,
|
|
5912
|
+
worktreesDir: paths.worktreesDir,
|
|
5913
|
+
nodeModulesAgeMs: retention.nodeModulesAgeMs,
|
|
5914
|
+
worktreesAgeMs: retention.worktreesAgeMs,
|
|
5915
|
+
includeOrphans: retention.includeOrphans,
|
|
5916
|
+
runIdFilter: retention.runIdFilter,
|
|
5917
|
+
index,
|
|
5918
|
+
now: paths.now
|
|
5919
|
+
};
|
|
5920
|
+
const skips = [];
|
|
5921
|
+
const actions = [];
|
|
5922
|
+
for (const raw of scanNodeModulesCandidates(scanOpts)) {
|
|
5923
|
+
const candidate = attachCandidateBytes(raw, retention.accountBytes);
|
|
5924
|
+
const pathSkip = isHarnessNodeModulesPath(candidate.path, paths.harnessRoot, paths.worktreesDir);
|
|
5925
|
+
if (pathSkip) {
|
|
5926
|
+
recordSkip(skips, candidate.path, pathSkip);
|
|
5927
|
+
actions.push({ ...candidate, executed: false, skipped: true, skipReason: pathSkip });
|
|
5928
|
+
continue;
|
|
5929
|
+
}
|
|
5930
|
+
const worktreePath = path31.resolve(candidate.path, "..");
|
|
5931
|
+
const indexed = index.get(worktreePath) ?? null;
|
|
5932
|
+
const guardReason = skipNodeModulesRemoval({
|
|
5933
|
+
indexed,
|
|
5934
|
+
includeOrphans: retention.includeOrphans,
|
|
5935
|
+
nodeModulesAgeMs: retention.nodeModulesAgeMs,
|
|
5936
|
+
ageMs: candidate.ageMs
|
|
5937
|
+
});
|
|
5938
|
+
if (guardReason) {
|
|
5939
|
+
recordSkip(skips, candidate.path, guardReason);
|
|
5940
|
+
actions.push({ ...candidate, executed: false, skipped: true, skipReason: guardReason });
|
|
5941
|
+
continue;
|
|
5942
|
+
}
|
|
5943
|
+
actions.push(removeNodeModules(candidate, retention.execute));
|
|
5944
|
+
}
|
|
5945
|
+
for (const raw of scanWorktreeCandidates(scanOpts)) {
|
|
5946
|
+
const candidate = attachCandidateBytes(raw, retention.accountBytes);
|
|
5947
|
+
const indexed = index.get(path31.resolve(candidate.path)) ?? null;
|
|
5948
|
+
const orphanSafety = indexed ? null : assessOrphanWorktreeSafety({
|
|
5949
|
+
worktreePath: candidate.path,
|
|
5950
|
+
harnessRoot: paths.harnessRoot,
|
|
5951
|
+
runId: candidate.runId,
|
|
5952
|
+
workerName: candidate.worker,
|
|
5953
|
+
now: paths.now
|
|
5954
|
+
});
|
|
5955
|
+
const guardSkip = skipWorktreeRemoval({
|
|
5956
|
+
indexed,
|
|
5957
|
+
worktreePath: path31.resolve(candidate.path),
|
|
5958
|
+
includeOrphans: retention.includeOrphans,
|
|
5959
|
+
worktreesAgeMs: retention.worktreesAgeMs,
|
|
5960
|
+
ageMs: candidate.ageMs,
|
|
5961
|
+
orphanSafety,
|
|
5962
|
+
worktreeRemovalGuard: options.worktreeRemovalGuard
|
|
5963
|
+
});
|
|
5964
|
+
if (guardSkip) {
|
|
5965
|
+
const { reason: guardReason, detail: guardDetail } = normalizeGuardSkip(guardSkip);
|
|
5966
|
+
recordSkip(skips, candidate.path, guardReason, guardDetail);
|
|
5967
|
+
actions.push({ ...candidate, executed: false, skipped: true, skipReason: guardReason });
|
|
5968
|
+
continue;
|
|
5969
|
+
}
|
|
5970
|
+
actions.push(removeWorktree(candidate, retention.execute));
|
|
5971
|
+
}
|
|
5972
|
+
let candidateBytes = 0;
|
|
5973
|
+
let reclaimableBytes = 0;
|
|
5974
|
+
let removedBytes = 0;
|
|
5975
|
+
let removedPaths = 0;
|
|
5976
|
+
let skippedPaths = 0;
|
|
5977
|
+
for (const action of actions) {
|
|
5978
|
+
if (action.bytes) candidateBytes += action.bytes;
|
|
5979
|
+
if (!action.skipped && !action.executed && action.bytes) reclaimableBytes += action.bytes;
|
|
5980
|
+
if (action.executed) {
|
|
5981
|
+
removedPaths += 1;
|
|
5982
|
+
removedBytes += action.bytes ?? 0;
|
|
5983
|
+
} else if (action.skipped) {
|
|
5984
|
+
skippedPaths += 1;
|
|
5985
|
+
if (action.skipReason === "dry_run" && action.bytes) reclaimableBytes += action.bytes;
|
|
5986
|
+
}
|
|
5987
|
+
}
|
|
5988
|
+
const storage = retention.accountBytes ? harnessStorageSnapshot({ harnessRoot: paths.harnessRoot, now: paths.now }) : void 0;
|
|
5989
|
+
return {
|
|
5990
|
+
harnessRoot: paths.harnessRoot,
|
|
5991
|
+
dryRun: !retention.execute,
|
|
5992
|
+
execute: retention.execute,
|
|
5993
|
+
nodeModulesAgeMs: retention.nodeModulesAgeMs,
|
|
5994
|
+
worktreesAgeMs: retention.worktreesAgeMs,
|
|
5995
|
+
includeOrphans: retention.includeOrphans,
|
|
5996
|
+
scannedAt: new Date(paths.now).toISOString(),
|
|
5997
|
+
finalizedRuns,
|
|
5998
|
+
actions,
|
|
5999
|
+
skips,
|
|
6000
|
+
totals: {
|
|
6001
|
+
candidateBytes,
|
|
6002
|
+
reclaimableBytes,
|
|
6003
|
+
removedBytes,
|
|
6004
|
+
removedPaths,
|
|
6005
|
+
skippedPaths,
|
|
6006
|
+
skipReasons: tallySkipReasons(actions, skips)
|
|
6007
|
+
},
|
|
6008
|
+
...storage ? { storage } : {}
|
|
6009
|
+
};
|
|
6010
|
+
}
|
|
6011
|
+
function runPipelineHarnessCleanup(runId) {
|
|
6012
|
+
const retention = resolvePipelineHarnessRetention(runId);
|
|
6013
|
+
return runHarnessCleanup({
|
|
6014
|
+
execute: retention.execute,
|
|
6015
|
+
finalizeStaleRuns: retention.finalizeStaleRuns,
|
|
6016
|
+
accountBytes: retention.accountBytes,
|
|
6017
|
+
nodeModulesAgeMs: retention.nodeModulesAgeMs,
|
|
6018
|
+
worktreesAgeMs: retention.worktreesAgeMs,
|
|
6019
|
+
includeOrphans: retention.includeOrphans,
|
|
6020
|
+
runIdFilter: retention.runIdFilter
|
|
6021
|
+
});
|
|
6022
|
+
}
|
|
6023
|
+
function isPipelineCleanupEnabled() {
|
|
6024
|
+
return process.env.KYNVER_PIPELINE_CLEANUP !== "0";
|
|
6025
|
+
}
|
|
6026
|
+
|
|
6027
|
+
// src/cli.ts
|
|
6028
|
+
import { mkdirSync as mkdirSync7, realpathSync } from "node:fs";
|
|
6029
|
+
import { fileURLToPath as fileURLToPath5 } from "node:url";
|
|
6030
|
+
|
|
6031
|
+
// src/discard-disposable.ts
|
|
6032
|
+
import { existsSync as existsSync23, rmSync as rmSync2 } from "node:fs";
|
|
6033
|
+
import path32 from "node:path";
|
|
6034
|
+
function normalizeRelativePath2(value) {
|
|
6035
|
+
const normalized = value.replace(/\\/g, "/").replace(/^\.\//, "").trim();
|
|
6036
|
+
if (!normalized || normalized.startsWith("/") || normalized.includes("..")) {
|
|
6037
|
+
throw new Error(`unsafe path: ${value}`);
|
|
6038
|
+
}
|
|
6039
|
+
return normalized;
|
|
6040
|
+
}
|
|
6041
|
+
function parsePathsArg(raw) {
|
|
6042
|
+
if (typeof raw !== "string" || !raw.trim()) return [];
|
|
6043
|
+
return raw.split(",").map((p) => p.trim()).filter(Boolean);
|
|
6044
|
+
}
|
|
6045
|
+
function discardDisposableArtifacts(args) {
|
|
6046
|
+
const worker = loadWorker(String(args.run), String(args.name));
|
|
6047
|
+
const paths = [
|
|
6048
|
+
...parsePathsArg(args.path),
|
|
6049
|
+
...Array.isArray(args.paths) ? args.paths : []
|
|
6050
|
+
];
|
|
6051
|
+
if (paths.length === 0) {
|
|
6052
|
+
return { ok: false, removed: [], reason: "requires at least one --path" };
|
|
6053
|
+
}
|
|
6054
|
+
const worktreeRoot = path32.resolve(worker.worktreePath);
|
|
6055
|
+
const removed = [];
|
|
6056
|
+
for (const raw of paths) {
|
|
6057
|
+
const rel = normalizeRelativePath2(raw);
|
|
6058
|
+
const abs = path32.resolve(worktreeRoot, rel);
|
|
6059
|
+
if (!abs.startsWith(worktreeRoot + path32.sep) && abs !== worktreeRoot) {
|
|
6060
|
+
return { ok: false, removed, reason: `path escapes worktree: ${raw}` };
|
|
6061
|
+
}
|
|
6062
|
+
if (!existsSync23(abs)) {
|
|
6063
|
+
return { ok: false, removed, reason: `path not found: ${raw}` };
|
|
6064
|
+
}
|
|
6065
|
+
rmSync2(abs, { recursive: true, force: true });
|
|
6066
|
+
removed.push(rel);
|
|
6067
|
+
}
|
|
6068
|
+
const prior = Array.isArray(worker.disposableArtifactsRemoved) ? worker.disposableArtifactsRemoved.filter((p) => typeof p === "string") : [];
|
|
6069
|
+
worker.disposableArtifactsRemoved = [.../* @__PURE__ */ new Set([...prior, ...removed])];
|
|
6070
|
+
saveWorker(worker.runId, worker);
|
|
6071
|
+
const status = computeWorkerStatus(worker);
|
|
6072
|
+
return {
|
|
6073
|
+
ok: true,
|
|
6074
|
+
removed,
|
|
6075
|
+
...status.changedFiles.length ? { reason: "worktree still has other changes" } : {}
|
|
6076
|
+
};
|
|
6077
|
+
}
|
|
6078
|
+
function discardDisposableCli(args) {
|
|
6079
|
+
const result = discardDisposableArtifacts(args);
|
|
6080
|
+
console.log(JSON.stringify(result, null, 2));
|
|
6081
|
+
if (!result.ok) process.exit(1);
|
|
6082
|
+
}
|
|
6083
|
+
|
|
6084
|
+
// src/pipeline-tick.ts
|
|
6085
|
+
import path36 from "node:path";
|
|
6086
|
+
|
|
6087
|
+
// src/pipeline-dispatch.ts
|
|
6088
|
+
var RESERVED_REVIEW_STARTS = 1;
|
|
6089
|
+
function countDispatchStarts(result) {
|
|
6090
|
+
if (!result || typeof result !== "object") return 0;
|
|
6091
|
+
const startedCount = result.startedCount;
|
|
6092
|
+
if (typeof startedCount === "number") return startedCount;
|
|
6093
|
+
const outcomes = result.outcomes;
|
|
6094
|
+
if (!Array.isArray(outcomes)) return 0;
|
|
6095
|
+
return outcomes.filter((o) => o.started).length;
|
|
6096
|
+
}
|
|
6097
|
+
function stripCliMaxStarts(args) {
|
|
6098
|
+
const { maxStarts: _maxStarts, ...rest } = args;
|
|
6099
|
+
return rest;
|
|
6100
|
+
}
|
|
6101
|
+
async function runPipelineDispatch(args, slots) {
|
|
6102
|
+
if (slots <= 0) {
|
|
6103
|
+
return { ok: true, skipped: true, reason: "no slots", maxStarts: 0, startedCount: 0 };
|
|
6104
|
+
}
|
|
6105
|
+
const base = stripCliMaxStarts(args);
|
|
6106
|
+
const reviewBudget = Math.min(slots, RESERVED_REVIEW_STARTS);
|
|
6107
|
+
const workBudget = Math.max(0, slots - reviewBudget);
|
|
6108
|
+
const review = await dispatchRun({
|
|
6109
|
+
...base,
|
|
6110
|
+
execute: true,
|
|
6111
|
+
pipeline: true,
|
|
6112
|
+
lane: "review",
|
|
6113
|
+
maxStarts: String(reviewBudget)
|
|
6114
|
+
});
|
|
6115
|
+
const reviewStarted = countDispatchStarts(review);
|
|
6116
|
+
const workSlots = workBudget + (reviewBudget - reviewStarted);
|
|
6117
|
+
if (workSlots <= 0) {
|
|
6118
|
+
return {
|
|
6119
|
+
...typeof review === "object" && review !== null ? review : {},
|
|
6120
|
+
passes: { review },
|
|
6121
|
+
startedCount: reviewStarted
|
|
6122
|
+
};
|
|
6123
|
+
}
|
|
6124
|
+
const work = await dispatchRun({
|
|
6125
|
+
...base,
|
|
6126
|
+
execute: true,
|
|
6127
|
+
pipeline: true,
|
|
6128
|
+
maxStarts: String(workSlots)
|
|
6129
|
+
});
|
|
6130
|
+
const workStarted = countDispatchStarts(work);
|
|
6131
|
+
return {
|
|
6132
|
+
passes: { review, work },
|
|
6133
|
+
startedCount: reviewStarted + workStarted,
|
|
6134
|
+
ok: true
|
|
6135
|
+
};
|
|
6136
|
+
}
|
|
6137
|
+
|
|
6138
|
+
// src/stale-reconcile.ts
|
|
6139
|
+
import path33 from "node:path";
|
|
6140
|
+
var STALE_RECONCILE_HEARTBEAT_MS = 15 * 60 * 1e3;
|
|
6141
|
+
function staleReconcileDisabled() {
|
|
6142
|
+
return process.env.KYNVER_NO_STALE_CLEANUP === "1";
|
|
6143
|
+
}
|
|
6144
|
+
function reconcileStaleWorkers() {
|
|
6145
|
+
if (staleReconcileDisabled()) {
|
|
6146
|
+
return { workers: [], finalizedRuns: finalizeStaleRuns() };
|
|
6147
|
+
}
|
|
6148
|
+
const outcomes = [];
|
|
6149
|
+
const now = Date.now();
|
|
6150
|
+
for (const run of listRunRecords()) {
|
|
6151
|
+
for (const name of Object.keys(run.workers || {})) {
|
|
6152
|
+
const workerPath = path33.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json");
|
|
6153
|
+
const worker = readJson(workerPath, void 0);
|
|
6154
|
+
if (!worker || worker.status !== "running") {
|
|
6155
|
+
outcomes.push({
|
|
6156
|
+
runId: run.id,
|
|
6157
|
+
worker: name,
|
|
6158
|
+
action: "skipped",
|
|
6159
|
+
reason: worker ? `worker status is ${worker.status}` : "worker.json missing"
|
|
6160
|
+
});
|
|
6161
|
+
continue;
|
|
6162
|
+
}
|
|
6163
|
+
const status = computeWorkerStatus(worker, { base: run.base, baseCommit: run.baseCommit });
|
|
6164
|
+
if (status.finalResult) {
|
|
6165
|
+
if (worker.status === "running") {
|
|
6166
|
+
const nextStatus = status.attention.state === "blocked" ? "blocked" : status.attention.state === "done" || status.status === "done" ? "done" : "exited";
|
|
6167
|
+
worker.status = nextStatus;
|
|
6168
|
+
worker.reconciledAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
6169
|
+
worker.reconcileReason = "synced finished worker record after terminal stdout/heartbeat";
|
|
6170
|
+
saveWorker(run.id, worker);
|
|
6171
|
+
outcomes.push({
|
|
6172
|
+
runId: run.id,
|
|
6173
|
+
worker: name,
|
|
6174
|
+
action: "marked_exited",
|
|
6175
|
+
reason: worker.reconcileReason
|
|
6176
|
+
});
|
|
6177
|
+
} else {
|
|
6178
|
+
outcomes.push({ runId: run.id, worker: name, action: "skipped", reason: "final result present" });
|
|
6179
|
+
}
|
|
6180
|
+
continue;
|
|
6181
|
+
}
|
|
6182
|
+
if (!status.alive) {
|
|
6183
|
+
const nextStatus = status.attention.state === "blocked" ? "blocked" : status.status === "done" ? "done" : "exited";
|
|
6184
|
+
worker.status = nextStatus;
|
|
6185
|
+
worker.reconciledAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
6186
|
+
worker.reconcileReason = status.attention.reason;
|
|
6187
|
+
saveWorker(run.id, worker);
|
|
6188
|
+
outcomes.push({
|
|
6189
|
+
runId: run.id,
|
|
6190
|
+
worker: name,
|
|
6191
|
+
action: "marked_exited",
|
|
6192
|
+
reason: status.attention.reason
|
|
6193
|
+
});
|
|
6194
|
+
continue;
|
|
6195
|
+
}
|
|
6196
|
+
if (status.attention.state === "stale" && worker.pid && isPidAlive(worker.pid)) {
|
|
6197
|
+
const hbMs = status.lastHeartbeatAt ? Date.parse(status.lastHeartbeatAt) : NaN;
|
|
6198
|
+
const actMs = status.lastActivityAt ? Date.parse(status.lastActivityAt) : NaN;
|
|
6199
|
+
const hbStale = !Number.isFinite(hbMs) || now - hbMs > STALE_RECONCILE_HEARTBEAT_MS;
|
|
6200
|
+
const actStale = Number.isFinite(actMs) && now - actMs > STALE_MS;
|
|
6201
|
+
if (hbStale && actStale) {
|
|
6202
|
+
killWorkerProcess(worker.pid, "SIGTERM");
|
|
6203
|
+
worker.status = "exited";
|
|
6204
|
+
worker.reconciledAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
6205
|
+
worker.reconcileReason = `reconciled stale worker: ${status.attention.reason}`;
|
|
6206
|
+
saveWorker(run.id, worker);
|
|
6207
|
+
outcomes.push({
|
|
6208
|
+
runId: run.id,
|
|
6209
|
+
worker: name,
|
|
6210
|
+
action: "killed_stale",
|
|
6211
|
+
reason: status.attention.reason
|
|
6212
|
+
});
|
|
6213
|
+
continue;
|
|
6214
|
+
}
|
|
6215
|
+
}
|
|
6216
|
+
outcomes.push({
|
|
6217
|
+
runId: run.id,
|
|
6218
|
+
worker: name,
|
|
6219
|
+
action: "skipped",
|
|
6220
|
+
reason: status.attention.reason
|
|
6221
|
+
});
|
|
6222
|
+
}
|
|
6223
|
+
}
|
|
6224
|
+
return { workers: outcomes, finalizedRuns: finalizeStaleRuns() };
|
|
6225
|
+
}
|
|
6226
|
+
function reconcileRunsCli() {
|
|
6227
|
+
const result = reconcileStaleWorkers();
|
|
6228
|
+
const markedExited = result.workers.filter((w) => w.action === "marked_exited").length;
|
|
6229
|
+
const killedStale = result.workers.filter((w) => w.action === "killed_stale").length;
|
|
6230
|
+
const skipped = result.workers.filter((w) => w.action === "skipped").length;
|
|
6231
|
+
console.log(
|
|
6232
|
+
JSON.stringify(
|
|
6233
|
+
{
|
|
6234
|
+
ok: true,
|
|
6235
|
+
workers: { markedExited, killedStale, skipped, total: result.workers.length },
|
|
6236
|
+
finalizedRuns: result.finalizedRuns.length,
|
|
6237
|
+
details: { workers: result.workers, finalizedRuns: result.finalizedRuns }
|
|
6238
|
+
},
|
|
6239
|
+
null,
|
|
6240
|
+
2
|
|
6241
|
+
)
|
|
6242
|
+
);
|
|
5531
6243
|
}
|
|
5532
6244
|
|
|
5533
|
-
// src/
|
|
5534
|
-
|
|
5535
|
-
|
|
5536
|
-
|
|
5537
|
-
|
|
5538
|
-
|
|
5539
|
-
}
|
|
5540
|
-
|
|
5541
|
-
|
|
5542
|
-
|
|
5543
|
-
|
|
5544
|
-
|
|
5545
|
-
|
|
5546
|
-
}
|
|
5547
|
-
|
|
5548
|
-
const counts = {};
|
|
5549
|
-
for (const skip of skips) {
|
|
5550
|
-
counts[skip.reason] = (counts[skip.reason] ?? 0) + 1;
|
|
5551
|
-
}
|
|
5552
|
-
for (const action of actions) {
|
|
5553
|
-
if (action.skipReason) {
|
|
5554
|
-
counts[action.skipReason] = (counts[action.skipReason] ?? 0) + 1;
|
|
5555
|
-
}
|
|
5556
|
-
}
|
|
5557
|
-
return counts;
|
|
6245
|
+
// src/plan-progress-daemon-sync.ts
|
|
6246
|
+
import path34 from "node:path";
|
|
6247
|
+
|
|
6248
|
+
// src/plan-progress-sync.ts
|
|
6249
|
+
async function syncPlanProgress(args) {
|
|
6250
|
+
const base = resolveBaseUrl(args.baseUrl);
|
|
6251
|
+
const secret = await resolveCallbackSecretWithMint(args.secret, args.agentOsId, { baseUrl: base });
|
|
6252
|
+
const url = `${base}/api/agent-os/by-id/${encodeURIComponent(args.agentOsId)}/tasks/${encodeURIComponent(args.taskId)}/plan-progress-sync`;
|
|
6253
|
+
const res = await postJsonWithCredentialRefresh(url, secret, {
|
|
6254
|
+
phase: args.phase,
|
|
6255
|
+
taskId: args.taskId,
|
|
6256
|
+
blocker: args.blocker,
|
|
6257
|
+
artifact: args.artifact
|
|
6258
|
+
}, { agentOsId: args.agentOsId, baseUrl: base });
|
|
6259
|
+
return { ok: res.ok, status: res.status, response: res.response };
|
|
5558
6260
|
}
|
|
5559
|
-
|
|
5560
|
-
|
|
5561
|
-
|
|
5562
|
-
const
|
|
5563
|
-
const
|
|
5564
|
-
|
|
5565
|
-
|
|
5566
|
-
|
|
5567
|
-
|
|
5568
|
-
|
|
5569
|
-
|
|
5570
|
-
|
|
5571
|
-
|
|
5572
|
-
|
|
5573
|
-
|
|
5574
|
-
|
|
5575
|
-
|
|
5576
|
-
for (const raw of scanNodeModulesCandidates(scanOpts)) {
|
|
5577
|
-
const candidate = attachCandidateBytes(raw, retention.accountBytes);
|
|
5578
|
-
const pathSkip = isHarnessNodeModulesPath(candidate.path, paths.harnessRoot, paths.worktreesDir);
|
|
5579
|
-
if (pathSkip) {
|
|
5580
|
-
recordSkip(skips, candidate.path, pathSkip);
|
|
5581
|
-
actions.push({ ...candidate, executed: false, skipped: true, skipReason: pathSkip });
|
|
5582
|
-
continue;
|
|
5583
|
-
}
|
|
5584
|
-
const worktreePath = path30.resolve(candidate.path, "..");
|
|
5585
|
-
const indexed = index.get(worktreePath) ?? null;
|
|
5586
|
-
const guardReason = skipNodeModulesRemoval({
|
|
5587
|
-
indexed,
|
|
5588
|
-
includeOrphans: retention.includeOrphans,
|
|
5589
|
-
nodeModulesAgeMs: retention.nodeModulesAgeMs,
|
|
5590
|
-
ageMs: candidate.ageMs
|
|
5591
|
-
});
|
|
5592
|
-
if (guardReason) {
|
|
5593
|
-
recordSkip(skips, candidate.path, guardReason);
|
|
5594
|
-
actions.push({ ...candidate, executed: false, skipped: true, skipReason: guardReason });
|
|
6261
|
+
|
|
6262
|
+
// src/plan-progress-daemon-sync.ts
|
|
6263
|
+
async function syncActiveWorkerPlanProgress(runId, args) {
|
|
6264
|
+
const run = loadRun(runId);
|
|
6265
|
+
const agentOsId = String(args.agentOsId || "");
|
|
6266
|
+
if (!agentOsId) return [];
|
|
6267
|
+
const outcomes = [];
|
|
6268
|
+
for (const name of Object.keys(run.workers || {})) {
|
|
6269
|
+
const worker = readJson(
|
|
6270
|
+
path34.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
|
|
6271
|
+
void 0
|
|
6272
|
+
);
|
|
6273
|
+
if (!worker?.dispatched || !worker.taskId) continue;
|
|
6274
|
+
const status = computeWorkerStatus(worker);
|
|
6275
|
+
if (!status.heartbeatBlocker) continue;
|
|
6276
|
+
if (worker.lastSyncedHeartbeatBlocker === status.heartbeatBlocker) {
|
|
6277
|
+
outcomes.push({ worker: name, phase: "heartbeat_blocker", ok: true, skipped: true });
|
|
5595
6278
|
continue;
|
|
5596
6279
|
}
|
|
5597
|
-
|
|
5598
|
-
|
|
5599
|
-
|
|
5600
|
-
|
|
5601
|
-
|
|
5602
|
-
|
|
5603
|
-
|
|
5604
|
-
includeOrphans: retention.includeOrphans,
|
|
5605
|
-
worktreesAgeMs: retention.worktreesAgeMs,
|
|
5606
|
-
ageMs: candidate.ageMs
|
|
6280
|
+
const res = await syncPlanProgress({
|
|
6281
|
+
agentOsId,
|
|
6282
|
+
taskId: worker.taskId,
|
|
6283
|
+
phase: "heartbeat_blocker",
|
|
6284
|
+
blocker: status.heartbeatBlocker,
|
|
6285
|
+
baseUrl: args.baseUrl ? String(args.baseUrl) : void 0,
|
|
6286
|
+
secret: args.secret ? String(args.secret) : void 0
|
|
5607
6287
|
});
|
|
5608
|
-
if (
|
|
5609
|
-
|
|
5610
|
-
|
|
5611
|
-
continue;
|
|
6288
|
+
if (res.ok) {
|
|
6289
|
+
worker.lastSyncedHeartbeatBlocker = status.heartbeatBlocker;
|
|
6290
|
+
saveWorker(run.id, worker);
|
|
5612
6291
|
}
|
|
5613
|
-
|
|
6292
|
+
outcomes.push({ worker: name, phase: "heartbeat_blocker", ok: res.ok });
|
|
5614
6293
|
}
|
|
5615
|
-
|
|
5616
|
-
|
|
5617
|
-
|
|
5618
|
-
|
|
5619
|
-
|
|
5620
|
-
|
|
5621
|
-
|
|
5622
|
-
|
|
5623
|
-
|
|
5624
|
-
|
|
5625
|
-
|
|
5626
|
-
|
|
5627
|
-
|
|
5628
|
-
|
|
6294
|
+
return outcomes;
|
|
6295
|
+
}
|
|
6296
|
+
|
|
6297
|
+
// src/workspace-runtime-config.ts
|
|
6298
|
+
async function fetchWorkspaceRuntimePreferences(agentOsId, args) {
|
|
6299
|
+
const base = resolveBaseUrl(args.baseUrl ? String(args.baseUrl) : void 0);
|
|
6300
|
+
const secret = await resolveCallbackSecretWithMint(args.secret ? String(args.secret) : void 0, agentOsId, { baseUrl: base });
|
|
6301
|
+
const url = `${base}/api/agent-os/by-id/${encodeURIComponent(agentOsId)}/runtime`;
|
|
6302
|
+
try {
|
|
6303
|
+
const res = await getJson(url, secret);
|
|
6304
|
+
if (!res.ok) return null;
|
|
6305
|
+
const body = res.response;
|
|
6306
|
+
const raw = body?.preferences?.maxConcurrentWorkers;
|
|
6307
|
+
if (raw === null || raw === void 0) {
|
|
6308
|
+
return { maxConcurrentWorkers: null };
|
|
5629
6309
|
}
|
|
6310
|
+
const n = Number(raw);
|
|
6311
|
+
if (!Number.isFinite(n) || n <= 0) return { maxConcurrentWorkers: null };
|
|
6312
|
+
return { maxConcurrentWorkers: Math.floor(n) };
|
|
6313
|
+
} catch {
|
|
6314
|
+
return null;
|
|
5630
6315
|
}
|
|
5631
|
-
return {
|
|
5632
|
-
harnessRoot: paths.harnessRoot,
|
|
5633
|
-
dryRun: !retention.execute,
|
|
5634
|
-
execute: retention.execute,
|
|
5635
|
-
nodeModulesAgeMs: retention.nodeModulesAgeMs,
|
|
5636
|
-
worktreesAgeMs: retention.worktreesAgeMs,
|
|
5637
|
-
includeOrphans: retention.includeOrphans,
|
|
5638
|
-
scannedAt: new Date(paths.now).toISOString(),
|
|
5639
|
-
finalizedRuns,
|
|
5640
|
-
actions,
|
|
5641
|
-
skips,
|
|
5642
|
-
totals: {
|
|
5643
|
-
candidateBytes,
|
|
5644
|
-
reclaimableBytes,
|
|
5645
|
-
removedBytes,
|
|
5646
|
-
removedPaths,
|
|
5647
|
-
skippedPaths,
|
|
5648
|
-
skipReasons: tallySkipReasons(actions, skips)
|
|
5649
|
-
}
|
|
5650
|
-
};
|
|
5651
|
-
}
|
|
5652
|
-
function runPipelineHarnessCleanup(runId) {
|
|
5653
|
-
const retention = resolvePipelineHarnessRetention(runId);
|
|
5654
|
-
return runHarnessCleanup({
|
|
5655
|
-
execute: retention.execute,
|
|
5656
|
-
finalizeStaleRuns: retention.finalizeStaleRuns,
|
|
5657
|
-
accountBytes: retention.accountBytes,
|
|
5658
|
-
nodeModulesAgeMs: retention.nodeModulesAgeMs,
|
|
5659
|
-
worktreesAgeMs: retention.worktreesAgeMs,
|
|
5660
|
-
includeOrphans: retention.includeOrphans,
|
|
5661
|
-
runIdFilter: retention.runIdFilter
|
|
5662
|
-
});
|
|
5663
|
-
}
|
|
5664
|
-
function isPipelineCleanupEnabled() {
|
|
5665
|
-
return process.env.KYNVER_PIPELINE_CLEANUP !== "0";
|
|
5666
6316
|
}
|
|
5667
6317
|
|
|
5668
6318
|
// src/installed-package-versions.ts
|
|
5669
6319
|
import { readFile } from "node:fs/promises";
|
|
5670
6320
|
import { homedir as homedir6 } from "node:os";
|
|
5671
|
-
import
|
|
6321
|
+
import path35 from "node:path";
|
|
5672
6322
|
var MANAGED_PACKAGES = [
|
|
5673
6323
|
"@kynver-app/runtime",
|
|
5674
6324
|
"@kynver-app/openclaw-agent-os",
|
|
@@ -5683,12 +6333,12 @@ function unique(values) {
|
|
|
5683
6333
|
}
|
|
5684
6334
|
function moduleRoots() {
|
|
5685
6335
|
const home = homedir6();
|
|
5686
|
-
const openClawPrefix = trim(process.env.KYNVER_OPENCLAW_NPM_ROOT) ?? trim(process.env.OPENCLAW_NPM_ROOT) ??
|
|
5687
|
-
const npmGlobalRoot = trim(process.env.KYNVER_NPM_GLOBAL_ROOT) ?? trim(process.env.KYNVER_NPM_GLOBAL_MODULES_ROOT) ?? (trim(process.env.NPM_CONFIG_PREFIX) ?
|
|
6336
|
+
const openClawPrefix = trim(process.env.KYNVER_OPENCLAW_NPM_ROOT) ?? trim(process.env.OPENCLAW_NPM_ROOT) ?? path35.join(home, ".openclaw", "npm");
|
|
6337
|
+
const npmGlobalRoot = trim(process.env.KYNVER_NPM_GLOBAL_ROOT) ?? trim(process.env.KYNVER_NPM_GLOBAL_MODULES_ROOT) ?? (trim(process.env.NPM_CONFIG_PREFIX) ? path35.join(trim(process.env.NPM_CONFIG_PREFIX), "lib", "node_modules") : path35.join(home, ".npm-global", "lib", "node_modules"));
|
|
5688
6338
|
return unique([
|
|
5689
|
-
|
|
5690
|
-
|
|
5691
|
-
npmGlobalRoot.endsWith("node_modules") ? npmGlobalRoot :
|
|
6339
|
+
path35.join(openClawPrefix, "lib", "node_modules"),
|
|
6340
|
+
path35.join(openClawPrefix, "node_modules"),
|
|
6341
|
+
npmGlobalRoot.endsWith("node_modules") ? npmGlobalRoot : path35.join(npmGlobalRoot, "lib", "node_modules")
|
|
5692
6342
|
]);
|
|
5693
6343
|
}
|
|
5694
6344
|
async function readVersion(packageJsonPath) {
|
|
@@ -5704,7 +6354,7 @@ async function collectInstalledPackageVersions(observedAt = (/* @__PURE__ */ new
|
|
|
5704
6354
|
const out = {};
|
|
5705
6355
|
for (const packageName of MANAGED_PACKAGES) {
|
|
5706
6356
|
for (const root of roots) {
|
|
5707
|
-
const packageJsonPath =
|
|
6357
|
+
const packageJsonPath = path35.join(root, packageName, "package.json");
|
|
5708
6358
|
const version = await readVersion(packageJsonPath);
|
|
5709
6359
|
if (!version) continue;
|
|
5710
6360
|
out[packageName] = { version, observedAt, path: packageJsonPath };
|
|
@@ -5720,7 +6370,7 @@ async function completeFinishedWorkers(runId, args) {
|
|
|
5720
6370
|
const outcomes = [];
|
|
5721
6371
|
for (const name of Object.keys(run.workers || {})) {
|
|
5722
6372
|
const worker = readJson(
|
|
5723
|
-
|
|
6373
|
+
path36.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
|
|
5724
6374
|
void 0
|
|
5725
6375
|
);
|
|
5726
6376
|
if (!worker?.taskId || worker.localOnly) continue;
|
|
@@ -5862,7 +6512,7 @@ async function runDaemon(args) {
|
|
|
5862
6512
|
}
|
|
5863
6513
|
|
|
5864
6514
|
// src/plan-progress.ts
|
|
5865
|
-
import
|
|
6515
|
+
import path37 from "node:path";
|
|
5866
6516
|
|
|
5867
6517
|
// src/bounded-build/constants.ts
|
|
5868
6518
|
var DEFAULT_BUILD_MEM_BUDGET_BYTES = 1536 * 1024 * 1024;
|
|
@@ -5900,7 +6550,7 @@ function formatNodeOptionsFlag(mb = resolveNodeOldSpaceSizeMb()) {
|
|
|
5900
6550
|
}
|
|
5901
6551
|
|
|
5902
6552
|
// src/bounded-build/systemd-wrap.ts
|
|
5903
|
-
import { spawnSync as
|
|
6553
|
+
import { spawnSync as spawnSync4 } from "node:child_process";
|
|
5904
6554
|
var systemdAvailableCache;
|
|
5905
6555
|
function isSystemdRunAvailable() {
|
|
5906
6556
|
if (process.env.KYNVER_BUILD_SKIP_SYSTEMD === "1" || process.env.KYNVER_BUILD_SKIP_SYSTEMD === "true") {
|
|
@@ -5911,7 +6561,7 @@ function isSystemdRunAvailable() {
|
|
|
5911
6561
|
systemdAvailableCache = false;
|
|
5912
6562
|
return false;
|
|
5913
6563
|
}
|
|
5914
|
-
const res =
|
|
6564
|
+
const res = spawnSync4("systemd-run", ["--version"], { encoding: "utf8", stdio: ["ignore", "ignore", "pipe"] });
|
|
5915
6565
|
systemdAvailableCache = res.status === 0;
|
|
5916
6566
|
return systemdAvailableCache;
|
|
5917
6567
|
}
|
|
@@ -5935,7 +6585,7 @@ function buildSystemdRunArgv(opts) {
|
|
|
5935
6585
|
}
|
|
5936
6586
|
|
|
5937
6587
|
// src/bounded-build/admission.ts
|
|
5938
|
-
import { spawnSync as
|
|
6588
|
+
import { spawnSync as spawnSync5 } from "node:child_process";
|
|
5939
6589
|
function positiveInt3(value, fallback) {
|
|
5940
6590
|
const n = Number(value);
|
|
5941
6591
|
if (!Number.isFinite(n) || n <= 0) return fallback;
|
|
@@ -5971,7 +6621,7 @@ function assessBuildAdmission(opts = {}) {
|
|
|
5971
6621
|
}
|
|
5972
6622
|
function sleepMs2(ms) {
|
|
5973
6623
|
if (ms <= 0) return;
|
|
5974
|
-
|
|
6624
|
+
spawnSync5(process.execPath, ["-e", `const d=Date.now()+${Math.floor(ms)};while(Date.now()<d);`], {
|
|
5975
6625
|
stdio: "ignore"
|
|
5976
6626
|
});
|
|
5977
6627
|
}
|
|
@@ -5992,7 +6642,7 @@ function waitForBuildAdmission(timeoutMs, pollMs = 2e3, opts = {}) {
|
|
|
5992
6642
|
}
|
|
5993
6643
|
|
|
5994
6644
|
// src/bounded-build/exec.ts
|
|
5995
|
-
import { spawnSync as
|
|
6645
|
+
import { spawnSync as spawnSync6 } from "node:child_process";
|
|
5996
6646
|
function envArgv(env) {
|
|
5997
6647
|
const out = [];
|
|
5998
6648
|
for (const [key, value] of Object.entries(env)) {
|
|
@@ -6002,7 +6652,7 @@ function envArgv(env) {
|
|
|
6002
6652
|
return out;
|
|
6003
6653
|
}
|
|
6004
6654
|
function runSpawn(argv, opts) {
|
|
6005
|
-
const res =
|
|
6655
|
+
const res = spawnSync6(argv[0], argv.slice(1), {
|
|
6006
6656
|
cwd: opts.cwd,
|
|
6007
6657
|
env: opts.env,
|
|
6008
6658
|
encoding: "utf8",
|
|
@@ -6148,7 +6798,7 @@ async function emitPlanProgress(args) {
|
|
|
6148
6798
|
}
|
|
6149
6799
|
function verifyPlanLocal(args) {
|
|
6150
6800
|
const worktree = required(args.worktree ? String(args.worktree) : void 0, "worktree");
|
|
6151
|
-
const cwd =
|
|
6801
|
+
const cwd = path37.resolve(worktree);
|
|
6152
6802
|
const summary = runHarnessVerifyCommands(cwd);
|
|
6153
6803
|
const emitJson = args.json === true || args.json === "true";
|
|
6154
6804
|
const payload = { passed: summary.passed, worktree: cwd, steps: summary.steps };
|
|
@@ -6197,9 +6847,9 @@ async function verifyPlan(args) {
|
|
|
6197
6847
|
}
|
|
6198
6848
|
|
|
6199
6849
|
// src/harness-verify-cli.ts
|
|
6200
|
-
import
|
|
6850
|
+
import path38 from "node:path";
|
|
6201
6851
|
function runHarnessVerifyCli(args) {
|
|
6202
|
-
const cwd =
|
|
6852
|
+
const cwd = path38.resolve(required(args.worktree ? String(args.worktree) : void 0, "worktree"));
|
|
6203
6853
|
const emitJson = args.json === true || args.json === "true" || args.emitJson === true || args.emitJson === "true";
|
|
6204
6854
|
const commands = [];
|
|
6205
6855
|
const rawCmd = args.command;
|
|
@@ -6243,7 +6893,7 @@ function runHarnessVerifyCli(args) {
|
|
|
6243
6893
|
}
|
|
6244
6894
|
|
|
6245
6895
|
// src/plan-persist-cli.ts
|
|
6246
|
-
import { readFileSync as
|
|
6896
|
+
import { readFileSync as readFileSync11 } from "node:fs";
|
|
6247
6897
|
var OPERATIONS = ["create", "add_version", "update_metadata"];
|
|
6248
6898
|
var FAILURE_KINDS = [
|
|
6249
6899
|
"approval_guard",
|
|
@@ -6255,7 +6905,7 @@ var FAILURE_KINDS = [
|
|
|
6255
6905
|
function readBodyArg(args) {
|
|
6256
6906
|
const bodyFile = args.bodyFile ? String(args.bodyFile) : void 0;
|
|
6257
6907
|
if (bodyFile) {
|
|
6258
|
-
return { body:
|
|
6908
|
+
return { body: readFileSync11(bodyFile, "utf8"), bodyPathHint: bodyFile };
|
|
6259
6909
|
}
|
|
6260
6910
|
const inline = args.body ? String(args.body) : void 0;
|
|
6261
6911
|
if (inline) return { body: inline };
|
|
@@ -6345,7 +6995,7 @@ function runCleanupCli(args) {
|
|
|
6345
6995
|
}
|
|
6346
6996
|
|
|
6347
6997
|
// src/monitor/monitor.service.ts
|
|
6348
|
-
import
|
|
6998
|
+
import path40 from "node:path";
|
|
6349
6999
|
|
|
6350
7000
|
// src/monitor/monitor.classify.ts
|
|
6351
7001
|
function expectedLeaseOwner(runId) {
|
|
@@ -6401,11 +7051,11 @@ function classifyWorkerHealth(input) {
|
|
|
6401
7051
|
}
|
|
6402
7052
|
|
|
6403
7053
|
// src/monitor/monitor.store.ts
|
|
6404
|
-
import { existsSync as
|
|
6405
|
-
import
|
|
7054
|
+
import { existsSync as existsSync24, mkdirSync as mkdirSync6, readdirSync as readdirSync8, unlinkSync as unlinkSync2 } from "node:fs";
|
|
7055
|
+
import path39 from "node:path";
|
|
6406
7056
|
function monitorsDir() {
|
|
6407
7057
|
const { harnessRoot } = getHarnessPaths();
|
|
6408
|
-
const dir =
|
|
7058
|
+
const dir = path39.join(harnessRoot, "monitors");
|
|
6409
7059
|
mkdirSync6(dir, { recursive: true });
|
|
6410
7060
|
return dir;
|
|
6411
7061
|
}
|
|
@@ -6413,7 +7063,7 @@ function monitorIdFor(runId, workerName) {
|
|
|
6413
7063
|
return workerName ? `${safeSlug(runId)}--${safeSlug(workerName)}` : safeSlug(runId);
|
|
6414
7064
|
}
|
|
6415
7065
|
function monitorPath(monitorId) {
|
|
6416
|
-
return
|
|
7066
|
+
return path39.join(monitorsDir(), `${monitorId}.json`);
|
|
6417
7067
|
}
|
|
6418
7068
|
function loadMonitorSession(monitorId) {
|
|
6419
7069
|
return readJson(monitorPath(monitorId), void 0);
|
|
@@ -6423,18 +7073,18 @@ function saveMonitorSession(session) {
|
|
|
6423
7073
|
}
|
|
6424
7074
|
function deleteMonitorSession(monitorId) {
|
|
6425
7075
|
const file = monitorPath(monitorId);
|
|
6426
|
-
if (!
|
|
7076
|
+
if (!existsSync24(file)) return false;
|
|
6427
7077
|
unlinkSync2(file);
|
|
6428
7078
|
return true;
|
|
6429
7079
|
}
|
|
6430
7080
|
function listMonitorSessions() {
|
|
6431
7081
|
const dir = monitorsDir();
|
|
6432
|
-
if (!
|
|
7082
|
+
if (!existsSync24(dir)) return [];
|
|
6433
7083
|
const entries = [];
|
|
6434
|
-
for (const name of
|
|
7084
|
+
for (const name of readdirSync8(dir)) {
|
|
6435
7085
|
if (!name.endsWith(".json")) continue;
|
|
6436
7086
|
const session = readJson(
|
|
6437
|
-
|
|
7087
|
+
path39.join(dir, name),
|
|
6438
7088
|
void 0
|
|
6439
7089
|
);
|
|
6440
7090
|
if (!session?.monitorId) continue;
|
|
@@ -6525,7 +7175,7 @@ async function fetchTaskLeasesForWorkers(input) {
|
|
|
6525
7175
|
// src/monitor/monitor.service.ts
|
|
6526
7176
|
function workerRecord2(runId, name) {
|
|
6527
7177
|
return readJson(
|
|
6528
|
-
|
|
7178
|
+
path40.join(runDirectory(runId), "workers", safeSlug(name), "worker.json"),
|
|
6529
7179
|
void 0
|
|
6530
7180
|
);
|
|
6531
7181
|
}
|
|
@@ -6727,18 +7377,18 @@ async function runMonitorLoop(args) {
|
|
|
6727
7377
|
|
|
6728
7378
|
// src/monitor/monitor-spawn.ts
|
|
6729
7379
|
import { spawn as spawn4 } from "node:child_process";
|
|
6730
|
-
import { closeSync as closeSync4, existsSync as
|
|
6731
|
-
import
|
|
7380
|
+
import { closeSync as closeSync4, existsSync as existsSync25, openSync as openSync4 } from "node:fs";
|
|
7381
|
+
import path41 from "node:path";
|
|
6732
7382
|
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
6733
7383
|
function resolveDefaultCliPath2() {
|
|
6734
|
-
return
|
|
7384
|
+
return path41.join(fileURLToPath4(new URL(".", import.meta.url)), "cli.js");
|
|
6735
7385
|
}
|
|
6736
7386
|
function spawnMonitorSidecar(opts) {
|
|
6737
7387
|
const cliPath = opts.cliPath ?? resolveDefaultCliPath2();
|
|
6738
|
-
if (!
|
|
7388
|
+
if (!existsSync25(cliPath)) return void 0;
|
|
6739
7389
|
const monitorId = monitorIdFor(opts.runId, opts.workerName);
|
|
6740
7390
|
const { harnessRoot } = getHarnessPaths();
|
|
6741
|
-
const logPath =
|
|
7391
|
+
const logPath = path41.join(harnessRoot, "monitors", `${monitorId}.log`);
|
|
6742
7392
|
let logFd;
|
|
6743
7393
|
try {
|
|
6744
7394
|
logFd = openSync4(logPath, "a");
|
|
@@ -6858,16 +7508,16 @@ async function monitorTickCli(args) {
|
|
|
6858
7508
|
}
|
|
6859
7509
|
|
|
6860
7510
|
// src/doctor/runtime-takeover.ts
|
|
6861
|
-
import
|
|
7511
|
+
import path43 from "node:path";
|
|
6862
7512
|
|
|
6863
7513
|
// src/doctor/runtime-takeover.probes.ts
|
|
6864
|
-
import { accessSync, constants, existsSync as
|
|
7514
|
+
import { accessSync, constants, existsSync as existsSync26, readFileSync as readFileSync12 } from "node:fs";
|
|
6865
7515
|
import { homedir as homedir7 } from "node:os";
|
|
6866
|
-
import
|
|
6867
|
-
import { spawnSync as
|
|
7516
|
+
import path42 from "node:path";
|
|
7517
|
+
import { spawnSync as spawnSync7 } from "node:child_process";
|
|
6868
7518
|
function captureCommand(bin, args) {
|
|
6869
7519
|
try {
|
|
6870
|
-
const res =
|
|
7520
|
+
const res = spawnSync7(bin, args, { encoding: "utf8" });
|
|
6871
7521
|
const stdout = (res.stdout || "").trim();
|
|
6872
7522
|
const stderr = (res.stderr || "").trim();
|
|
6873
7523
|
const ok = res.status === 0;
|
|
@@ -6892,7 +7542,7 @@ function tokenPrefix(token) {
|
|
|
6892
7542
|
return trimmed.length <= 12 ? `${trimmed}\u2026` : `${trimmed.slice(0, 12)}\u2026`;
|
|
6893
7543
|
}
|
|
6894
7544
|
function isWritable(target) {
|
|
6895
|
-
if (!
|
|
7545
|
+
if (!existsSync26(target)) return false;
|
|
6896
7546
|
try {
|
|
6897
7547
|
accessSync(target, constants.W_OK);
|
|
6898
7548
|
return true;
|
|
@@ -6905,15 +7555,15 @@ var defaultRuntimeTakeoverProbes = {
|
|
|
6905
7555
|
commandOnPath: (bin) => captureCommand(process.platform === "win32" ? "where" : "which", [bin]),
|
|
6906
7556
|
kynverVersion: (bin) => captureCommand(bin, ["--version"]),
|
|
6907
7557
|
loadConfig: () => loadUserConfig(),
|
|
6908
|
-
configFilePath: () =>
|
|
6909
|
-
credentialsFilePath: () =>
|
|
7558
|
+
configFilePath: () => path42.join(homedir7(), ".kynver", "config.json"),
|
|
7559
|
+
credentialsFilePath: () => path42.join(homedir7(), ".kynver", "credentials"),
|
|
6910
7560
|
readCredentials: () => {
|
|
6911
|
-
const credPath =
|
|
6912
|
-
if (!
|
|
7561
|
+
const credPath = path42.join(homedir7(), ".kynver", "credentials");
|
|
7562
|
+
if (!existsSync26(credPath)) {
|
|
6913
7563
|
return { hasApiKey: false };
|
|
6914
7564
|
}
|
|
6915
7565
|
try {
|
|
6916
|
-
const parsed = JSON.parse(
|
|
7566
|
+
const parsed = JSON.parse(readFileSync12(credPath, "utf8"));
|
|
6917
7567
|
return {
|
|
6918
7568
|
hasApiKey: Boolean(parsed.apiKey?.trim()),
|
|
6919
7569
|
runnerTokenPrefix: tokenPrefix(parsed.runnerToken),
|
|
@@ -6932,6 +7582,7 @@ var defaultRuntimeTakeoverProbes = {
|
|
|
6932
7582
|
kynverHarnessRoot: process.env.KYNVER_HARNESS_ROOT?.trim() || void 0,
|
|
6933
7583
|
opusHarnessRoot: process.env.OPUS_HARNESS_ROOT?.trim() || void 0,
|
|
6934
7584
|
kynverSchedulerProvider: process.env.KYNVER_SCHEDULER_PROVIDER?.trim() || void 0,
|
|
7585
|
+
openclawCronStorePath: Boolean(process.env.OPENCLAW_CRON_STORE_PATH?.trim()),
|
|
6935
7586
|
qstashTokenPresent: Boolean(process.env.QSTASH_TOKEN?.trim()),
|
|
6936
7587
|
kynverHostedDeployment: (() => {
|
|
6937
7588
|
const v = process.env.KYNVER_HOSTED_DEPLOYMENT?.trim().toLowerCase();
|
|
@@ -6939,18 +7590,59 @@ var defaultRuntimeTakeoverProbes = {
|
|
|
6939
7590
|
})()
|
|
6940
7591
|
}),
|
|
6941
7592
|
harnessRoot: () => resolveHarnessRoot(),
|
|
6942
|
-
legacyOpenclawHarnessRoot: () =>
|
|
6943
|
-
pathExists: (target) =>
|
|
7593
|
+
legacyOpenclawHarnessRoot: () => path42.join(homedir7(), ".openclaw", "harness"),
|
|
7594
|
+
pathExists: (target) => existsSync26(target),
|
|
6944
7595
|
pathWritable: (target) => isWritable(target),
|
|
6945
7596
|
vercelVersion: () => captureCommand("vercel", ["--version"]),
|
|
6946
7597
|
vercelWhoami: () => captureCommand("vercel", ["whoami"])
|
|
6947
7598
|
};
|
|
6948
7599
|
|
|
6949
7600
|
// src/doctor/runtime-takeover-scheduler.ts
|
|
7601
|
+
function hasLocalOpenClawDependency(env, ctx) {
|
|
7602
|
+
return env.kynverSchedulerProvider === "kynver-cron" || env.kynverSchedulerProvider === "openclaw-cron" || ctx.deploymentSchedulerProvider === "kynver-cron" || ctx.deploymentSchedulerProvider === "openclaw-cron" || Boolean(env.openclawCronStorePath);
|
|
7603
|
+
}
|
|
7604
|
+
function hasQstashCutover(env, ctx) {
|
|
7605
|
+
return env.kynverSchedulerProvider === "qstash" || ctx.deploymentSchedulerProvider === "qstash";
|
|
7606
|
+
}
|
|
6950
7607
|
function check(partial) {
|
|
6951
7608
|
return partial;
|
|
6952
7609
|
}
|
|
6953
7610
|
function assessRuntimeTakeoverScheduler(env, ctx) {
|
|
7611
|
+
const schedulerDetails = {
|
|
7612
|
+
schedulerProvider: env.kynverSchedulerProvider ?? null,
|
|
7613
|
+
deploymentSchedulerProvider: ctx.deploymentSchedulerProvider ?? null,
|
|
7614
|
+
openclawCronStorePath: Boolean(env.openclawCronStorePath)
|
|
7615
|
+
};
|
|
7616
|
+
if (hasQstashCutover(env, ctx) && !hasLocalOpenClawDependency(env, ctx)) {
|
|
7617
|
+
const source = env.kynverSchedulerProvider === "qstash" ? "KYNVER_SCHEDULER_PROVIDER=qstash on this host" : "deploymentSchedulerProvider=qstash in ~/.kynver/config.json";
|
|
7618
|
+
return check({
|
|
7619
|
+
id: "hotspot_openclaw_scheduler",
|
|
7620
|
+
label: "Scheduler provider (runtime daemon vs OpenClaw cron)",
|
|
7621
|
+
status: "pass",
|
|
7622
|
+
summary: `AgentOS scheduler cut over to QStash (${source})`,
|
|
7623
|
+
details: schedulerDetails
|
|
7624
|
+
});
|
|
7625
|
+
}
|
|
7626
|
+
if (hasLocalOpenClawDependency(env, ctx)) {
|
|
7627
|
+
const parts = [];
|
|
7628
|
+
if (env.kynverSchedulerProvider === "openclaw-cron") {
|
|
7629
|
+
parts.push("KYNVER_SCHEDULER_PROVIDER=openclaw-cron");
|
|
7630
|
+
}
|
|
7631
|
+
if (ctx.deploymentSchedulerProvider === "openclaw-cron") {
|
|
7632
|
+
parts.push("deploymentSchedulerProvider=openclaw-cron in config");
|
|
7633
|
+
}
|
|
7634
|
+
if (env.openclawCronStorePath) {
|
|
7635
|
+
parts.push("OPENCLAW_CRON_STORE_PATH set (local cron bridge)");
|
|
7636
|
+
}
|
|
7637
|
+
return check({
|
|
7638
|
+
id: "hotspot_openclaw_scheduler",
|
|
7639
|
+
label: "Scheduler provider (runtime daemon vs OpenClaw cron)",
|
|
7640
|
+
status: "warn",
|
|
7641
|
+
summary: `OpenClaw local cron still active (${parts.join("; ")})`,
|
|
7642
|
+
remediation: "On the Kynver deployment: set KYNVER_SCHEDULER_PROVIDER=qstash with QSTASH_TOKEN configured. On user runners: unset KYNVER_SCHEDULER_PROVIDER and OPENCLAW_CRON_STORE_PATH; set deploymentSchedulerProvider to qstash in ~/.kynver/config.json after Vercel env is updated.",
|
|
7643
|
+
details: schedulerDetails
|
|
7644
|
+
});
|
|
7645
|
+
}
|
|
6954
7646
|
const runnerOpenclaw = env.kynverSchedulerProvider === "openclaw-cron";
|
|
6955
7647
|
const runnerQstash = env.kynverSchedulerProvider === "qstash";
|
|
6956
7648
|
const hostedDeployment = Boolean(env.qstashTokenPresent) || Boolean(env.kynverHostedDeployment);
|
|
@@ -7227,8 +7919,8 @@ function assessVercelCli(probes) {
|
|
|
7227
7919
|
}
|
|
7228
7920
|
function assessHarnessDirs(probes) {
|
|
7229
7921
|
const harnessRoot = probes.harnessRoot();
|
|
7230
|
-
const runsDir =
|
|
7231
|
-
const worktreesDir =
|
|
7922
|
+
const runsDir = path43.join(harnessRoot, "runs");
|
|
7923
|
+
const worktreesDir = path43.join(harnessRoot, "worktrees");
|
|
7232
7924
|
const displayHarnessRoot = redactHomePath(harnessRoot);
|
|
7233
7925
|
const displayRunsDir = redactHomePath(runsDir);
|
|
7234
7926
|
const displayWorktreesDir = redactHomePath(worktreesDir);
|
|
@@ -7337,7 +8029,8 @@ function assessOpenclawHotspots(probes) {
|
|
|
7337
8029
|
assessRuntimeTakeoverScheduler(env, {
|
|
7338
8030
|
agentOsId: targetAgentOsId ?? null,
|
|
7339
8031
|
apiBaseUrl: config.apiBaseUrl?.trim() ?? env.kynverApiUrl ?? null,
|
|
7340
|
-
hasScopedRunnerToken
|
|
8032
|
+
hasScopedRunnerToken,
|
|
8033
|
+
deploymentSchedulerProvider: config.deploymentSchedulerProvider === "qstash" || config.deploymentSchedulerProvider === "kynver-cron" || config.deploymentSchedulerProvider === "openclaw-cron" ? config.deploymentSchedulerProvider : void 0
|
|
7341
8034
|
}),
|
|
7342
8035
|
check2({
|
|
7343
8036
|
id: "hotspot_lease_source_names",
|
|
@@ -7443,6 +8136,7 @@ function usage(code = 0) {
|
|
|
7443
8136
|
" kynver worker tail --run RUN_ID --name worker [--lines 40] [--raw]",
|
|
7444
8137
|
" kynver worker stop --run RUN_ID --name worker",
|
|
7445
8138
|
" kynver worker complete --run RUN_ID --name worker [--agent-os-id AOS_ID] [--task-id TASK_ID] [--base-url URL] [--secret SECRET]",
|
|
8139
|
+
" kynver worker discard-disposable --run RUN_ID --name worker --path scripts/helper.mjs[,other]",
|
|
7446
8140
|
" kynver worker auto-complete --run RUN_ID --name worker [--agent-os-id AOS_ID] [--poll-ms 5000] [--max-total-ms 21600000] [--complete-attempts 3] [--complete-backoff-ms 5000] [--base-url URL] [--secret SECRET]",
|
|
7447
8141
|
" kynver run reconcile",
|
|
7448
8142
|
" kynver plan progress --plan PLAN_ID --row ROW_KEY --role ROLE --status STATUS [--task TASK_ID] [--note NOTE] [--evidence type:value] [--agent-os-id AOS_ID]",
|
|
@@ -7452,6 +8146,7 @@ function usage(code = 0) {
|
|
|
7452
8146
|
" kynver plan outbox list",
|
|
7453
8147
|
" kynver plan outbox drain [--max N] [--id OUTBOX_ID]",
|
|
7454
8148
|
" kynver cleanup [--execute] [--node-modules-age-ms MS] [--worktrees-age-ms MS] [--harness-root PATH] [--include-orphans] [--skip-finalize]",
|
|
8149
|
+
" --include-orphans also scans whole worktree directories (<harnessRoot>/worktrees/<runId>/<workerId>/) that no run/worker.json references; orphans pass salvage gates (recent heartbeat, dirty git, ahead of origin/main) before removal.",
|
|
7455
8150
|
" kynver monitor start --run RUN_ID [--name worker] [--agent-os-id AOS_ID] [--poll-ms MS]",
|
|
7456
8151
|
" kynver monitor status [--run RUN_ID] [--name worker] [--tick]",
|
|
7457
8152
|
" kynver monitor stop --run RUN_ID [--name worker]",
|
|
@@ -7512,6 +8207,7 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
7512
8207
|
if (scope === "worker" && action === "tail") return tailWorker(args);
|
|
7513
8208
|
if (scope === "worker" && action === "stop") return stopWorker(args);
|
|
7514
8209
|
if (scope === "worker" && action === "complete") return void await completeWorker(args);
|
|
8210
|
+
if (scope === "worker" && action === "discard-disposable") return discardDisposableCli(args);
|
|
7515
8211
|
if (scope === "worker" && action === "auto-complete") return void await autoCompleteWorkerCli(args);
|
|
7516
8212
|
if (scope === "monitor" && action === "start") {
|
|
7517
8213
|
const result = await startMonitorCli(args);
|
|
@@ -7650,7 +8346,7 @@ function pickVercelStatusContext(statuses) {
|
|
|
7650
8346
|
}
|
|
7651
8347
|
|
|
7652
8348
|
// src/vercel/vercel-evidence.ts
|
|
7653
|
-
import { spawnSync as
|
|
8349
|
+
import { spawnSync as spawnSync8 } from "node:child_process";
|
|
7654
8350
|
var DEFAULT_INSPECT_WAIT_SECONDS = 120;
|
|
7655
8351
|
function mapGitHubStateToEvidence(state) {
|
|
7656
8352
|
if (state === "success") return "ready";
|
|
@@ -7716,7 +8412,7 @@ function evidenceFromGitHubVercelStatus(statuses, options = {}) {
|
|
|
7716
8412
|
}
|
|
7717
8413
|
function defaultRunVercelInspect(target, waitSeconds) {
|
|
7718
8414
|
const args = ["inspect", target, "--wait", String(waitSeconds)];
|
|
7719
|
-
const result =
|
|
8415
|
+
const result = spawnSync8("vercel", args, {
|
|
7720
8416
|
encoding: "utf8",
|
|
7721
8417
|
stdio: ["ignore", "pipe", "pipe"]
|
|
7722
8418
|
});
|
|
@@ -7825,11 +8521,16 @@ function collectVercelEvidence(input) {
|
|
|
7825
8521
|
export {
|
|
7826
8522
|
DEFAULT_DISPATCH_LEASE_MS,
|
|
7827
8523
|
DEFAULT_HARNESS_VERIFY_COMMANDS,
|
|
8524
|
+
DEFAULT_WSL_HOST_CRITICAL_FREE_BYTES,
|
|
8525
|
+
DEFAULT_WSL_HOST_MOUNT,
|
|
8526
|
+
DEFAULT_WSL_HOST_WARN_FREE_BYTES,
|
|
7828
8527
|
FORBIDDEN_WORKER_ENV_KEYS,
|
|
7829
8528
|
PACKAGE_VERSION,
|
|
8529
|
+
applyProductionDatabaseToProcess,
|
|
7830
8530
|
assessAutoCompleteEligibility,
|
|
7831
8531
|
assessBuildAdmission,
|
|
7832
8532
|
assessExitedWorkerSalvage,
|
|
8533
|
+
assessOrphanWorktreeSafety,
|
|
7833
8534
|
assessPrHandoffRequirement,
|
|
7834
8535
|
assessWorkerLanding,
|
|
7835
8536
|
assessWorkerLandingContract,
|
|
@@ -7862,6 +8563,7 @@ export {
|
|
|
7862
8563
|
getHarnessPaths,
|
|
7863
8564
|
getMonitorStatus,
|
|
7864
8565
|
gitRepoRoot,
|
|
8566
|
+
harnessStorageSnapshot,
|
|
7865
8567
|
hashPlanBody,
|
|
7866
8568
|
isDashboardVercelUrl,
|
|
7867
8569
|
isEngagementRequiredSkip,
|
|
@@ -7869,9 +8571,11 @@ export {
|
|
|
7869
8571
|
isForbiddenWorkerEnvKey,
|
|
7870
8572
|
isKynverMonorepoRoot,
|
|
7871
8573
|
isLandingBlockedWorkerStatus,
|
|
8574
|
+
isPipelineCleanupEnabled,
|
|
7872
8575
|
isSystemdRunAvailable,
|
|
7873
8576
|
isTerminalHeartbeatPhase,
|
|
7874
8577
|
isVercelStatusContext,
|
|
8578
|
+
isWslHost,
|
|
7875
8579
|
landingContractAttentionReason,
|
|
7876
8580
|
listForbiddenWorkerEnvKeys,
|
|
7877
8581
|
listMonitors,
|
|
@@ -7882,6 +8586,7 @@ export {
|
|
|
7882
8586
|
mergeNodeOptionsForBuildCheck,
|
|
7883
8587
|
normalizeCursorModelAlias,
|
|
7884
8588
|
observeRunnerDiskGate,
|
|
8589
|
+
observeWslHostDisk,
|
|
7885
8590
|
parseArgs,
|
|
7886
8591
|
parseClaudeStream,
|
|
7887
8592
|
parseHarnessStream,
|
|
@@ -7892,6 +8597,7 @@ export {
|
|
|
7892
8597
|
postJson,
|
|
7893
8598
|
preflightCursorModel,
|
|
7894
8599
|
readMemAvailableBytes,
|
|
8600
|
+
readProductionDbKeysFromEnvFile,
|
|
7895
8601
|
reconcileRunsCli,
|
|
7896
8602
|
reconcileStaleWorkers,
|
|
7897
8603
|
redactHarness,
|
|
@@ -7901,12 +8607,16 @@ export {
|
|
|
7901
8607
|
resolveCallbackSecretWithMint,
|
|
7902
8608
|
resolveDefaultRepo,
|
|
7903
8609
|
resolveHarnessRoot,
|
|
8610
|
+
resolveProductionDatabaseUrl,
|
|
7904
8611
|
resolveVercelInspectTarget,
|
|
7905
8612
|
runBoundedBuildCheck,
|
|
7906
8613
|
runDaemon,
|
|
8614
|
+
runHarnessCleanup,
|
|
7907
8615
|
runHarnessVerifyCommands,
|
|
7908
8616
|
runMonitorTick,
|
|
8617
|
+
runPipelineHarnessCleanup,
|
|
7909
8618
|
runStatus,
|
|
8619
|
+
safeDatabaseUrlHint,
|
|
7910
8620
|
saveUserConfig,
|
|
7911
8621
|
scrubClaudeEnv,
|
|
7912
8622
|
scrubWorkerEnv,
|
|
@@ -7918,6 +8628,7 @@ export {
|
|
|
7918
8628
|
summarizeEvent,
|
|
7919
8629
|
summarizeNpmAuditReport,
|
|
7920
8630
|
summarizeShellToolCallEvent,
|
|
8631
|
+
summarizeWslRecoverySteps,
|
|
7921
8632
|
sweepRun,
|
|
7922
8633
|
tailWorker,
|
|
7923
8634
|
terminalFinalResultFromHeartbeat,
|