@kynver-app/runtime 0.1.62 → 0.1.66
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/README.md +3 -3
- package/dist/box-resource-snapshot-shared.d.ts +2 -0
- package/dist/box-resource-snapshot.d.ts +45 -0
- package/dist/cleanup-active-worktrees.d.ts +14 -0
- package/dist/cleanup-build-cache-paths.d.ts +5 -0
- package/dist/cleanup-dependency-scan.d.ts +12 -0
- package/dist/cleanup-disk-pressure.d.ts +15 -0
- package/dist/cleanup-duplicate-worktrees.d.ts +7 -0
- package/dist/cleanup-execute.d.ts +4 -0
- package/dist/cleanup-guards-helpers.d.ts +1 -1
- package/dist/cleanup-guards.d.ts +13 -0
- package/dist/cleanup-harness-roots.d.ts +6 -0
- package/dist/cleanup-retention-config.d.ts +3 -0
- package/dist/cleanup-scan.d.ts +1 -0
- package/dist/cleanup-types.d.ts +12 -1
- package/dist/cleanup-worktree-index.d.ts +2 -0
- package/dist/cli.js +1639 -558
- package/dist/cli.js.map +4 -4
- package/dist/harness-notice/harness-notice.auto-complete.d.ts +3 -0
- package/dist/harness-notice/harness-notice.monitor-tick.d.ts +6 -0
- package/dist/harness-notice/harness-notice.parse.d.ts +3 -0
- package/dist/harness-notice/harness-notice.tool-response.d.ts +3 -0
- package/dist/harness-notice/harness-notice.types.d.ts +17 -0
- package/dist/harness-notice/harness-notice.worker-complete.d.ts +3 -0
- package/dist/harness-notice/harness-notice.worker-status.d.ts +2 -0
- package/dist/harness-notice/index.d.ts +7 -0
- package/dist/harness-repair-target.d.ts +23 -0
- package/dist/heartbeat.d.ts +3 -0
- package/dist/index.d.ts +9 -2
- package/dist/index.js +2079 -700
- package/dist/index.js.map +4 -4
- package/dist/landing-contract-gate.d.ts +3 -1
- package/dist/model-routing.d.ts +2 -0
- package/dist/pr-handoff/pr-handoff-assess.d.ts +2 -0
- package/dist/pr-handoff/pr-handoff.types.d.ts +1 -1
- package/dist/prompt.d.ts +2 -0
- package/dist/repair-target-worktree.d.ts +4 -0
- package/dist/resource-gate.d.ts +5 -0
- package/dist/run-list.d.ts +38 -0
- package/dist/run-store.d.ts +2 -0
- package/dist/runner-identity.d.ts +12 -0
- package/dist/scheduler-cutover-cli.d.ts +2 -0
- package/dist/scheduler-cutover.d.ts +22 -0
- package/dist/status.d.ts +3 -0
- package/dist/supervisor.d.ts +2 -0
- package/dist/vercel/index.d.ts +1 -1
- package/dist/vercel/vercel-url.d.ts +2 -0
- package/dist/worker-provider-policy.d.ts +26 -0
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -87,10 +87,6 @@ function tailFile(file, lines) {
|
|
|
87
87
|
function readMaybeFile(file) {
|
|
88
88
|
return file ? readFileSync(path.resolve(file), "utf8") : "";
|
|
89
89
|
}
|
|
90
|
-
function listRunIds(runsDir) {
|
|
91
|
-
if (!existsSync(runsDir)) return [];
|
|
92
|
-
return readdirSync(runsDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
|
|
93
|
-
}
|
|
94
90
|
function sleepMs(ms) {
|
|
95
91
|
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
96
92
|
}
|
|
@@ -611,7 +607,7 @@ async function runSetup(args) {
|
|
|
611
607
|
...existing,
|
|
612
608
|
...inferSetupFields(existing, args),
|
|
613
609
|
...maxWorkersRaw ? { maxConcurrentWorkers: Math.max(1, Math.floor(Number(maxWorkersRaw))) } : {},
|
|
614
|
-
workerProvider: typeof args.provider === "string" ? args.provider : existing.workerProvider || "
|
|
610
|
+
workerProvider: typeof args.provider === "string" ? args.provider : existing.workerProvider || "cursor"
|
|
615
611
|
});
|
|
616
612
|
saveUserConfig(config);
|
|
617
613
|
let runnerCredentialNote;
|
|
@@ -733,23 +729,23 @@ function isWslHost() {
|
|
|
733
729
|
function observeWslHostDisk(options = {}) {
|
|
734
730
|
const wsl = options.forceWsl === void 0 ? isWslHost() : options.forceWsl;
|
|
735
731
|
if (!wsl) return null;
|
|
736
|
-
const
|
|
732
|
+
const path50 = options.wslHostMount?.trim() || process.env.KYNVER_WSL_HOST_MOUNT?.trim() || DEFAULT_WSL_HOST_MOUNT;
|
|
737
733
|
const warnBelowBytes = options.wslHostFreeWarnBytes ?? DEFAULT_WSL_HOST_WARN_FREE_BYTES;
|
|
738
734
|
const criticalBelowBytes = options.wslHostFreeCriticalBytes ?? DEFAULT_WSL_HOST_CRITICAL_FREE_BYTES;
|
|
739
735
|
const statfs = options.statfs ?? statfsSync;
|
|
740
736
|
let stats;
|
|
741
737
|
try {
|
|
742
|
-
stats = statfs(
|
|
738
|
+
stats = statfs(path50);
|
|
743
739
|
} catch (error) {
|
|
744
740
|
return {
|
|
745
741
|
ok: false,
|
|
746
|
-
path:
|
|
742
|
+
path: path50,
|
|
747
743
|
freeBytes: 0,
|
|
748
744
|
totalBytes: 0,
|
|
749
745
|
usedPercent: 100,
|
|
750
746
|
warnBelowBytes,
|
|
751
747
|
criticalBelowBytes,
|
|
752
|
-
reason: `Windows host disk probe failed at ${
|
|
748
|
+
reason: `Windows host disk probe failed at ${path50}: ${error.message}`,
|
|
753
749
|
probeError: error.message
|
|
754
750
|
};
|
|
755
751
|
}
|
|
@@ -763,11 +759,11 @@ function observeWslHostDisk(options = {}) {
|
|
|
763
759
|
let reason = null;
|
|
764
760
|
if (!ok) {
|
|
765
761
|
const tag = criticalFree ? "critical" : "warning";
|
|
766
|
-
reason = `Windows host disk ${
|
|
762
|
+
reason = `Windows host disk ${path50} at ${tag}: ${freeGiB} GiB free (<${(criticalFree ? criticalBelowBytes : warnBelowBytes) / 1024 / 1024 / 1024} GiB); WSL VHDX cannot grow safely. ${summarizeWslRecoverySteps()}`;
|
|
767
763
|
}
|
|
768
764
|
return {
|
|
769
765
|
ok,
|
|
770
|
-
path:
|
|
766
|
+
path: path50,
|
|
771
767
|
freeBytes,
|
|
772
768
|
totalBytes,
|
|
773
769
|
usedPercent,
|
|
@@ -787,12 +783,12 @@ var DEFAULT_CRITICAL_FREE_BYTES = 15 * 1024 * 1024 * 1024;
|
|
|
787
783
|
var DEFAULT_MAX_USED_PERCENT = 80;
|
|
788
784
|
var DEFAULT_HARD_MAX_USED_PERCENT = 90;
|
|
789
785
|
function observeRunnerDiskGate(input = {}) {
|
|
790
|
-
const
|
|
786
|
+
const path50 = input.diskPath?.trim() || "/";
|
|
791
787
|
const warnBelowBytes = input.diskFreeWarnBytes ?? DEFAULT_WARN_FREE_BYTES;
|
|
792
788
|
const criticalBelowBytes = input.diskFreeCriticalBytes ?? DEFAULT_CRITICAL_FREE_BYTES;
|
|
793
789
|
const maxUsedPercent = input.diskMaxUsedPercent ?? DEFAULT_MAX_USED_PERCENT;
|
|
794
790
|
const hardMaxUsedPercent = input.diskHardMaxUsedPercent ?? DEFAULT_HARD_MAX_USED_PERCENT;
|
|
795
|
-
const stats = statfsSync2(
|
|
791
|
+
const stats = statfsSync2(path50);
|
|
796
792
|
const freeBytes = Number(stats.bavail) * Number(stats.bsize);
|
|
797
793
|
const totalBytes = Number(stats.blocks) * Number(stats.bsize);
|
|
798
794
|
const usedPercent = totalBytes > 0 ? (totalBytes - freeBytes) / totalBytes * 100 : 100;
|
|
@@ -815,7 +811,7 @@ function observeRunnerDiskGate(input = {}) {
|
|
|
815
811
|
}
|
|
816
812
|
return {
|
|
817
813
|
ok,
|
|
818
|
-
path:
|
|
814
|
+
path: path50,
|
|
819
815
|
freeBytes,
|
|
820
816
|
totalBytes,
|
|
821
817
|
usedPercent,
|
|
@@ -895,6 +891,12 @@ function loadRun(id) {
|
|
|
895
891
|
}
|
|
896
892
|
function listRunRecords() {
|
|
897
893
|
const { runsDir } = getPaths();
|
|
894
|
+
return listRunRecordsAt(runsDir);
|
|
895
|
+
}
|
|
896
|
+
function listRunRecordsForHarnessRoot(harnessRoot) {
|
|
897
|
+
return listRunRecordsAt(path6.join(harnessRoot, "runs"));
|
|
898
|
+
}
|
|
899
|
+
function listRunRecordsAt(runsDir) {
|
|
898
900
|
if (!existsSync6(runsDir)) return [];
|
|
899
901
|
const runs = [];
|
|
900
902
|
for (const entry of readdirSync2(runsDir, { withFileTypes: true })) {
|
|
@@ -923,7 +925,10 @@ function saveWorker(runId, worker) {
|
|
|
923
925
|
}
|
|
924
926
|
function runDirectory(id) {
|
|
925
927
|
const { runsDir } = getPaths();
|
|
926
|
-
return
|
|
928
|
+
return runDirectoryAt(runsDir, id);
|
|
929
|
+
}
|
|
930
|
+
function runDirectoryAt(harnessRoot, id) {
|
|
931
|
+
return runDir(path6.join(harnessRoot, "runs"), safeSlug(id));
|
|
927
932
|
}
|
|
928
933
|
|
|
929
934
|
// src/heartbeat.ts
|
|
@@ -944,7 +949,9 @@ function parseHeartbeat(file) {
|
|
|
944
949
|
lastHeartbeatPhase: null,
|
|
945
950
|
lastHeartbeatSummary: null,
|
|
946
951
|
heartbeatBlocker: null,
|
|
947
|
-
timestampAnomalies: []
|
|
952
|
+
timestampAnomalies: [],
|
|
953
|
+
lastBoxResourceSnapshot: null,
|
|
954
|
+
lastPrEvidence: []
|
|
948
955
|
};
|
|
949
956
|
if (!existsSync7(file)) return result;
|
|
950
957
|
const maxFutureMs = Date.now() + HEARTBEAT_FUTURE_SKEW_MS;
|
|
@@ -971,6 +978,14 @@ function parseHeartbeat(file) {
|
|
|
971
978
|
if (row.phase !== void 0 && row.phase !== null) result.lastHeartbeatPhase = String(row.phase);
|
|
972
979
|
if (row.summary !== void 0 && row.summary !== null) result.lastHeartbeatSummary = String(row.summary);
|
|
973
980
|
result.heartbeatBlocker = row.blocker ? String(row.blocker) : null;
|
|
981
|
+
if (row.boxResourceSnapshot && typeof row.boxResourceSnapshot === "object" && !Array.isArray(row.boxResourceSnapshot)) {
|
|
982
|
+
result.lastBoxResourceSnapshot = row.boxResourceSnapshot;
|
|
983
|
+
}
|
|
984
|
+
if (Array.isArray(row.prEvidence)) {
|
|
985
|
+
result.lastPrEvidence = row.prEvidence.filter(
|
|
986
|
+
(entry2) => !!entry2 && typeof entry2 === "object" && typeof entry2.prUrl === "string"
|
|
987
|
+
);
|
|
988
|
+
}
|
|
974
989
|
}
|
|
975
990
|
return result;
|
|
976
991
|
}
|
|
@@ -1488,10 +1503,35 @@ function workerPrUrls(snapshot, finalResult) {
|
|
|
1488
1503
|
function assessWorkerLandingContract(input) {
|
|
1489
1504
|
const { contract, snapshot } = input;
|
|
1490
1505
|
const finalResult = input.finalResult ?? snapshot.finalResult;
|
|
1491
|
-
if (!contract.landingOnly && contract.targetPrUrls.length === 0) {
|
|
1506
|
+
if (!contract.landingOnly && contract.targetPrUrls.length === 0 && !contract.repairEnforceOriginalPr) {
|
|
1492
1507
|
return { blocked: false };
|
|
1493
1508
|
}
|
|
1494
1509
|
if (!hasFinalResult3(finalResult)) return { blocked: false };
|
|
1510
|
+
const repairTarget = contract.repairEnforceOriginalPr ? normalizePrUrl(trimOrNull3(contract.targetPrUrl) ?? "") ?? (contract.targetPrUrls.length === 1 ? normalizePrUrl(contract.targetPrUrls[0]) : null) : null;
|
|
1511
|
+
if (repairTarget) {
|
|
1512
|
+
const workerPrs2 = workerPrUrls(snapshot, finalResult);
|
|
1513
|
+
const supersedes = finalResult && typeof finalResult === "object" && !Array.isArray(finalResult) && finalResult.supersedesOriginalTargetPr === true;
|
|
1514
|
+
if (!supersedes) {
|
|
1515
|
+
for (const pr of workerPrs2) {
|
|
1516
|
+
if (pr !== repairTarget) {
|
|
1517
|
+
return {
|
|
1518
|
+
blocked: true,
|
|
1519
|
+
reason: "duplicate_repair_pr",
|
|
1520
|
+
detail: `Repair worker opened or attached PR ${pr} instead of canonical target ${repairTarget}`
|
|
1521
|
+
};
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
const reconciliation2 = parseReconciliation(finalResult);
|
|
1526
|
+
const entry = reconciliation2.find((r) => r.prUrl === repairTarget);
|
|
1527
|
+
if (!entry || entry.outcome !== "merged" && !(entry.reason?.trim() && (entry.outcome === "skipped" || entry.outcome === "blocked"))) {
|
|
1528
|
+
return {
|
|
1529
|
+
blocked: true,
|
|
1530
|
+
reason: "missing_repair_target_reconciliation",
|
|
1531
|
+
detail: `Repair worker must reconcile target PR ${repairTarget}`
|
|
1532
|
+
};
|
|
1533
|
+
}
|
|
1534
|
+
}
|
|
1495
1535
|
const reconciliation = parseReconciliation(finalResult);
|
|
1496
1536
|
const byUrl = new Map(reconciliation.map((r) => [r.prUrl, r]));
|
|
1497
1537
|
const targetSet = new Set(
|
|
@@ -1646,6 +1686,12 @@ function computeWorkerStatus(worker, options = {}) {
|
|
|
1646
1686
|
]);
|
|
1647
1687
|
const error = parsed.error || (!alive && !finalResult ? tailFile(worker.stderrPath, 10).trim() || void 0 : void 0);
|
|
1648
1688
|
const completionBlocker = typeof worker.completionBlocker === "string" && worker.completionBlocker.trim() ? worker.completionBlocker.trim() : null;
|
|
1689
|
+
const landingContract = worker.repairTargetPrUrl ? {
|
|
1690
|
+
landingOnly: false,
|
|
1691
|
+
targetPrUrls: [worker.repairTargetPrUrl],
|
|
1692
|
+
targetPrUrl: worker.repairTargetPrUrl,
|
|
1693
|
+
repairEnforceOriginalPr: true
|
|
1694
|
+
} : null;
|
|
1649
1695
|
const attention = computeAttention({
|
|
1650
1696
|
alive,
|
|
1651
1697
|
finalResult,
|
|
@@ -1658,7 +1704,9 @@ function computeWorkerStatus(worker, options = {}) {
|
|
|
1658
1704
|
error,
|
|
1659
1705
|
changedFiles,
|
|
1660
1706
|
gitAncestry,
|
|
1661
|
-
completionBlocker
|
|
1707
|
+
completionBlocker,
|
|
1708
|
+
landingContract,
|
|
1709
|
+
prUrl: worker.repairTargetPrUrl ?? worker.taskPrUrl ?? null
|
|
1662
1710
|
});
|
|
1663
1711
|
const workerStatusLabel = completionBlocker || attention.state === "blocked" ? "blocked" : completionAcknowledged || attention.state === "done" ? "done" : finalResult ? "exited" : alive ? "running" : "exited";
|
|
1664
1712
|
return {
|
|
@@ -1780,9 +1828,16 @@ function observeRunnerResourceGate(input) {
|
|
|
1780
1828
|
const slotsByCapacity = Math.max(0, maxConcurrentWorkers - activeWorkers);
|
|
1781
1829
|
const slotsByFreeMem = capacityFromFree;
|
|
1782
1830
|
let slotsAvailable = Math.min(slotsByCapacity, slotsByFreeMem);
|
|
1831
|
+
const skipDisk = input.skipDiskGate || process.env.KYNVER_RESOURCE_GATE_SKIP_DISK === "1";
|
|
1832
|
+
const diskGate = skipDisk ? void 0 : observeRunnerDiskGate({
|
|
1833
|
+
diskPath: input.diskPath?.trim() || process.env.KYNVER_DISK_GUARD_PATH?.trim() || "/"
|
|
1834
|
+
});
|
|
1835
|
+
if (diskGate && !diskGate.ok) slotsAvailable = 0;
|
|
1783
1836
|
let reason = null;
|
|
1784
1837
|
if (slotsAvailable <= 0) {
|
|
1785
|
-
if (
|
|
1838
|
+
if (diskGate && !diskGate.ok) {
|
|
1839
|
+
reason = diskGate.reason ?? "disk gate blocked worker admission";
|
|
1840
|
+
} else if (activeWorkers >= maxConcurrentWorkers) {
|
|
1786
1841
|
reason = `at worker limit (${activeWorkers}/${maxConcurrentWorkers} running)`;
|
|
1787
1842
|
} else if (capacityFromFree <= 0) {
|
|
1788
1843
|
reason = "insufficient free memory \u2014 waiting for workers to finish";
|
|
@@ -1802,7 +1857,8 @@ function observeRunnerResourceGate(input) {
|
|
|
1802
1857
|
maxConcurrentWorkers,
|
|
1803
1858
|
activeWorkers,
|
|
1804
1859
|
slotsAvailable,
|
|
1805
|
-
reason
|
|
1860
|
+
reason,
|
|
1861
|
+
...diskGate ? { diskGate } : {}
|
|
1806
1862
|
};
|
|
1807
1863
|
}
|
|
1808
1864
|
|
|
@@ -1990,10 +2046,76 @@ var claudeProvider = {
|
|
|
1990
2046
|
}
|
|
1991
2047
|
};
|
|
1992
2048
|
|
|
2049
|
+
// src/worker-provider-policy.ts
|
|
2050
|
+
var DEFAULT_WORKER_PROVIDER = "cursor";
|
|
2051
|
+
var CLAUDE_FAMILY = /* @__PURE__ */ new Set(["claude", "opus", "anthropic"]);
|
|
2052
|
+
var TASK_OVERRIDE_MARKERS = [
|
|
2053
|
+
/\[worker-provider:\s*claude\]/i,
|
|
2054
|
+
/\[use-claude-worker\]/i,
|
|
2055
|
+
/\[operator-worker-provider:\s*claude\]/i
|
|
2056
|
+
];
|
|
2057
|
+
function taskString2(task, key) {
|
|
2058
|
+
const v = task[key];
|
|
2059
|
+
return typeof v === "string" ? v.trim() : "";
|
|
2060
|
+
}
|
|
2061
|
+
function isClaudeFamilyProvider(provider) {
|
|
2062
|
+
if (!provider?.trim()) return false;
|
|
2063
|
+
const normalized = provider.trim().toLowerCase();
|
|
2064
|
+
if (CLAUDE_FAMILY.has(normalized)) return true;
|
|
2065
|
+
return normalized.includes("claude") || normalized.includes("opus");
|
|
2066
|
+
}
|
|
2067
|
+
function taskAllowsClaudeWorker(task) {
|
|
2068
|
+
if (!task) return false;
|
|
2069
|
+
const override = task.workerProviderOverride;
|
|
2070
|
+
if (typeof override === "string" && isClaudeFamilyProvider(override)) return true;
|
|
2071
|
+
const ref = taskString2(task, "executorRef").toLowerCase();
|
|
2072
|
+
if (ref === "provider:claude" || ref.startsWith("provider:claude:")) return true;
|
|
2073
|
+
if (ref.includes("claude-worker-override") || ref.includes("operator-claude")) return true;
|
|
2074
|
+
const description = taskString2(task, "description");
|
|
2075
|
+
if (TASK_OVERRIDE_MARKERS.some((re) => re.test(description))) return true;
|
|
2076
|
+
const title = taskString2(task, "title");
|
|
2077
|
+
if (/\[use-claude-worker\]/i.test(title)) return true;
|
|
2078
|
+
return false;
|
|
2079
|
+
}
|
|
2080
|
+
function coerceCursorModel(model, ruleSuffix) {
|
|
2081
|
+
const coerced = {
|
|
2082
|
+
provider: DEFAULT_WORKER_PROVIDER,
|
|
2083
|
+
model: CURSOR_DEFAULT_MODEL,
|
|
2084
|
+
rule: `policy:cursor_default${ruleSuffix}`,
|
|
2085
|
+
requestedModel: model
|
|
2086
|
+
};
|
|
2087
|
+
return coerced;
|
|
2088
|
+
}
|
|
2089
|
+
function enforceCursorWorkerProvider(input) {
|
|
2090
|
+
const { routing, task } = input;
|
|
2091
|
+
const explicit = input.explicitProvider?.trim().toLowerCase();
|
|
2092
|
+
if (input.explicitProviderIsOperatorOverride && isClaudeFamilyProvider(explicit)) {
|
|
2093
|
+
return {
|
|
2094
|
+
...routing,
|
|
2095
|
+
provider: "claude",
|
|
2096
|
+
rule: routing.rule.startsWith("explicit:") ? routing.rule : "explicit:operator_provider"
|
|
2097
|
+
};
|
|
2098
|
+
}
|
|
2099
|
+
if (taskAllowsClaudeWorker(task)) {
|
|
2100
|
+
return routing;
|
|
2101
|
+
}
|
|
2102
|
+
if (!isClaudeFamilyProvider(routing.provider)) {
|
|
2103
|
+
return routing;
|
|
2104
|
+
}
|
|
2105
|
+
const suffix = routing.rule && routing.rule !== "default:global" ? `:${routing.rule.replace(/:/g, "_")}` : "";
|
|
2106
|
+
return coerceCursorModel(routing.model, suffix);
|
|
2107
|
+
}
|
|
2108
|
+
function resolveConfiguredWorkerProvider(configured, fallback = DEFAULT_WORKER_PROVIDER) {
|
|
2109
|
+
const trimmed = configured?.trim();
|
|
2110
|
+
if (!trimmed) return fallback;
|
|
2111
|
+
if (isClaudeFamilyProvider(trimmed)) return DEFAULT_WORKER_PROVIDER;
|
|
2112
|
+
return trimmed;
|
|
2113
|
+
}
|
|
2114
|
+
|
|
1993
2115
|
// src/model-routing.ts
|
|
1994
2116
|
var GLOBAL_DEFAULT_MODEL = "claude-sonnet-4-6";
|
|
1995
2117
|
var CURSOR_DEFAULT_MODEL = "composer-2.5";
|
|
1996
|
-
function
|
|
2118
|
+
function taskString3(task, key) {
|
|
1997
2119
|
const v = task[key];
|
|
1998
2120
|
return typeof v === "string" ? v.trim() : "";
|
|
1999
2121
|
}
|
|
@@ -2009,11 +2131,14 @@ function resolveGlobalDefaultModel(config = loadUserConfig()) {
|
|
|
2009
2131
|
}
|
|
2010
2132
|
function inferProviderFromModel(model) {
|
|
2011
2133
|
const m = (model ?? "").toLowerCase();
|
|
2012
|
-
if (!m) return "
|
|
2134
|
+
if (!m) return "cursor";
|
|
2013
2135
|
if (m.includes("composer") || m.includes("cursor") || m.includes("codex") || m.startsWith("gpt-") || m.startsWith("gpt5")) {
|
|
2014
2136
|
return "cursor";
|
|
2015
2137
|
}
|
|
2016
|
-
|
|
2138
|
+
if (/^claude[-_]/i.test(m) || /^(?:opus|sonnet|haiku)\b/i.test(m)) {
|
|
2139
|
+
return "claude";
|
|
2140
|
+
}
|
|
2141
|
+
return "cursor";
|
|
2017
2142
|
}
|
|
2018
2143
|
function normalizeProviderAliasModel(model, explicitProvider) {
|
|
2019
2144
|
const alias = model.trim().toLowerCase();
|
|
@@ -2047,41 +2172,33 @@ function isOpusLane(ref, title) {
|
|
|
2047
2172
|
return false;
|
|
2048
2173
|
}
|
|
2049
2174
|
function inferModelRoutingFromTask(task) {
|
|
2050
|
-
const ref = normalizeRef(
|
|
2051
|
-
const title =
|
|
2052
|
-
const priority =
|
|
2053
|
-
const roleLane = normalizeRef(
|
|
2175
|
+
const ref = normalizeRef(taskString3(task, "executorRef"));
|
|
2176
|
+
const title = taskString3(task, "title").toLowerCase();
|
|
2177
|
+
const priority = taskString3(task, "priority") || "normal";
|
|
2178
|
+
const roleLane = normalizeRef(taskString3(task, "roleLane"));
|
|
2054
2179
|
if (ref.includes("cursor") || ref.includes("codex") || ref.includes("composer") || ref.includes("copilot") || roleLane === "implementer" || roleLane === "repair_implementer") {
|
|
2055
2180
|
return { provider: "cursor", rule: "lane:implementation" };
|
|
2056
2181
|
}
|
|
2057
2182
|
if (ref.includes("landing") || title.startsWith("land:") || title.includes(" merge")) {
|
|
2058
|
-
return {
|
|
2059
|
-
model: "claude-haiku-4-5-20251001",
|
|
2060
|
-
provider: "claude",
|
|
2061
|
-
rule: "lane:landing"
|
|
2062
|
-
};
|
|
2183
|
+
return { provider: "cursor", rule: "lane:landing" };
|
|
2063
2184
|
}
|
|
2064
2185
|
if (ref.includes("review") || /^review[\s:]/.test(title) || roleLane.includes("review")) {
|
|
2065
2186
|
if (isOpusLane(ref, title) || roleLane === "deep_reviewer") {
|
|
2066
|
-
return {
|
|
2187
|
+
return { provider: "cursor", rule: "lane:deep_review" };
|
|
2067
2188
|
}
|
|
2068
|
-
return {
|
|
2189
|
+
return { provider: "cursor", rule: "lane:review" };
|
|
2069
2190
|
}
|
|
2070
2191
|
if (isOpusLane(ref, title) || roleLane === "plan_author") {
|
|
2071
|
-
return {
|
|
2192
|
+
return { provider: "cursor", rule: "lane:planning" };
|
|
2072
2193
|
}
|
|
2073
2194
|
if (priority === "critical") {
|
|
2074
|
-
return {
|
|
2195
|
+
return { provider: "cursor", rule: "priority:critical" };
|
|
2075
2196
|
}
|
|
2076
2197
|
if (priority === "high") {
|
|
2077
|
-
return {
|
|
2198
|
+
return { provider: "cursor", rule: "priority:high" };
|
|
2078
2199
|
}
|
|
2079
2200
|
if (priority === "low") {
|
|
2080
|
-
return {
|
|
2081
|
-
model: "claude-haiku-4-5-20251001",
|
|
2082
|
-
provider: "claude",
|
|
2083
|
-
rule: "priority:low"
|
|
2084
|
-
};
|
|
2201
|
+
return { provider: "cursor", rule: "priority:low" };
|
|
2085
2202
|
}
|
|
2086
2203
|
const model = resolveGlobalDefaultModel();
|
|
2087
2204
|
return {
|
|
@@ -2091,31 +2208,41 @@ function inferModelRoutingFromTask(task) {
|
|
|
2091
2208
|
};
|
|
2092
2209
|
}
|
|
2093
2210
|
function resolveWorkerLaunch(input) {
|
|
2211
|
+
let decision;
|
|
2094
2212
|
if (input.explicitModel?.trim()) {
|
|
2095
|
-
const
|
|
2096
|
-
const providerAlias = normalizeProviderAliasModel(
|
|
2097
|
-
if (providerAlias)
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2213
|
+
const model = input.explicitModel.trim();
|
|
2214
|
+
const providerAlias = normalizeProviderAliasModel(model, input.explicitProvider);
|
|
2215
|
+
if (providerAlias) {
|
|
2216
|
+
decision = providerAlias;
|
|
2217
|
+
} else {
|
|
2218
|
+
decision = {
|
|
2219
|
+
model,
|
|
2220
|
+
provider: input.explicitProvider?.trim() || inferProviderFromModel(model),
|
|
2221
|
+
rule: "explicit:cli",
|
|
2222
|
+
requestedModel: model
|
|
2223
|
+
};
|
|
2224
|
+
}
|
|
2225
|
+
} else if (input.task && Object.keys(input.task).length > 0) {
|
|
2106
2226
|
const inferred = inferModelRoutingFromTask(input.task);
|
|
2107
|
-
|
|
2227
|
+
decision = {
|
|
2108
2228
|
...inferred,
|
|
2109
2229
|
requestedModel: inferred.model
|
|
2110
2230
|
};
|
|
2231
|
+
} else {
|
|
2232
|
+
const model = resolveGlobalDefaultModel();
|
|
2233
|
+
decision = {
|
|
2234
|
+
model,
|
|
2235
|
+
provider: input.explicitProvider?.trim() || inferProviderFromModel(model),
|
|
2236
|
+
rule: "default:global",
|
|
2237
|
+
requestedModel: model
|
|
2238
|
+
};
|
|
2111
2239
|
}
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
};
|
|
2240
|
+
return enforceCursorWorkerProvider({
|
|
2241
|
+
routing: decision,
|
|
2242
|
+
task: input.task,
|
|
2243
|
+
explicitProvider: input.explicitProvider,
|
|
2244
|
+
explicitProviderIsOperatorOverride: input.explicitProviderIsOperatorOverride
|
|
2245
|
+
});
|
|
2119
2246
|
}
|
|
2120
2247
|
function resolveModelFallback(startedModel, launchModel, providerDefault) {
|
|
2121
2248
|
return startedModel || launchModel || providerDefault || resolveGlobalDefaultModel() || CLAUDE_DEFAULT_MODEL;
|
|
@@ -2207,6 +2334,76 @@ function hasLiveWorkerForTask(runId, taskId) {
|
|
|
2207
2334
|
import { existsSync as existsSync12, mkdirSync as mkdirSync3 } from "node:fs";
|
|
2208
2335
|
import path14 from "node:path";
|
|
2209
2336
|
|
|
2337
|
+
// src/harness-repair-target.ts
|
|
2338
|
+
var HARNESS_CONTRACT_RE = /<!--\s*harness-contract:\s*(\{[\s\S]*?\})\s*-->/i;
|
|
2339
|
+
var FIX_EXECUTOR_REF_PREFIX = "next-action-fix:";
|
|
2340
|
+
function trimOrNull4(value) {
|
|
2341
|
+
if (typeof value !== "string") return null;
|
|
2342
|
+
const t = value.trim();
|
|
2343
|
+
return t.length ? t : null;
|
|
2344
|
+
}
|
|
2345
|
+
function normalizePrUrl2(url) {
|
|
2346
|
+
const m = url.trim().match(/github\.com\/([^/]+\/[^/]+)\/(?:pull|pulls)\/(\d+)/i);
|
|
2347
|
+
if (!m) return trimOrNull4(url);
|
|
2348
|
+
return `https://github.com/${m[1]}/pull/${m[2]}`;
|
|
2349
|
+
}
|
|
2350
|
+
function isHarnessRepairTask(task) {
|
|
2351
|
+
const title = (task.title ?? "").trim().toLowerCase();
|
|
2352
|
+
if (title.startsWith("fix:") || title.startsWith("repair:")) return true;
|
|
2353
|
+
const ref = (task.executorRef ?? "").toLowerCase();
|
|
2354
|
+
if (ref.startsWith(FIX_EXECUTOR_REF_PREFIX)) return true;
|
|
2355
|
+
if (ref.includes("repair") || ref.includes("unblock")) return true;
|
|
2356
|
+
return false;
|
|
2357
|
+
}
|
|
2358
|
+
function parseRepairTargetContractFromDescription(description) {
|
|
2359
|
+
const empty = {
|
|
2360
|
+
repairEnforceOriginalPr: false,
|
|
2361
|
+
targetPrUrl: null,
|
|
2362
|
+
targetPrBranch: null
|
|
2363
|
+
};
|
|
2364
|
+
if (!description) return empty;
|
|
2365
|
+
const m = description.match(HARNESS_CONTRACT_RE);
|
|
2366
|
+
if (!m?.[1]) return empty;
|
|
2367
|
+
try {
|
|
2368
|
+
const parsed = JSON.parse(m[1]);
|
|
2369
|
+
const url = trimOrNull4(
|
|
2370
|
+
String(parsed.targetPrUrl ?? parsed.target_pr_url ?? "")
|
|
2371
|
+
);
|
|
2372
|
+
const branch = trimOrNull4(
|
|
2373
|
+
String(parsed.targetPrBranch ?? parsed.target_pr_branch ?? "")
|
|
2374
|
+
);
|
|
2375
|
+
const enforce = parsed.repairEnforceOriginalPr === true || parsed.repair_enforce_original_pr === true;
|
|
2376
|
+
return {
|
|
2377
|
+
repairEnforceOriginalPr: enforce || Boolean(url),
|
|
2378
|
+
targetPrUrl: url ? normalizePrUrl2(url) : null,
|
|
2379
|
+
targetPrBranch: branch
|
|
2380
|
+
};
|
|
2381
|
+
} catch {
|
|
2382
|
+
return empty;
|
|
2383
|
+
}
|
|
2384
|
+
}
|
|
2385
|
+
function resolveHarnessRepairTargetFromTask(task) {
|
|
2386
|
+
if (!isHarnessRepairTask(task)) return null;
|
|
2387
|
+
const block = parseRepairTargetContractFromDescription(task.description);
|
|
2388
|
+
const taskPr = task.prUrl ? normalizePrUrl2(task.prUrl) : null;
|
|
2389
|
+
const targetPrUrl = block.targetPrUrl ?? taskPr;
|
|
2390
|
+
if (!targetPrUrl) return null;
|
|
2391
|
+
return {
|
|
2392
|
+
targetPrUrl,
|
|
2393
|
+
targetPrBranch: block.targetPrBranch ?? trimOrNull4(task.branch)
|
|
2394
|
+
};
|
|
2395
|
+
}
|
|
2396
|
+
function repairTargetPromptLines(target) {
|
|
2397
|
+
return [
|
|
2398
|
+
"Repair target PR policy:",
|
|
2399
|
+
`- Work on the existing target PR branch \u2014 do not open a duplicate repair PR by default.`,
|
|
2400
|
+
`- Canonical target PR: ${target.targetPrUrl}`,
|
|
2401
|
+
...target.targetPrBranch ? [`- Canonical target branch: \`${target.targetPrBranch}\` (checkout is already on this branch).`] : [],
|
|
2402
|
+
`- Reconcile ${target.targetPrUrl} in structured finalResult.targetPrReconciliation.`,
|
|
2403
|
+
`- Only supersede the original when the branch is inaccessible: set supersedesOriginalTargetPr: true with reason and close/comment on the original PR.`
|
|
2404
|
+
];
|
|
2405
|
+
}
|
|
2406
|
+
|
|
2210
2407
|
// src/prompt.ts
|
|
2211
2408
|
function buildPrompt(input) {
|
|
2212
2409
|
const ownership = input.ownedPaths.length ? `Owned paths: ${input.ownedPaths.join(", ")}. Do not edit outside these paths without stopping and reporting why.` : "Owned paths: unrestricted for this worker, but keep edits tightly scoped.";
|
|
@@ -2251,7 +2448,7 @@ function buildPrompt(input) {
|
|
|
2251
2448
|
`Progress heartbeat file: ${input.heartbeatPath}`,
|
|
2252
2449
|
"After each major step, append one JSON line to the heartbeat file with fields: ts, phase, summary, changedFiles, blocker.",
|
|
2253
2450
|
"Final response must include files changed, verification commands, and unresolved risks.",
|
|
2254
|
-
"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.",
|
|
2451
|
+
"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. Persona-attributed tasks: put repeatable lane lessons in lessonsLearned/laneGuidance (with evidence); substantive rows auto-persist as persona-scoped Lane A rules \u2014 global cross-lane policy stays in owner memory, not worker lessons.",
|
|
2255
2452
|
"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.",
|
|
2256
2453
|
"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.",
|
|
2257
2454
|
"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.",
|
|
@@ -2268,6 +2465,13 @@ function buildPrompt(input) {
|
|
|
2268
2465
|
"",
|
|
2269
2466
|
...input.personaMarkdown?.trim() ? [input.personaMarkdown.trim(), ""] : [],
|
|
2270
2467
|
...input.instructionPolicyMarkdown?.trim() ? ["Operating rules (Lane A \u2014 from AgentOS memory policy):", input.instructionPolicyMarkdown.trim(), ""] : [],
|
|
2468
|
+
...input.repairTargetPrUrl ? [
|
|
2469
|
+
...repairTargetPromptLines({
|
|
2470
|
+
targetPrUrl: input.repairTargetPrUrl,
|
|
2471
|
+
targetPrBranch: input.repairTargetBranch ?? null
|
|
2472
|
+
}),
|
|
2473
|
+
""
|
|
2474
|
+
] : [],
|
|
2271
2475
|
"Task:",
|
|
2272
2476
|
input.task
|
|
2273
2477
|
].join("\n");
|
|
@@ -2433,7 +2637,15 @@ var BUILTIN = {
|
|
|
2433
2637
|
var overrideProvider = null;
|
|
2434
2638
|
function resolveWorkerProvider(name) {
|
|
2435
2639
|
if (overrideProvider) return overrideProvider;
|
|
2436
|
-
const
|
|
2640
|
+
const explicit = name?.trim();
|
|
2641
|
+
if (explicit) {
|
|
2642
|
+
const provider2 = BUILTIN[explicit];
|
|
2643
|
+
if (!provider2) {
|
|
2644
|
+
throw new Error(`unknown worker provider "${explicit}" \u2014 supported: ${Object.keys(BUILTIN).join(", ")}`);
|
|
2645
|
+
}
|
|
2646
|
+
return provider2;
|
|
2647
|
+
}
|
|
2648
|
+
const configured = resolveConfiguredWorkerProvider(loadUserConfig().workerProvider);
|
|
2437
2649
|
const provider = BUILTIN[configured];
|
|
2438
2650
|
if (!provider) {
|
|
2439
2651
|
throw new Error(`unknown worker provider "${configured}" \u2014 supported: ${Object.keys(BUILTIN).join(", ")}`);
|
|
@@ -2529,7 +2741,7 @@ var NO_PR_COMMITS_BETWEEN_RE = /no commits between/i;
|
|
|
2529
2741
|
function isGhNoCommitsBetweenError(detail) {
|
|
2530
2742
|
return Boolean(detail && NO_PR_COMMITS_BETWEEN_RE.test(detail));
|
|
2531
2743
|
}
|
|
2532
|
-
function
|
|
2744
|
+
function trimOrNull5(value) {
|
|
2533
2745
|
if (typeof value !== "string") return null;
|
|
2534
2746
|
const trimmed = value.trim();
|
|
2535
2747
|
return trimmed.length ? trimmed : null;
|
|
@@ -2537,7 +2749,7 @@ function trimOrNull4(value) {
|
|
|
2537
2749
|
function committedHead(ancestry) {
|
|
2538
2750
|
if (!ancestry?.checked) return null;
|
|
2539
2751
|
if (ancestry.headIsAncestorOfBase !== false) return null;
|
|
2540
|
-
return
|
|
2752
|
+
return trimOrNull5(ancestry.head);
|
|
2541
2753
|
}
|
|
2542
2754
|
function extractPrUrlFromText(value) {
|
|
2543
2755
|
if (value === void 0 || value === null) return null;
|
|
@@ -2545,7 +2757,7 @@ function extractPrUrlFromText(value) {
|
|
|
2545
2757
|
const m = text.match(
|
|
2546
2758
|
/https?:\/\/[^\s)>"]+\/(?:pull|pulls|merge_requests|pull-requests)\/\d+/i
|
|
2547
2759
|
);
|
|
2548
|
-
return m ?
|
|
2760
|
+
return m ? trimOrNull5(m[0]) : null;
|
|
2549
2761
|
}
|
|
2550
2762
|
function countCommitsAheadOfBase(worktreePath, baseRef, exec) {
|
|
2551
2763
|
const base = baseRef.trim();
|
|
@@ -2557,21 +2769,21 @@ function countCommitsAheadOfBase(worktreePath, baseRef, exec) {
|
|
|
2557
2769
|
}
|
|
2558
2770
|
function isReviewArtifactWorker(worker, snapshot) {
|
|
2559
2771
|
if (snapshot.changedFiles.length > 0) return false;
|
|
2560
|
-
const persona =
|
|
2772
|
+
const persona = trimOrNull5(worker.personaSlug)?.toLowerCase();
|
|
2561
2773
|
if (persona && REVIEW_PERSONA_SLUGS.has(persona)) return true;
|
|
2562
|
-
const rule =
|
|
2774
|
+
const rule = trimOrNull5(worker.routingRule) ?? "";
|
|
2563
2775
|
if (rule && REVIEW_LANE_RULE.test(rule)) return true;
|
|
2564
2776
|
return false;
|
|
2565
2777
|
}
|
|
2566
2778
|
function hasWorkProduct(snapshot, options) {
|
|
2567
2779
|
if (snapshot.changedFiles.length > 0) return true;
|
|
2568
|
-
const baseRef =
|
|
2780
|
+
const baseRef = trimOrNull5(options?.baseRef);
|
|
2569
2781
|
if (baseRef && options?.exec && options.worktreePath) {
|
|
2570
2782
|
const ahead = countCommitsAheadOfBase(options.worktreePath, baseRef, options.exec);
|
|
2571
2783
|
if (ahead === 0) return false;
|
|
2572
2784
|
if (ahead !== null && ahead > 0) return true;
|
|
2573
2785
|
}
|
|
2574
|
-
if (
|
|
2786
|
+
if (trimOrNull5(snapshot.headCommit)) return true;
|
|
2575
2787
|
if (committedHead(snapshot.gitAncestry)) return true;
|
|
2576
2788
|
return false;
|
|
2577
2789
|
}
|
|
@@ -2587,7 +2799,7 @@ function assessPrHandoffRequirement(input) {
|
|
|
2587
2799
|
})) {
|
|
2588
2800
|
return { required: false, reason: "expert_review_task" };
|
|
2589
2801
|
}
|
|
2590
|
-
const rule =
|
|
2802
|
+
const rule = trimOrNull5(input.routingRule) ?? "";
|
|
2591
2803
|
if (rule && REVIEW_LANE_RULE.test(rule)) {
|
|
2592
2804
|
return { required: false, reason: "review_lane" };
|
|
2593
2805
|
}
|
|
@@ -2598,10 +2810,14 @@ function assessPrHandoffRequirement(input) {
|
|
|
2598
2810
|
if (isReviewArtifactWorker(workerCtx, input.snapshot)) {
|
|
2599
2811
|
return { required: false, reason: "review_artifact" };
|
|
2600
2812
|
}
|
|
2601
|
-
if (
|
|
2813
|
+
if (trimOrNull5(input.patchPath) || trimOrNull5(input.artifactBundlePath)) {
|
|
2602
2814
|
return { required: false, reason: "patch_or_bundle" };
|
|
2603
2815
|
}
|
|
2604
|
-
const
|
|
2816
|
+
const repairTarget = trimOrNull5(input.repairTargetPrUrl);
|
|
2817
|
+
if (repairTarget) {
|
|
2818
|
+
return { required: false, reason: "repair_target_pr" };
|
|
2819
|
+
}
|
|
2820
|
+
const prUrl = trimOrNull5(input.prUrl) ?? trimOrNull5(input.taskPrUrl) ?? trimOrNull5(input.snapshot.prUrl);
|
|
2605
2821
|
if (prUrl) {
|
|
2606
2822
|
return { required: false, reason: "already_has_pr" };
|
|
2607
2823
|
}
|
|
@@ -2622,8 +2838,8 @@ function buildPrHandoffSnapshotFromStatus(status, extras) {
|
|
|
2622
2838
|
worktreePath: status.worktreePath,
|
|
2623
2839
|
gitAncestry: status.gitAncestry,
|
|
2624
2840
|
finalResult: status.finalResult,
|
|
2625
|
-
headCommit:
|
|
2626
|
-
prUrl:
|
|
2841
|
+
headCommit: trimOrNull5(extras?.headCommit) ?? committedHead(status.gitAncestry),
|
|
2842
|
+
prUrl: trimOrNull5(extras?.prUrl) ?? null
|
|
2627
2843
|
};
|
|
2628
2844
|
}
|
|
2629
2845
|
|
|
@@ -2829,6 +3045,7 @@ function ensurePrReadyHandoff(input, exec = defaultPrHandoffExec) {
|
|
|
2829
3045
|
executorRef: input.worker.executorRef,
|
|
2830
3046
|
parentTaskId: input.worker.parentTaskId,
|
|
2831
3047
|
taskPrUrl: input.worker.taskPrUrl,
|
|
3048
|
+
repairTargetPrUrl: input.worker.repairTargetPrUrl,
|
|
2832
3049
|
baseRef,
|
|
2833
3050
|
exec,
|
|
2834
3051
|
worker: input.worker,
|
|
@@ -2857,6 +3074,48 @@ function ensurePrReadyHandoff(input, exec = defaultPrHandoffExec) {
|
|
|
2857
3074
|
nextAction: "Ensure `origin` points at GitHub, push the branch, open a PR, and rerun `kynver worker complete`."
|
|
2858
3075
|
};
|
|
2859
3076
|
}
|
|
3077
|
+
const repairTarget = input.worker.repairTargetPrUrl?.trim();
|
|
3078
|
+
if (repairTarget) {
|
|
3079
|
+
let committed2 = false;
|
|
3080
|
+
let pushed2 = false;
|
|
3081
|
+
let headCommit2 = snapshot.headCommit ?? resolveHeadCommit(snapshot.worktreePath, exec) ?? void 0;
|
|
3082
|
+
if (snapshot.changedFiles.length > 0) {
|
|
3083
|
+
const pushResult2 = commitAndPushBranch({
|
|
3084
|
+
worktreePath: snapshot.worktreePath,
|
|
3085
|
+
branch: snapshot.branch,
|
|
3086
|
+
commitMessage: `fix(harness): repair target PR ${repairTarget}`,
|
|
3087
|
+
hasDirtyFiles: true,
|
|
3088
|
+
exec
|
|
3089
|
+
});
|
|
3090
|
+
if (!pushResult2.ok) {
|
|
3091
|
+
return {
|
|
3092
|
+
ok: false,
|
|
3093
|
+
reason: `PR-ready handoff blocked: ${pushResult2.detail ?? "git commit/push failed"}`,
|
|
3094
|
+
nextAction: "Commit and push to the target PR branch, then rerun `kynver worker complete`."
|
|
3095
|
+
};
|
|
3096
|
+
}
|
|
3097
|
+
committed2 = pushResult2.committed;
|
|
3098
|
+
pushed2 = pushResult2.pushed;
|
|
3099
|
+
headCommit2 = pushResult2.headCommit ?? headCommit2;
|
|
3100
|
+
} else {
|
|
3101
|
+
const pushOnly = exec.git(snapshot.worktreePath, ["push", "-u", "origin", snapshot.branch]);
|
|
3102
|
+
if (pushOnly.status !== 0 && !/already up to date/i.test(pushOnly.stderr || pushOnly.stdout)) {
|
|
3103
|
+
return {
|
|
3104
|
+
ok: false,
|
|
3105
|
+
reason: `PR-ready handoff blocked: ${pushOnly.stderr || pushOnly.stdout || "git push failed"}`,
|
|
3106
|
+
nextAction: "Push the target branch to origin, then rerun `kynver worker complete`."
|
|
3107
|
+
};
|
|
3108
|
+
}
|
|
3109
|
+
pushed2 = pushOnly.status === 0;
|
|
3110
|
+
}
|
|
3111
|
+
return {
|
|
3112
|
+
ok: true,
|
|
3113
|
+
prUrl: repairTarget,
|
|
3114
|
+
headCommit: headCommit2,
|
|
3115
|
+
committed: committed2,
|
|
3116
|
+
pushed: pushed2
|
|
3117
|
+
};
|
|
3118
|
+
}
|
|
2860
3119
|
const existing = findOpenPrUrl(snapshot.worktreePath, repo, snapshot.branch, exec);
|
|
2861
3120
|
if (existing) {
|
|
2862
3121
|
return {
|
|
@@ -2984,13 +3243,13 @@ function dirtyPathsCoveredByDisposableRemoval(changedFiles, removed) {
|
|
|
2984
3243
|
if (removed.length === 0) return false;
|
|
2985
3244
|
const removedSet = new Set(removed.map((p) => normalizeRelativePath(p)));
|
|
2986
3245
|
return material.every((line) => {
|
|
2987
|
-
const
|
|
2988
|
-
return removedSet.has(
|
|
3246
|
+
const path50 = normalizeRelativePath(pathFromGitStatusLine(line));
|
|
3247
|
+
return removedSet.has(path50);
|
|
2989
3248
|
});
|
|
2990
3249
|
}
|
|
2991
3250
|
|
|
2992
3251
|
// src/worktree-completion-handoff.ts
|
|
2993
|
-
function
|
|
3252
|
+
function trimOrNull6(value) {
|
|
2994
3253
|
if (typeof value !== "string") return null;
|
|
2995
3254
|
const t = value.trim();
|
|
2996
3255
|
return t.length ? t : null;
|
|
@@ -3017,7 +3276,7 @@ function assessWorktreeCompletionHandoff(input) {
|
|
|
3017
3276
|
const materialDirty = materialWorktreeChanges(rawDirty);
|
|
3018
3277
|
const removed = mergedDisposableRemoved(input);
|
|
3019
3278
|
const effectivelyClean = materialDirty.length === 0 || dirtyPathsCoveredByDisposableRemoval(rawDirty, removed);
|
|
3020
|
-
if (
|
|
3279
|
+
if (trimOrNull6(input.prUrl)) {
|
|
3021
3280
|
if (!effectivelyClean) {
|
|
3022
3281
|
return {
|
|
3023
3282
|
allowed: false,
|
|
@@ -3028,7 +3287,7 @@ function assessWorktreeCompletionHandoff(input) {
|
|
|
3028
3287
|
}
|
|
3029
3288
|
return { allowed: true, state: "pr_handoff", materialDirtyCount: 0 };
|
|
3030
3289
|
}
|
|
3031
|
-
if (
|
|
3290
|
+
if (trimOrNull6(input.headCommit)) {
|
|
3032
3291
|
if (!effectivelyClean) {
|
|
3033
3292
|
return {
|
|
3034
3293
|
allowed: false,
|
|
@@ -3039,7 +3298,7 @@ function assessWorktreeCompletionHandoff(input) {
|
|
|
3039
3298
|
}
|
|
3040
3299
|
return { allowed: true, state: "commit_handoff", materialDirtyCount: 0 };
|
|
3041
3300
|
}
|
|
3042
|
-
if (
|
|
3301
|
+
if (trimOrNull6(input.artifactBundlePath) || trimOrNull6(input.patchPath)) {
|
|
3043
3302
|
if (!effectivelyClean) {
|
|
3044
3303
|
return {
|
|
3045
3304
|
allowed: false,
|
|
@@ -3769,6 +4028,19 @@ function spawnCompletionSidecar(opts) {
|
|
|
3769
4028
|
}
|
|
3770
4029
|
}
|
|
3771
4030
|
|
|
4031
|
+
// src/repair-target-worktree.ts
|
|
4032
|
+
function addWorktreeForRepairBranch(repo, worktreePath, branch) {
|
|
4033
|
+
git(repo, ["fetch", "origin", branch, "--prune"], { allowFailure: true });
|
|
4034
|
+
const remoteRef = `origin/${branch}`;
|
|
4035
|
+
const added = gitCapture(repo, ["worktree", "add", "-B", branch, worktreePath, remoteRef]);
|
|
4036
|
+
if (added.status === 0) return;
|
|
4037
|
+
const fallback = gitCapture(repo, ["worktree", "add", "-b", branch, worktreePath, "HEAD"]);
|
|
4038
|
+
if (fallback.status !== 0) {
|
|
4039
|
+
const detail = added.stderr || added.stdout || fallback.stderr || fallback.stdout || "git worktree add failed for repair target branch";
|
|
4040
|
+
throw new Error(detail);
|
|
4041
|
+
}
|
|
4042
|
+
}
|
|
4043
|
+
|
|
3772
4044
|
// src/supervisor.ts
|
|
3773
4045
|
function spawnWorkerProcess(run, opts) {
|
|
3774
4046
|
const rawName = typeof opts.name === "string" ? opts.name.trim() : "";
|
|
@@ -3779,13 +4051,14 @@ function spawnWorkerProcess(run, opts) {
|
|
|
3779
4051
|
if (run.workers?.[name]) throw new Error(`worker already exists in run ${run.id}: ${name}`);
|
|
3780
4052
|
if (!opts.task) throw new Error(`missing task text for worker ${name}`);
|
|
3781
4053
|
const routing = opts.routingRule || opts.requestedModel ? {
|
|
3782
|
-
provider: opts.provider ||
|
|
4054
|
+
provider: opts.provider || DEFAULT_WORKER_PROVIDER,
|
|
3783
4055
|
model: opts.model,
|
|
3784
4056
|
rule: opts.routingRule || "explicit:spawn",
|
|
3785
4057
|
requestedModel: opts.requestedModel ?? opts.model
|
|
3786
4058
|
} : resolveWorkerLaunch({
|
|
3787
4059
|
explicitModel: opts.model,
|
|
3788
|
-
explicitProvider: opts.provider
|
|
4060
|
+
explicitProvider: opts.provider,
|
|
4061
|
+
explicitProviderIsOperatorOverride: Boolean(opts.provider?.trim())
|
|
3789
4062
|
});
|
|
3790
4063
|
const provider = resolveWorkerProvider(routing.provider);
|
|
3791
4064
|
let launchModel = routing.model;
|
|
@@ -3805,10 +4078,15 @@ function spawnWorkerProcess(run, opts) {
|
|
|
3805
4078
|
const workerDir = path14.join(runDirectory(run.id), "workers", name);
|
|
3806
4079
|
mkdirSync3(workerDir, { recursive: true });
|
|
3807
4080
|
const worktreePath = path14.join(worktreesDir, run.id, name);
|
|
3808
|
-
const
|
|
4081
|
+
const repairBranch = opts.repairTargetBranch?.trim() || void 0;
|
|
4082
|
+
const branch = repairBranch || opts.branch || `agent/${run.id}/${name}`;
|
|
3809
4083
|
if (existsSync12(worktreePath)) throw new Error(`worktree path already exists: ${worktreePath}`);
|
|
3810
4084
|
git(run.repo, ["fetch", "origin", "--prune"], { allowFailure: true });
|
|
3811
|
-
|
|
4085
|
+
if (repairBranch) {
|
|
4086
|
+
addWorktreeForRepairBranch(run.repo, worktreePath, repairBranch);
|
|
4087
|
+
} else {
|
|
4088
|
+
git(run.repo, ["worktree", "add", "-b", branch, worktreePath, run.baseCommit], { throwError: true });
|
|
4089
|
+
}
|
|
3812
4090
|
const stdoutPath = path14.join(workerDir, "stdout.jsonl");
|
|
3813
4091
|
const stderrPath = path14.join(workerDir, "stderr.log");
|
|
3814
4092
|
const heartbeatPath = path14.join(workerDir, "heartbeat.jsonl");
|
|
@@ -3821,7 +4099,9 @@ function spawnWorkerProcess(run, opts) {
|
|
|
3821
4099
|
taskId: opts.taskId,
|
|
3822
4100
|
instructionPolicyMarkdown: opts.instructionPolicyMarkdown,
|
|
3823
4101
|
personaMarkdown: opts.personaMarkdown,
|
|
3824
|
-
model: launchModel
|
|
4102
|
+
model: launchModel,
|
|
4103
|
+
repairTargetPrUrl: opts.repairTargetPrUrl,
|
|
4104
|
+
repairTargetBranch: opts.repairTargetBranch ?? (repairBranch || void 0)
|
|
3825
4105
|
});
|
|
3826
4106
|
let started;
|
|
3827
4107
|
try {
|
|
@@ -3874,6 +4154,8 @@ function spawnWorkerProcess(run, opts) {
|
|
|
3874
4154
|
...opts.parentTaskId ? { parentTaskId: String(opts.parentTaskId) } : {},
|
|
3875
4155
|
...opts.taskTitle ? { taskTitle: String(opts.taskTitle) } : {},
|
|
3876
4156
|
...opts.taskPrUrl ? { taskPrUrl: String(opts.taskPrUrl) } : {},
|
|
4157
|
+
...opts.repairTargetPrUrl ? { repairTargetPrUrl: String(opts.repairTargetPrUrl) } : {},
|
|
4158
|
+
...opts.repairTargetBranch ? { repairTargetBranch: String(opts.repairTargetBranch) } : {},
|
|
3877
4159
|
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3878
4160
|
};
|
|
3879
4161
|
saveWorker(run.id, worker);
|
|
@@ -4545,6 +4827,24 @@ function extractPlanOutboxFromTask(task) {
|
|
|
4545
4827
|
};
|
|
4546
4828
|
}
|
|
4547
4829
|
|
|
4830
|
+
// src/runner-identity.ts
|
|
4831
|
+
import os3 from "node:os";
|
|
4832
|
+
function trimOrNull7(value) {
|
|
4833
|
+
if (!value?.trim()) return null;
|
|
4834
|
+
return value.trim();
|
|
4835
|
+
}
|
|
4836
|
+
function resolveRunnerPresencePayload(input = {}) {
|
|
4837
|
+
const env = input.env ?? process.env;
|
|
4838
|
+
const runnerId = trimOrNull7(env.KYNVER_RUNTIME_ID) ?? trimOrNull7(env.OPENCLAW_RUNTIME_ID) ?? trimOrNull7(env.HOSTNAME) ?? os3.hostname();
|
|
4839
|
+
return {
|
|
4840
|
+
runnerId,
|
|
4841
|
+
hostname: trimOrNull7(env.HOSTNAME) ?? os3.hostname(),
|
|
4842
|
+
profile: trimOrNull7(env.KYNVER_RUNNER_PROFILE) ?? trimOrNull7(env.OPENCLAW_RUNNER_PROFILE),
|
|
4843
|
+
harnessRepo: trimOrNull7(env.KYNVER_HARNESS_REPO) ?? trimOrNull7(env.KYNVER_DEFAULT_REPO),
|
|
4844
|
+
runId: input.runId ?? null
|
|
4845
|
+
};
|
|
4846
|
+
}
|
|
4847
|
+
|
|
4548
4848
|
// src/dispatch.ts
|
|
4549
4849
|
var DEFAULT_DISPATCH_LEASE_MS = 60 * 60 * 1e3;
|
|
4550
4850
|
function readHarnessWorkerContext(decision) {
|
|
@@ -4609,7 +4909,8 @@ async function dispatchRun(args) {
|
|
|
4609
4909
|
const secret = await resolveCallbackSecretWithMint(args.secret ? String(args.secret) : void 0, agentOsId, { baseUrl: base });
|
|
4610
4910
|
const execute = args.execute === true || args.execute === "true";
|
|
4611
4911
|
const dryRun = !execute;
|
|
4612
|
-
const
|
|
4912
|
+
const runnerPresence = resolveRunnerPresencePayload({ runId: run.id });
|
|
4913
|
+
const leaseOwner = `kynver-harness:${run.id}@runner:${runnerPresence.runnerId}`;
|
|
4613
4914
|
const runnerDiskGate = args.diskPath ? observeRunnerDiskGate({ diskPath: String(args.diskPath) }) : observeRunnerDiskGate({ diskPath: run.repo });
|
|
4614
4915
|
const runnerResourceGate = observeRunnerResourceGate({ runId: run.id });
|
|
4615
4916
|
const requestedStarts = Number(args.maxStarts) > 0 ? Math.floor(Number(args.maxStarts)) : 1;
|
|
@@ -4621,11 +4922,18 @@ async function dispatchRun(args) {
|
|
|
4621
4922
|
void 0
|
|
4622
4923
|
);
|
|
4623
4924
|
if (!worker?.taskId || !isPidAlive(worker.pid)) continue;
|
|
4925
|
+
const ownedPaths = Array.isArray(worker.ownedPaths) ? worker.ownedPaths.filter((p) => typeof p === "string") : [];
|
|
4926
|
+
const writeSetPrefixes = Array.isArray(
|
|
4927
|
+
worker.writeSetPrefixes
|
|
4928
|
+
) ? (worker.writeSetPrefixes ?? []).filter((p) => typeof p === "string") : [];
|
|
4624
4929
|
activeHarnessWorkers.push({
|
|
4625
4930
|
runId: run.id,
|
|
4626
4931
|
workerName: name,
|
|
4627
4932
|
taskId: worker.taskId,
|
|
4628
|
-
pid: worker.pid
|
|
4933
|
+
pid: worker.pid,
|
|
4934
|
+
...ownedPaths.length ? { ownedPaths } : {},
|
|
4935
|
+
...writeSetPrefixes.length ? { writeSetPrefixes } : {},
|
|
4936
|
+
...worker.allowConcurrentHotspot ? { allowConcurrentHotspot: true } : {}
|
|
4629
4937
|
});
|
|
4630
4938
|
}
|
|
4631
4939
|
const dispatchUrl = `${base}/api/agent-os/by-id/${encodeURIComponent(agentOsId)}/tasks/dispatch-next`;
|
|
@@ -4638,6 +4946,7 @@ async function dispatchRun(args) {
|
|
|
4638
4946
|
runnerDiskGate,
|
|
4639
4947
|
runnerResourceGate,
|
|
4640
4948
|
activeHarnessWorkers,
|
|
4949
|
+
runnerPresence,
|
|
4641
4950
|
harnessBoardSnapshot: buildRunBoard(run.id),
|
|
4642
4951
|
...args.lane ? { lane: String(args.lane) } : {},
|
|
4643
4952
|
executor: args.executor ? String(args.executor) : "harness",
|
|
@@ -4730,10 +5039,19 @@ async function dispatchRun(args) {
|
|
|
4730
5039
|
const name = safeSlug(`t-${task.id}-a${task.attempt}`);
|
|
4731
5040
|
const routing = resolveWorkerLaunch({
|
|
4732
5041
|
explicitModel: args.model ? String(args.model) : void 0,
|
|
5042
|
+
explicitProvider: args.provider ? String(args.provider) : void 0,
|
|
5043
|
+
explicitProviderIsOperatorOverride: Boolean(args.provider),
|
|
4733
5044
|
task: enrichTaskForModelRouting(task)
|
|
4734
5045
|
});
|
|
4735
5046
|
try {
|
|
4736
5047
|
const planId = task.planId ? String(task.planId) : void 0;
|
|
5048
|
+
const repairTarget = resolveHarnessRepairTargetFromTask({
|
|
5049
|
+
title: task.title ? String(task.title) : void 0,
|
|
5050
|
+
description: task.description ? String(task.description) : null,
|
|
5051
|
+
executorRef: task.executorRef ? String(task.executorRef) : null,
|
|
5052
|
+
prUrl: task.prUrl ? String(task.prUrl) : null,
|
|
5053
|
+
branch: task.branch ? String(task.branch) : null
|
|
5054
|
+
});
|
|
4737
5055
|
const worker = spawnWorkerProcess(run, {
|
|
4738
5056
|
name,
|
|
4739
5057
|
task: buildDispatchTaskText(task, agentOsId),
|
|
@@ -4745,10 +5063,13 @@ async function dispatchRun(args) {
|
|
|
4745
5063
|
agentOsId,
|
|
4746
5064
|
taskId: String(task.id),
|
|
4747
5065
|
planId,
|
|
5066
|
+
branch: repairTarget?.targetPrBranch ?? void 0,
|
|
4748
5067
|
executorRef: task.executorRef ? String(task.executorRef) : void 0,
|
|
4749
5068
|
parentTaskId: task.parentTaskId ? String(task.parentTaskId) : void 0,
|
|
4750
5069
|
taskTitle: task.title ? String(task.title) : void 0,
|
|
4751
|
-
taskPrUrl: task.prUrl ? String(task.prUrl) : void 0,
|
|
5070
|
+
taskPrUrl: repairTarget?.targetPrUrl ?? (task.prUrl ? String(task.prUrl) : void 0),
|
|
5071
|
+
repairTargetPrUrl: repairTarget?.targetPrUrl,
|
|
5072
|
+
repairTargetBranch: repairTarget?.targetPrBranch ?? void 0,
|
|
4752
5073
|
instructionPolicyMarkdown: harnessContext?.instructionPolicyMarkdown ?? null,
|
|
4753
5074
|
instructionPolicyFingerprint: harnessContext?.instructionPolicyFingerprint ?? null,
|
|
4754
5075
|
instructionPolicyEvidence: harnessContext?.instructionPolicyEvidence ?? null,
|
|
@@ -4883,106 +5204,384 @@ async function sweepRun(args) {
|
|
|
4883
5204
|
}
|
|
4884
5205
|
|
|
4885
5206
|
// src/worktree.ts
|
|
4886
|
-
import { existsSync as
|
|
4887
|
-
import
|
|
5207
|
+
import { existsSync as existsSync16, mkdirSync as mkdirSync5 } from "node:fs";
|
|
5208
|
+
import path25 from "node:path";
|
|
4888
5209
|
|
|
4889
|
-
// src/
|
|
4890
|
-
import
|
|
4891
|
-
|
|
4892
|
-
return path20.resolve(resolveUserPath(value.trim()));
|
|
4893
|
-
}
|
|
4894
|
-
function fromConfigured(value, source, persistedInConfig) {
|
|
4895
|
-
const trimmed = value?.trim();
|
|
4896
|
-
if (!trimmed) return null;
|
|
4897
|
-
return {
|
|
4898
|
-
repo: expandConfiguredRepo(trimmed),
|
|
4899
|
-
source,
|
|
4900
|
-
persistedInConfig
|
|
4901
|
-
};
|
|
4902
|
-
}
|
|
4903
|
-
function resolveDefaultRepo(opts = {}) {
|
|
4904
|
-
const env = opts.env ?? process.env;
|
|
4905
|
-
const config = opts.config ?? loadUserConfig();
|
|
4906
|
-
const fromConfig = fromConfigured(config.defaultRepo, "config", true);
|
|
4907
|
-
if (fromConfig) return fromConfig;
|
|
4908
|
-
const fromDefaultEnv = fromConfigured(env.KYNVER_DEFAULT_REPO, "env_default_repo", false);
|
|
4909
|
-
if (fromDefaultEnv) return fromDefaultEnv;
|
|
4910
|
-
const fromHarnessEnv = fromConfigured(env.KYNVER_HARNESS_REPO, "env_harness_repo", false);
|
|
4911
|
-
if (fromHarnessEnv) return fromHarnessEnv;
|
|
4912
|
-
const discovered = discoverDefaultRepo({
|
|
4913
|
-
cwd: opts.cwd,
|
|
4914
|
-
runtimeModuleUrl: opts.runtimeModuleUrl
|
|
4915
|
-
});
|
|
4916
|
-
if (!discovered) return null;
|
|
4917
|
-
return {
|
|
4918
|
-
repo: discovered.repo,
|
|
4919
|
-
source: discovered.source,
|
|
4920
|
-
persistedInConfig: false
|
|
4921
|
-
};
|
|
4922
|
-
}
|
|
4923
|
-
function formatResolvedDefaultRepo(resolved) {
|
|
4924
|
-
return {
|
|
4925
|
-
defaultRepo: displayUserPath(resolved.repo),
|
|
4926
|
-
source: resolved.source,
|
|
4927
|
-
persistedInConfig: resolved.persistedInConfig
|
|
4928
|
-
};
|
|
4929
|
-
}
|
|
5210
|
+
// src/run-list.ts
|
|
5211
|
+
import { existsSync as existsSync15, readFileSync as readFileSync9 } from "node:fs";
|
|
5212
|
+
import path22 from "node:path";
|
|
4930
5213
|
|
|
4931
|
-
// src/
|
|
5214
|
+
// src/stale-reconcile.ts
|
|
4932
5215
|
import path21 from "node:path";
|
|
4933
|
-
|
|
4934
|
-
|
|
4935
|
-
|
|
4936
|
-
|
|
4937
|
-
|
|
5216
|
+
|
|
5217
|
+
// src/finalize.ts
|
|
5218
|
+
import path20 from "node:path";
|
|
5219
|
+
var ACTIVE_RUN_STATUSES = /* @__PURE__ */ new Set([
|
|
5220
|
+
"running",
|
|
5221
|
+
"dispatching",
|
|
5222
|
+
"pending",
|
|
5223
|
+
"queued",
|
|
5224
|
+
"needs_attention"
|
|
5225
|
+
]);
|
|
5226
|
+
var TERMINAL_RUN_STATUSES = /* @__PURE__ */ new Set(["completed", "failed", "cancelled", "done"]);
|
|
5227
|
+
function deriveTerminalRunStatus(run) {
|
|
5228
|
+
const names = Object.keys(run.workers || {});
|
|
5229
|
+
if (names.length === 0) return "failed";
|
|
5230
|
+
let anyAlive = false;
|
|
5231
|
+
let anyResult = false;
|
|
5232
|
+
let anyCompletionBlocked = false;
|
|
5233
|
+
let anyLandingBlocked = false;
|
|
5234
|
+
for (const name of names) {
|
|
5235
|
+
const worker = readJson(
|
|
5236
|
+
path20.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
|
|
5237
|
+
void 0
|
|
5238
|
+
);
|
|
5239
|
+
if (!worker) continue;
|
|
5240
|
+
const status = computeWorkerStatus(worker, {
|
|
5241
|
+
base: run.base,
|
|
5242
|
+
baseCommit: run.baseCommit
|
|
5243
|
+
});
|
|
5244
|
+
if (status.alive && !status.finalResult) {
|
|
5245
|
+
anyAlive = true;
|
|
5246
|
+
break;
|
|
5247
|
+
}
|
|
5248
|
+
if (typeof worker.completionBlocker === "string" && worker.completionBlocker) {
|
|
5249
|
+
anyCompletionBlocked = true;
|
|
5250
|
+
}
|
|
5251
|
+
if (isLandingBlockedWorkerStatus(status)) {
|
|
5252
|
+
anyLandingBlocked = true;
|
|
5253
|
+
}
|
|
5254
|
+
if (status.finalResult && status.attention.state === "done") anyResult = true;
|
|
5255
|
+
}
|
|
5256
|
+
if (anyAlive) return null;
|
|
5257
|
+
if (anyCompletionBlocked) return null;
|
|
5258
|
+
if (anyLandingBlocked) return null;
|
|
5259
|
+
return anyResult ? "completed" : "failed";
|
|
4938
5260
|
}
|
|
4939
|
-
function
|
|
4940
|
-
const
|
|
4941
|
-
|
|
4942
|
-
|
|
5261
|
+
function finalizeStaleRuns() {
|
|
5262
|
+
const finalized = [];
|
|
5263
|
+
for (const run of listRunRecords()) {
|
|
5264
|
+
if (!ACTIVE_RUN_STATUSES.has(run.status)) continue;
|
|
5265
|
+
const next = deriveTerminalRunStatus(run);
|
|
5266
|
+
if (!next || next === run.status) continue;
|
|
5267
|
+
const from = run.status;
|
|
5268
|
+
run.status = next;
|
|
5269
|
+
saveRun(run);
|
|
5270
|
+
finalized.push({ runId: run.id, from, to: next });
|
|
5271
|
+
}
|
|
5272
|
+
return finalized;
|
|
4943
5273
|
}
|
|
4944
5274
|
|
|
4945
|
-
// src/
|
|
4946
|
-
|
|
4947
|
-
|
|
4948
|
-
|
|
4949
|
-
const resolved = resolveDefaultRepo();
|
|
4950
|
-
if (resolved) return resolved.repo;
|
|
4951
|
-
required("", "--repo (or set defaultRepo via `kynver setup` / KYNVER_DEFAULT_REPO)");
|
|
4952
|
-
return "";
|
|
5275
|
+
// src/stale-reconcile.ts
|
|
5276
|
+
var STALE_RECONCILE_HEARTBEAT_MS = 15 * 60 * 1e3;
|
|
5277
|
+
function staleReconcileDisabled() {
|
|
5278
|
+
return process.env.KYNVER_NO_STALE_CLEANUP === "1";
|
|
4953
5279
|
}
|
|
4954
|
-
function
|
|
4955
|
-
|
|
4956
|
-
|
|
4957
|
-
|
|
4958
|
-
const
|
|
4959
|
-
|
|
4960
|
-
|
|
4961
|
-
|
|
4962
|
-
|
|
4963
|
-
|
|
4964
|
-
|
|
4965
|
-
|
|
4966
|
-
|
|
4967
|
-
|
|
4968
|
-
|
|
5280
|
+
function reconcileStaleWorkers() {
|
|
5281
|
+
if (staleReconcileDisabled()) {
|
|
5282
|
+
return { workers: [], finalizedRuns: finalizeStaleRuns() };
|
|
5283
|
+
}
|
|
5284
|
+
const outcomes = [];
|
|
5285
|
+
const now = Date.now();
|
|
5286
|
+
for (const run of listRunRecords()) {
|
|
5287
|
+
for (const name of Object.keys(run.workers || {})) {
|
|
5288
|
+
const workerPath = path21.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json");
|
|
5289
|
+
const worker = readJson(workerPath, void 0);
|
|
5290
|
+
if (!worker || worker.status !== "running") {
|
|
5291
|
+
outcomes.push({
|
|
5292
|
+
runId: run.id,
|
|
5293
|
+
worker: name,
|
|
5294
|
+
action: "skipped",
|
|
5295
|
+
reason: worker ? `worker status is ${worker.status}` : "worker.json missing"
|
|
5296
|
+
});
|
|
5297
|
+
continue;
|
|
5298
|
+
}
|
|
5299
|
+
const status = computeWorkerStatus(worker, { base: run.base, baseCommit: run.baseCommit });
|
|
5300
|
+
if (status.finalResult) {
|
|
5301
|
+
if (worker.status === "running") {
|
|
5302
|
+
const nextStatus = status.attention.state === "blocked" ? "blocked" : status.attention.state === "done" || status.status === "done" ? "done" : "exited";
|
|
5303
|
+
worker.status = nextStatus;
|
|
5304
|
+
worker.reconciledAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
5305
|
+
worker.reconcileReason = "synced finished worker record after terminal stdout/heartbeat";
|
|
5306
|
+
saveWorker(run.id, worker);
|
|
5307
|
+
outcomes.push({
|
|
5308
|
+
runId: run.id,
|
|
5309
|
+
worker: name,
|
|
5310
|
+
action: "marked_exited",
|
|
5311
|
+
reason: worker.reconcileReason
|
|
5312
|
+
});
|
|
5313
|
+
} else {
|
|
5314
|
+
outcomes.push({ runId: run.id, worker: name, action: "skipped", reason: "final result present" });
|
|
5315
|
+
}
|
|
5316
|
+
continue;
|
|
5317
|
+
}
|
|
5318
|
+
if (!status.alive) {
|
|
5319
|
+
const nextStatus = status.attention.state === "blocked" ? "blocked" : status.status === "done" ? "done" : "exited";
|
|
5320
|
+
worker.status = nextStatus;
|
|
5321
|
+
worker.reconciledAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
5322
|
+
worker.reconcileReason = status.attention.reason;
|
|
5323
|
+
saveWorker(run.id, worker);
|
|
5324
|
+
outcomes.push({
|
|
5325
|
+
runId: run.id,
|
|
5326
|
+
worker: name,
|
|
5327
|
+
action: "marked_exited",
|
|
5328
|
+
reason: status.attention.reason
|
|
5329
|
+
});
|
|
5330
|
+
continue;
|
|
5331
|
+
}
|
|
5332
|
+
if (status.attention.state === "stale" && worker.pid && isPidAlive(worker.pid)) {
|
|
5333
|
+
const hbMs = status.lastHeartbeatAt ? Date.parse(status.lastHeartbeatAt) : NaN;
|
|
5334
|
+
const actMs = status.lastActivityAt ? Date.parse(status.lastActivityAt) : NaN;
|
|
5335
|
+
const hbStale = !Number.isFinite(hbMs) || now - hbMs > STALE_RECONCILE_HEARTBEAT_MS;
|
|
5336
|
+
const actStale = Number.isFinite(actMs) && now - actMs > STALE_MS;
|
|
5337
|
+
if (hbStale && actStale) {
|
|
5338
|
+
killWorkerProcess(worker.pid, "SIGTERM");
|
|
5339
|
+
worker.status = "exited";
|
|
5340
|
+
worker.reconciledAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
5341
|
+
worker.reconcileReason = `reconciled stale worker: ${status.attention.reason}`;
|
|
5342
|
+
saveWorker(run.id, worker);
|
|
5343
|
+
outcomes.push({
|
|
5344
|
+
runId: run.id,
|
|
5345
|
+
worker: name,
|
|
5346
|
+
action: "killed_stale",
|
|
5347
|
+
reason: status.attention.reason
|
|
5348
|
+
});
|
|
5349
|
+
continue;
|
|
5350
|
+
}
|
|
5351
|
+
}
|
|
5352
|
+
outcomes.push({
|
|
5353
|
+
runId: run.id,
|
|
5354
|
+
worker: name,
|
|
5355
|
+
action: "skipped",
|
|
5356
|
+
reason: status.attention.reason
|
|
5357
|
+
});
|
|
5358
|
+
}
|
|
5359
|
+
}
|
|
5360
|
+
return { workers: outcomes, finalizedRuns: finalizeStaleRuns() };
|
|
5361
|
+
}
|
|
5362
|
+
function reconcileRunsCli() {
|
|
5363
|
+
const result = reconcileStaleWorkers();
|
|
5364
|
+
const markedExited = result.workers.filter((w) => w.action === "marked_exited").length;
|
|
5365
|
+
const killedStale = result.workers.filter((w) => w.action === "killed_stale").length;
|
|
5366
|
+
const skipped = result.workers.filter((w) => w.action === "skipped").length;
|
|
5367
|
+
console.log(
|
|
5368
|
+
JSON.stringify(
|
|
5369
|
+
{
|
|
5370
|
+
ok: true,
|
|
5371
|
+
workers: { markedExited, killedStale, skipped, total: result.workers.length },
|
|
5372
|
+
finalizedRuns: result.finalizedRuns.length,
|
|
5373
|
+
details: { workers: result.workers, finalizedRuns: result.finalizedRuns }
|
|
5374
|
+
},
|
|
5375
|
+
null,
|
|
5376
|
+
2
|
|
5377
|
+
)
|
|
5378
|
+
);
|
|
5379
|
+
}
|
|
5380
|
+
|
|
5381
|
+
// src/run-list.ts
|
|
5382
|
+
function heartbeatByteLength(heartbeatPath) {
|
|
5383
|
+
if (!heartbeatPath || !existsSync15(heartbeatPath)) return 0;
|
|
5384
|
+
try {
|
|
5385
|
+
return readFileSync9(heartbeatPath, "utf8").trim().length;
|
|
5386
|
+
} catch {
|
|
5387
|
+
return 0;
|
|
5388
|
+
}
|
|
5389
|
+
}
|
|
5390
|
+
function workerEvidence(run, workerName) {
|
|
5391
|
+
const workerPath = path22.join(runDirectory(run.id), "workers", safeSlug(workerName), "worker.json");
|
|
5392
|
+
const worker = readJson(workerPath, void 0);
|
|
5393
|
+
if (!worker) {
|
|
5394
|
+
return {
|
|
5395
|
+
worker: workerName,
|
|
5396
|
+
workerStatus: "missing",
|
|
5397
|
+
attention: "needs_attention",
|
|
5398
|
+
attentionReason: "worker.json missing",
|
|
5399
|
+
missingHeartbeat: true,
|
|
5400
|
+
missingFinalResult: true,
|
|
5401
|
+
landingBlocked: false,
|
|
5402
|
+
completionBlocked: false
|
|
5403
|
+
};
|
|
5404
|
+
}
|
|
5405
|
+
const status = computeWorkerStatus(worker, { base: run.base, baseCommit: run.baseCommit });
|
|
5406
|
+
const missingHeartbeat = heartbeatByteLength(worker.heartbeatPath) === 0;
|
|
5407
|
+
const missingFinalResult = !status.finalResult && !status.alive;
|
|
5408
|
+
const completionBlocked = typeof worker.completionBlocker === "string" && worker.completionBlocker.length > 0;
|
|
5409
|
+
return {
|
|
5410
|
+
worker: workerName,
|
|
5411
|
+
workerStatus: worker.status,
|
|
5412
|
+
attention: status.attention.state,
|
|
5413
|
+
attentionReason: status.attention.reason,
|
|
5414
|
+
missingHeartbeat,
|
|
5415
|
+
missingFinalResult,
|
|
5416
|
+
landingBlocked: isLandingBlockedWorkerStatus(status),
|
|
5417
|
+
completionBlocked
|
|
5418
|
+
};
|
|
5419
|
+
}
|
|
5420
|
+
function deriveFinalizeBlockedReason(input) {
|
|
5421
|
+
if (input.openWorkerCount > 0) return "active_workers";
|
|
5422
|
+
if (input.workers.some((w) => w.completionBlocked)) return "completion_blocked";
|
|
5423
|
+
if (input.workers.some((w) => w.landingBlocked)) return "landing_blocked";
|
|
5424
|
+
return null;
|
|
5425
|
+
}
|
|
5426
|
+
function aggregateRunAttention(workers) {
|
|
5427
|
+
const rank = {
|
|
5428
|
+
blocked: 5,
|
|
5429
|
+
needs_attention: 4,
|
|
5430
|
+
stale: 3,
|
|
5431
|
+
done: 2,
|
|
5432
|
+
ok: 1
|
|
5433
|
+
};
|
|
5434
|
+
let best = "ok";
|
|
5435
|
+
let reason;
|
|
5436
|
+
for (const w of workers) {
|
|
5437
|
+
const state = w.attention;
|
|
5438
|
+
if ((rank[state] ?? 0) >= (rank[best] ?? 0)) {
|
|
5439
|
+
best = state;
|
|
5440
|
+
reason = w.attentionReason;
|
|
5441
|
+
}
|
|
5442
|
+
}
|
|
5443
|
+
return { attention: best, attentionReason: reason };
|
|
5444
|
+
}
|
|
5445
|
+
function countOpenWorkers(run) {
|
|
5446
|
+
let open = 0;
|
|
5447
|
+
for (const name of Object.keys(run.workers || {})) {
|
|
5448
|
+
const workerPath = path22.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json");
|
|
5449
|
+
const worker = readJson(workerPath, void 0);
|
|
5450
|
+
if (!worker) continue;
|
|
5451
|
+
const status = computeWorkerStatus(worker, { base: run.base, baseCommit: run.baseCommit });
|
|
5452
|
+
if (status.alive && !status.finalResult) open += 1;
|
|
5453
|
+
}
|
|
5454
|
+
return open;
|
|
5455
|
+
}
|
|
5456
|
+
function buildRunListRows() {
|
|
5457
|
+
reconcileStaleWorkers();
|
|
5458
|
+
return listRunRecords().map((run) => {
|
|
5459
|
+
const workerNames = Object.keys(run.workers || {});
|
|
5460
|
+
const workers = workerNames.map((name) => workerEvidence(run, name));
|
|
5461
|
+
const missingHeartbeatWorkers = workers.filter((w) => w.missingHeartbeat).map((w) => w.worker);
|
|
5462
|
+
const missingFinalResultWorkers = workers.filter((w) => w.missingFinalResult).map((w) => w.worker);
|
|
5463
|
+
const landingBlockedWorkers = workers.filter((w) => w.landingBlocked).map((w) => w.worker);
|
|
5464
|
+
const completionBlockedWorkers = workers.filter((w) => w.completionBlocked).map((w) => w.worker);
|
|
5465
|
+
const { attention, attentionReason } = aggregateRunAttention(workers);
|
|
5466
|
+
const openWorkerCount = countOpenWorkers(run);
|
|
5467
|
+
const effectiveStatus = deriveRunStatus(
|
|
5468
|
+
run.status,
|
|
5469
|
+
workers.map((w) => ({ attention: w.attention, status: w.workerStatus }))
|
|
5470
|
+
);
|
|
5471
|
+
return {
|
|
5472
|
+
id: run.id,
|
|
5473
|
+
name: run.name,
|
|
5474
|
+
status: run.status,
|
|
5475
|
+
effectiveStatus,
|
|
5476
|
+
repo: run.repo,
|
|
5477
|
+
createdAt: run.createdAt,
|
|
5478
|
+
openWorkerCount,
|
|
5479
|
+
attention,
|
|
5480
|
+
attentionReason,
|
|
5481
|
+
finalizeBlockedReason: deriveFinalizeBlockedReason({ run, workers, openWorkerCount }),
|
|
5482
|
+
evidence: {
|
|
5483
|
+
missingHeartbeatWorkers,
|
|
5484
|
+
missingFinalResultWorkers,
|
|
5485
|
+
landingBlockedWorkers,
|
|
5486
|
+
completionBlockedWorkers,
|
|
5487
|
+
workers
|
|
5488
|
+
}
|
|
5489
|
+
};
|
|
5490
|
+
});
|
|
5491
|
+
}
|
|
5492
|
+
function listRunsCli() {
|
|
5493
|
+
console.log(JSON.stringify(buildRunListRows(), null, 2));
|
|
5494
|
+
}
|
|
5495
|
+
|
|
5496
|
+
// src/default-repo.ts
|
|
5497
|
+
import path23 from "node:path";
|
|
5498
|
+
function expandConfiguredRepo(value) {
|
|
5499
|
+
return path23.resolve(resolveUserPath(value.trim()));
|
|
5500
|
+
}
|
|
5501
|
+
function fromConfigured(value, source, persistedInConfig) {
|
|
5502
|
+
const trimmed = value?.trim();
|
|
5503
|
+
if (!trimmed) return null;
|
|
5504
|
+
return {
|
|
5505
|
+
repo: expandConfiguredRepo(trimmed),
|
|
5506
|
+
source,
|
|
5507
|
+
persistedInConfig
|
|
5508
|
+
};
|
|
5509
|
+
}
|
|
5510
|
+
function resolveDefaultRepo(opts = {}) {
|
|
5511
|
+
const env = opts.env ?? process.env;
|
|
5512
|
+
const config = opts.config ?? loadUserConfig();
|
|
5513
|
+
const fromConfig = fromConfigured(config.defaultRepo, "config", true);
|
|
5514
|
+
if (fromConfig) return fromConfig;
|
|
5515
|
+
const fromDefaultEnv = fromConfigured(env.KYNVER_DEFAULT_REPO, "env_default_repo", false);
|
|
5516
|
+
if (fromDefaultEnv) return fromDefaultEnv;
|
|
5517
|
+
const fromHarnessEnv = fromConfigured(env.KYNVER_HARNESS_REPO, "env_harness_repo", false);
|
|
5518
|
+
if (fromHarnessEnv) return fromHarnessEnv;
|
|
5519
|
+
const discovered = discoverDefaultRepo({
|
|
5520
|
+
cwd: opts.cwd,
|
|
5521
|
+
runtimeModuleUrl: opts.runtimeModuleUrl
|
|
5522
|
+
});
|
|
5523
|
+
if (!discovered) return null;
|
|
5524
|
+
return {
|
|
5525
|
+
repo: discovered.repo,
|
|
5526
|
+
source: discovered.source,
|
|
5527
|
+
persistedInConfig: false
|
|
5528
|
+
};
|
|
5529
|
+
}
|
|
5530
|
+
function formatResolvedDefaultRepo(resolved) {
|
|
5531
|
+
return {
|
|
5532
|
+
defaultRepo: displayUserPath(resolved.repo),
|
|
5533
|
+
source: resolved.source,
|
|
5534
|
+
persistedInConfig: resolved.persistedInConfig
|
|
5535
|
+
};
|
|
5536
|
+
}
|
|
5537
|
+
|
|
5538
|
+
// src/validate.ts
|
|
5539
|
+
import path24 from "node:path";
|
|
5540
|
+
var RUN_ID_RE = /^[a-z0-9][a-z0-9._-]{0,127}$/i;
|
|
5541
|
+
function validateRunId(runId) {
|
|
5542
|
+
const trimmed = runId.trim();
|
|
5543
|
+
if (!RUN_ID_RE.test(trimmed)) throw new Error(`invalid run id: ${runId}`);
|
|
5544
|
+
return trimmed;
|
|
5545
|
+
}
|
|
5546
|
+
function validateRepo(repo) {
|
|
5547
|
+
const resolved = path24.resolve(repo);
|
|
5548
|
+
if (resolved.includes("..")) throw new Error("repo path must not contain .. segments");
|
|
5549
|
+
return resolved;
|
|
5550
|
+
}
|
|
5551
|
+
|
|
5552
|
+
// src/worktree.ts
|
|
5553
|
+
function resolveCreateRunRepo(args) {
|
|
5554
|
+
const explicit = typeof args.repo === "string" ? args.repo.trim() : "";
|
|
5555
|
+
if (explicit) return explicit;
|
|
5556
|
+
const resolved = resolveDefaultRepo();
|
|
5557
|
+
if (resolved) return resolved.repo;
|
|
5558
|
+
required("", "--repo (or set defaultRepo via `kynver setup` / KYNVER_DEFAULT_REPO)");
|
|
5559
|
+
return "";
|
|
5560
|
+
}
|
|
5561
|
+
function createRun(args) {
|
|
5562
|
+
const repo = validateRepo(resolveCreateRunRepo(args));
|
|
5563
|
+
ensureGitRepo(repo);
|
|
5564
|
+
const id = args.id ? validateRunId(String(args.id)) : timestampSlug(String(args.name || "run"));
|
|
5565
|
+
const dir = runDirectory(id);
|
|
5566
|
+
if (existsSync16(dir)) failExists(`run already exists: ${id}`);
|
|
5567
|
+
mkdirSync5(dir, { recursive: true });
|
|
5568
|
+
const base = String(args.base || "origin/main");
|
|
5569
|
+
const baseCommit = git(repo, ["rev-parse", base]).trim();
|
|
5570
|
+
const run = {
|
|
5571
|
+
id,
|
|
5572
|
+
name: String(args.name || id),
|
|
5573
|
+
repo,
|
|
5574
|
+
base,
|
|
5575
|
+
baseCommit,
|
|
4969
5576
|
status: "created",
|
|
4970
5577
|
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4971
5578
|
workers: {}
|
|
4972
5579
|
};
|
|
4973
|
-
writeJson(
|
|
5580
|
+
writeJson(path25.join(dir, "run.json"), run);
|
|
4974
5581
|
console.log(JSON.stringify({ runId: id, runDir: dir, repo, base, baseCommit }, null, 2));
|
|
4975
5582
|
}
|
|
4976
5583
|
function listRuns() {
|
|
4977
|
-
|
|
4978
|
-
const rows = listRunIds(runsDir).map((id) => readJson(path22.join(runDirectory(id), "run.json"), void 0)).filter(Boolean).map((run) => ({
|
|
4979
|
-
id: run.id,
|
|
4980
|
-
name: run.name,
|
|
4981
|
-
status: run.status,
|
|
4982
|
-
repo: run.repo,
|
|
4983
|
-
createdAt: run.createdAt
|
|
4984
|
-
}));
|
|
4985
|
-
console.log(JSON.stringify(rows, null, 2));
|
|
5584
|
+
listRunsCli();
|
|
4986
5585
|
}
|
|
4987
5586
|
function failExists(message) {
|
|
4988
5587
|
console.error(message);
|
|
@@ -4990,8 +5589,8 @@ function failExists(message) {
|
|
|
4990
5589
|
}
|
|
4991
5590
|
|
|
4992
5591
|
// src/discard-disposable.ts
|
|
4993
|
-
import { existsSync as
|
|
4994
|
-
import
|
|
5592
|
+
import { existsSync as existsSync17, rmSync } from "node:fs";
|
|
5593
|
+
import path26 from "node:path";
|
|
4995
5594
|
function normalizeRelativePath2(value) {
|
|
4996
5595
|
const normalized = value.replace(/\\/g, "/").replace(/^\.\//, "").trim();
|
|
4997
5596
|
if (!normalized || normalized.startsWith("/") || normalized.includes("..")) {
|
|
@@ -5012,15 +5611,15 @@ function discardDisposableArtifacts(args) {
|
|
|
5012
5611
|
if (paths.length === 0) {
|
|
5013
5612
|
return { ok: false, removed: [], reason: "requires at least one --path" };
|
|
5014
5613
|
}
|
|
5015
|
-
const worktreeRoot =
|
|
5614
|
+
const worktreeRoot = path26.resolve(worker.worktreePath);
|
|
5016
5615
|
const removed = [];
|
|
5017
5616
|
for (const raw of paths) {
|
|
5018
5617
|
const rel = normalizeRelativePath2(raw);
|
|
5019
|
-
const abs =
|
|
5020
|
-
if (!abs.startsWith(worktreeRoot +
|
|
5618
|
+
const abs = path26.resolve(worktreeRoot, rel);
|
|
5619
|
+
if (!abs.startsWith(worktreeRoot + path26.sep) && abs !== worktreeRoot) {
|
|
5021
5620
|
return { ok: false, removed, reason: `path escapes worktree: ${raw}` };
|
|
5022
5621
|
}
|
|
5023
|
-
if (!
|
|
5622
|
+
if (!existsSync17(abs)) {
|
|
5024
5623
|
return { ok: false, removed, reason: `path not found: ${raw}` };
|
|
5025
5624
|
}
|
|
5026
5625
|
rmSync(abs, { recursive: true, force: true });
|
|
@@ -5043,7 +5642,7 @@ function discardDisposableCli(args) {
|
|
|
5043
5642
|
}
|
|
5044
5643
|
|
|
5045
5644
|
// src/pipeline-tick.ts
|
|
5046
|
-
import
|
|
5645
|
+
import path41 from "node:path";
|
|
5047
5646
|
|
|
5048
5647
|
// src/pipeline-dispatch.ts
|
|
5049
5648
|
var RESERVED_REVIEW_STARTS = 1;
|
|
@@ -5123,175 +5722,47 @@ function resolvePipelineMaxStarts(resourceGate, operatorTick) {
|
|
|
5123
5722
|
};
|
|
5124
5723
|
}
|
|
5125
5724
|
|
|
5126
|
-
// src/
|
|
5127
|
-
import
|
|
5725
|
+
// src/box-resource-snapshot.ts
|
|
5726
|
+
import os5 from "node:os";
|
|
5128
5727
|
|
|
5129
|
-
// src/
|
|
5130
|
-
import
|
|
5131
|
-
|
|
5132
|
-
"
|
|
5133
|
-
"
|
|
5134
|
-
"
|
|
5135
|
-
"queued",
|
|
5136
|
-
"needs_attention"
|
|
5137
|
-
]);
|
|
5138
|
-
var TERMINAL_RUN_STATUSES = /* @__PURE__ */ new Set(["completed", "failed", "cancelled", "done"]);
|
|
5139
|
-
function deriveTerminalRunStatus(run) {
|
|
5140
|
-
const names = Object.keys(run.workers || {});
|
|
5141
|
-
if (names.length === 0) return "failed";
|
|
5142
|
-
let anyAlive = false;
|
|
5143
|
-
let anyResult = false;
|
|
5144
|
-
let anyCompletionBlocked = false;
|
|
5145
|
-
let anyLandingBlocked = false;
|
|
5146
|
-
for (const name of names) {
|
|
5147
|
-
const worker = readJson(
|
|
5148
|
-
path24.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
|
|
5149
|
-
void 0
|
|
5150
|
-
);
|
|
5151
|
-
if (!worker) continue;
|
|
5152
|
-
const status = computeWorkerStatus(worker, {
|
|
5153
|
-
base: run.base,
|
|
5154
|
-
baseCommit: run.baseCommit
|
|
5155
|
-
});
|
|
5156
|
-
if (status.alive && !status.finalResult) {
|
|
5157
|
-
anyAlive = true;
|
|
5158
|
-
break;
|
|
5159
|
-
}
|
|
5160
|
-
if (typeof worker.completionBlocker === "string" && worker.completionBlocker) {
|
|
5161
|
-
anyCompletionBlocked = true;
|
|
5162
|
-
}
|
|
5163
|
-
if (isLandingBlockedWorkerStatus(status)) {
|
|
5164
|
-
anyLandingBlocked = true;
|
|
5165
|
-
}
|
|
5166
|
-
if (status.finalResult && status.attention.state === "done") anyResult = true;
|
|
5167
|
-
}
|
|
5168
|
-
if (anyAlive) return null;
|
|
5169
|
-
if (anyCompletionBlocked) return null;
|
|
5170
|
-
if (anyLandingBlocked) return null;
|
|
5171
|
-
return anyResult ? "completed" : "failed";
|
|
5728
|
+
// src/box-resource-snapshot-shared.ts
|
|
5729
|
+
import os4 from "node:os";
|
|
5730
|
+
function resolveBoxKindFromEnv(env = process.env) {
|
|
5731
|
+
const kind = (env.KYNVER_BOX_KIND ?? env.KYNVER_AGENT_OS_SLUG ?? "forge").trim().toLowerCase();
|
|
5732
|
+
if (kind === "ghost" || kind === "forge") return kind;
|
|
5733
|
+
return kind || "forge";
|
|
5172
5734
|
}
|
|
5173
|
-
function
|
|
5174
|
-
const
|
|
5175
|
-
|
|
5176
|
-
if (!ACTIVE_RUN_STATUSES.has(run.status)) continue;
|
|
5177
|
-
const next = deriveTerminalRunStatus(run);
|
|
5178
|
-
if (!next || next === run.status) continue;
|
|
5179
|
-
const from = run.status;
|
|
5180
|
-
run.status = next;
|
|
5181
|
-
saveRun(run);
|
|
5182
|
-
finalized.push({ runId: run.id, from, to: next });
|
|
5183
|
-
}
|
|
5184
|
-
return finalized;
|
|
5735
|
+
function defaultBoxId(boxKind, hostLabel) {
|
|
5736
|
+
const host = (hostLabel ?? os4.hostname()).trim().toLowerCase().replace(/\s+/g, "-") || "unknown-host";
|
|
5737
|
+
return `${boxKind}:${host}`;
|
|
5185
5738
|
}
|
|
5186
5739
|
|
|
5187
|
-
// src/
|
|
5188
|
-
|
|
5189
|
-
|
|
5190
|
-
|
|
5191
|
-
|
|
5192
|
-
|
|
5193
|
-
|
|
5194
|
-
|
|
5195
|
-
|
|
5196
|
-
|
|
5197
|
-
|
|
5198
|
-
|
|
5199
|
-
|
|
5200
|
-
|
|
5201
|
-
|
|
5202
|
-
|
|
5203
|
-
|
|
5204
|
-
|
|
5205
|
-
|
|
5206
|
-
|
|
5207
|
-
|
|
5208
|
-
|
|
5209
|
-
continue;
|
|
5210
|
-
}
|
|
5211
|
-
const status = computeWorkerStatus(worker, { base: run.base, baseCommit: run.baseCommit });
|
|
5212
|
-
if (status.finalResult) {
|
|
5213
|
-
if (worker.status === "running") {
|
|
5214
|
-
const nextStatus = status.attention.state === "blocked" ? "blocked" : status.attention.state === "done" || status.status === "done" ? "done" : "exited";
|
|
5215
|
-
worker.status = nextStatus;
|
|
5216
|
-
worker.reconciledAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
5217
|
-
worker.reconcileReason = "synced finished worker record after terminal stdout/heartbeat";
|
|
5218
|
-
saveWorker(run.id, worker);
|
|
5219
|
-
outcomes.push({
|
|
5220
|
-
runId: run.id,
|
|
5221
|
-
worker: name,
|
|
5222
|
-
action: "marked_exited",
|
|
5223
|
-
reason: worker.reconcileReason
|
|
5224
|
-
});
|
|
5225
|
-
} else {
|
|
5226
|
-
outcomes.push({ runId: run.id, worker: name, action: "skipped", reason: "final result present" });
|
|
5227
|
-
}
|
|
5228
|
-
continue;
|
|
5229
|
-
}
|
|
5230
|
-
if (!status.alive) {
|
|
5231
|
-
const nextStatus = status.attention.state === "blocked" ? "blocked" : status.status === "done" ? "done" : "exited";
|
|
5232
|
-
worker.status = nextStatus;
|
|
5233
|
-
worker.reconciledAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
5234
|
-
worker.reconcileReason = status.attention.reason;
|
|
5235
|
-
saveWorker(run.id, worker);
|
|
5236
|
-
outcomes.push({
|
|
5237
|
-
runId: run.id,
|
|
5238
|
-
worker: name,
|
|
5239
|
-
action: "marked_exited",
|
|
5240
|
-
reason: status.attention.reason
|
|
5241
|
-
});
|
|
5242
|
-
continue;
|
|
5243
|
-
}
|
|
5244
|
-
if (status.attention.state === "stale" && worker.pid && isPidAlive(worker.pid)) {
|
|
5245
|
-
const hbMs = status.lastHeartbeatAt ? Date.parse(status.lastHeartbeatAt) : NaN;
|
|
5246
|
-
const actMs = status.lastActivityAt ? Date.parse(status.lastActivityAt) : NaN;
|
|
5247
|
-
const hbStale = !Number.isFinite(hbMs) || now - hbMs > STALE_RECONCILE_HEARTBEAT_MS;
|
|
5248
|
-
const actStale = Number.isFinite(actMs) && now - actMs > STALE_MS;
|
|
5249
|
-
if (hbStale && actStale) {
|
|
5250
|
-
killWorkerProcess(worker.pid, "SIGTERM");
|
|
5251
|
-
worker.status = "exited";
|
|
5252
|
-
worker.reconciledAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
5253
|
-
worker.reconcileReason = `reconciled stale worker: ${status.attention.reason}`;
|
|
5254
|
-
saveWorker(run.id, worker);
|
|
5255
|
-
outcomes.push({
|
|
5256
|
-
runId: run.id,
|
|
5257
|
-
worker: name,
|
|
5258
|
-
action: "killed_stale",
|
|
5259
|
-
reason: status.attention.reason
|
|
5260
|
-
});
|
|
5261
|
-
continue;
|
|
5262
|
-
}
|
|
5263
|
-
}
|
|
5264
|
-
outcomes.push({
|
|
5265
|
-
runId: run.id,
|
|
5266
|
-
worker: name,
|
|
5267
|
-
action: "skipped",
|
|
5268
|
-
reason: status.attention.reason
|
|
5269
|
-
});
|
|
5270
|
-
}
|
|
5271
|
-
}
|
|
5272
|
-
return { workers: outcomes, finalizedRuns: finalizeStaleRuns() };
|
|
5273
|
-
}
|
|
5274
|
-
function reconcileRunsCli() {
|
|
5275
|
-
const result = reconcileStaleWorkers();
|
|
5276
|
-
const markedExited = result.workers.filter((w) => w.action === "marked_exited").length;
|
|
5277
|
-
const killedStale = result.workers.filter((w) => w.action === "killed_stale").length;
|
|
5278
|
-
const skipped = result.workers.filter((w) => w.action === "skipped").length;
|
|
5279
|
-
console.log(
|
|
5280
|
-
JSON.stringify(
|
|
5281
|
-
{
|
|
5282
|
-
ok: true,
|
|
5283
|
-
workers: { markedExited, killedStale, skipped, total: result.workers.length },
|
|
5284
|
-
finalizedRuns: result.finalizedRuns.length,
|
|
5285
|
-
details: { workers: result.workers, finalizedRuns: result.finalizedRuns }
|
|
5286
|
-
},
|
|
5287
|
-
null,
|
|
5288
|
-
2
|
|
5289
|
-
)
|
|
5290
|
-
);
|
|
5740
|
+
// src/box-resource-snapshot.ts
|
|
5741
|
+
function buildBoxResourceSnapshotFromGate(gate, input = {}) {
|
|
5742
|
+
const boxKind = (input.boxKind ?? resolveBoxKindFromEnv()).trim().toLowerCase() || "forge";
|
|
5743
|
+
const hostLabel = input.hostLabel ?? os5.hostname();
|
|
5744
|
+
const boxId = input.boxId ?? defaultBoxId(boxKind, hostLabel);
|
|
5745
|
+
return {
|
|
5746
|
+
boxId,
|
|
5747
|
+
boxKind,
|
|
5748
|
+
displayName: input.displayName ?? null,
|
|
5749
|
+
hostLabel,
|
|
5750
|
+
observedAt: input.observedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
5751
|
+
totalMemBytes: gate.totalMemBytes,
|
|
5752
|
+
freeMemBytes: gate.freeMemBytes,
|
|
5753
|
+
activeWorkers: gate.activeWorkers,
|
|
5754
|
+
maxConcurrentWorkers: gate.maxConcurrentWorkers,
|
|
5755
|
+
autoCap: gate.autoCap,
|
|
5756
|
+
slotsAvailable: gate.slotsAvailable,
|
|
5757
|
+
harnessRunId: input.harnessRunId ?? null,
|
|
5758
|
+
queuedTasks: input.queuedTasks ?? null,
|
|
5759
|
+
reason: gate.reason,
|
|
5760
|
+
...input.prEvidence?.length ? { prEvidence: input.prEvidence } : {}
|
|
5761
|
+
};
|
|
5291
5762
|
}
|
|
5292
5763
|
|
|
5293
5764
|
// src/plan-progress-daemon-sync.ts
|
|
5294
|
-
import
|
|
5765
|
+
import path27 from "node:path";
|
|
5295
5766
|
|
|
5296
5767
|
// src/plan-progress-sync.ts
|
|
5297
5768
|
async function syncPlanProgress(args) {
|
|
@@ -5315,7 +5786,7 @@ async function syncActiveWorkerPlanProgress(runId, args) {
|
|
|
5315
5786
|
const outcomes = [];
|
|
5316
5787
|
for (const name of Object.keys(run.workers || {})) {
|
|
5317
5788
|
const worker = readJson(
|
|
5318
|
-
|
|
5789
|
+
path27.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
|
|
5319
5790
|
void 0
|
|
5320
5791
|
);
|
|
5321
5792
|
if (!worker?.dispatched || !worker.taskId) continue;
|
|
@@ -5364,7 +5835,10 @@ async function fetchWorkspaceRuntimePreferences(agentOsId, args) {
|
|
|
5364
5835
|
}
|
|
5365
5836
|
|
|
5366
5837
|
// src/cleanup.ts
|
|
5367
|
-
import
|
|
5838
|
+
import path39 from "node:path";
|
|
5839
|
+
|
|
5840
|
+
// src/cleanup-guards.ts
|
|
5841
|
+
import path28 from "node:path";
|
|
5368
5842
|
|
|
5369
5843
|
// src/cleanup-run-liveness.ts
|
|
5370
5844
|
function isWorkerProcessLive(indexed) {
|
|
@@ -5385,12 +5859,30 @@ function runBlocksWorktreeRemoval(indexed) {
|
|
|
5385
5859
|
return deriveTerminalRunStatus(indexed.run) === null;
|
|
5386
5860
|
}
|
|
5387
5861
|
|
|
5862
|
+
// src/cleanup-build-cache-paths.ts
|
|
5863
|
+
var HARNESS_BUILD_CACHE_RELATIVE_PATHS = [
|
|
5864
|
+
".next",
|
|
5865
|
+
".turbo",
|
|
5866
|
+
"dist",
|
|
5867
|
+
"build",
|
|
5868
|
+
".cache",
|
|
5869
|
+
"node_modules/.cache"
|
|
5870
|
+
];
|
|
5871
|
+
function isGeneratedHarnessPath(pathPart) {
|
|
5872
|
+
const normalized = pathPart.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
5873
|
+
if (normalized === "node_modules" || normalized.startsWith("node_modules/")) return true;
|
|
5874
|
+
for (const rel of HARNESS_BUILD_CACHE_RELATIVE_PATHS) {
|
|
5875
|
+
if (normalized === rel || normalized.startsWith(`${rel}/`)) return true;
|
|
5876
|
+
}
|
|
5877
|
+
return false;
|
|
5878
|
+
}
|
|
5879
|
+
|
|
5388
5880
|
// src/cleanup-guards-helpers.ts
|
|
5389
5881
|
function materialWorktreeChanges2(changedFiles) {
|
|
5390
5882
|
return changedFiles.filter((line) => {
|
|
5391
5883
|
const trimmed = line.trim();
|
|
5392
5884
|
const pathPart = trimmed.startsWith("??") ? trimmed.slice(2).trim() : trimmed.length > 3 ? trimmed.slice(3).trim() : trimmed;
|
|
5393
|
-
return
|
|
5885
|
+
return !isGeneratedHarnessPath(pathPart);
|
|
5394
5886
|
});
|
|
5395
5887
|
}
|
|
5396
5888
|
|
|
@@ -5455,35 +5947,27 @@ function skipWorktreeRemoval(input) {
|
|
|
5455
5947
|
}
|
|
5456
5948
|
return null;
|
|
5457
5949
|
}
|
|
5458
|
-
function
|
|
5459
|
-
const { indexed,
|
|
5460
|
-
if (ageMs < nodeModulesAgeMs) return "below_age_threshold";
|
|
5461
|
-
if (
|
|
5462
|
-
if (isWorkerProcessLive(indexed)) return "active_worker";
|
|
5463
|
-
if (indexed.
|
|
5464
|
-
if (!isFinishedWorkerStatus(indexed.status)) return "run_still_active";
|
|
5465
|
-
if (hasUnrestorableWorktreeChanges(indexed.status)) return "dirty_worktree";
|
|
5466
|
-
const landing = assessWorkerLanding({
|
|
5467
|
-
finalResult: indexed.status.finalResult,
|
|
5468
|
-
changedFiles: indexed.status.changedFiles,
|
|
5469
|
-
gitAncestry: indexed.status.gitAncestry,
|
|
5470
|
-
prUrl: prUrlFromFinalResult(indexed.status.finalResult)
|
|
5471
|
-
});
|
|
5472
|
-
if (landing.blocked && materialWorktreeChanges2(indexed.status.changedFiles).length > 0) {
|
|
5473
|
-
return "landing_blocked";
|
|
5474
|
-
}
|
|
5950
|
+
function skipDependencyCacheRemoval(input) {
|
|
5951
|
+
const { indexed, nodeModulesAgeMs, ageMs, worktreePath, activeWorktreePaths, diskPressure } = input;
|
|
5952
|
+
if (!diskPressure && ageMs < nodeModulesAgeMs) return "below_age_threshold";
|
|
5953
|
+
if (activeWorktreePaths.has(path28.resolve(worktreePath))) return "active_worker";
|
|
5954
|
+
if (indexed && isWorkerProcessLive(indexed)) return "active_worker";
|
|
5955
|
+
if (indexed && hasUnrestorableWorktreeChanges(indexed.status)) return "dirty_worktree";
|
|
5475
5956
|
return null;
|
|
5476
5957
|
}
|
|
5958
|
+
function skipBuildCacheRemoval(input) {
|
|
5959
|
+
return skipDependencyCacheRemoval(input);
|
|
5960
|
+
}
|
|
5477
5961
|
|
|
5478
5962
|
// src/cleanup-execute.ts
|
|
5479
|
-
import { existsSync as
|
|
5480
|
-
import
|
|
5963
|
+
import { existsSync as existsSync19, rmSync as rmSync2 } from "node:fs";
|
|
5964
|
+
import path30 from "node:path";
|
|
5481
5965
|
|
|
5482
5966
|
// src/cleanup-dir-size.ts
|
|
5483
|
-
import { existsSync as
|
|
5484
|
-
import
|
|
5967
|
+
import { existsSync as existsSync18, readdirSync as readdirSync5, statSync as statSync2 } from "node:fs";
|
|
5968
|
+
import path29 from "node:path";
|
|
5485
5969
|
function directorySizeBytes(root, maxEntries = 5e4) {
|
|
5486
|
-
if (!
|
|
5970
|
+
if (!existsSync18(root)) return 0;
|
|
5487
5971
|
let total = 0;
|
|
5488
5972
|
let seen = 0;
|
|
5489
5973
|
const stack = [root];
|
|
@@ -5497,7 +5981,7 @@ function directorySizeBytes(root, maxEntries = 5e4) {
|
|
|
5497
5981
|
}
|
|
5498
5982
|
for (const name of entries) {
|
|
5499
5983
|
if (seen++ > maxEntries) return null;
|
|
5500
|
-
const full =
|
|
5984
|
+
const full = path29.join(current, name);
|
|
5501
5985
|
let st;
|
|
5502
5986
|
try {
|
|
5503
5987
|
st = statSync2(full);
|
|
@@ -5512,8 +5996,8 @@ function directorySizeBytes(root, maxEntries = 5e4) {
|
|
|
5512
5996
|
}
|
|
5513
5997
|
|
|
5514
5998
|
// src/cleanup-execute.ts
|
|
5515
|
-
function
|
|
5516
|
-
if (!
|
|
5999
|
+
function removeDependencyCache(candidate, execute) {
|
|
6000
|
+
if (!existsSync19(candidate.path)) {
|
|
5517
6001
|
return {
|
|
5518
6002
|
...candidate,
|
|
5519
6003
|
executed: false,
|
|
@@ -5543,8 +6027,17 @@ function removeNodeModules(candidate, execute) {
|
|
|
5543
6027
|
};
|
|
5544
6028
|
}
|
|
5545
6029
|
}
|
|
6030
|
+
function removeNodeModules(candidate, execute) {
|
|
6031
|
+
return removeDependencyCache(candidate, execute);
|
|
6032
|
+
}
|
|
6033
|
+
function removeNextCache(candidate, execute) {
|
|
6034
|
+
return removeDependencyCache(candidate, execute);
|
|
6035
|
+
}
|
|
6036
|
+
function removeBuildCache(candidate, execute) {
|
|
6037
|
+
return removeDependencyCache(candidate, execute);
|
|
6038
|
+
}
|
|
5546
6039
|
function removeWorktree(candidate, execute) {
|
|
5547
|
-
if (!
|
|
6040
|
+
if (!existsSync19(candidate.path)) {
|
|
5548
6041
|
return {
|
|
5549
6042
|
...candidate,
|
|
5550
6043
|
executed: false,
|
|
@@ -5561,7 +6054,7 @@ function removeWorktree(candidate, execute) {
|
|
|
5561
6054
|
if (repo) {
|
|
5562
6055
|
git(repo, ["worktree", "remove", "--force", candidate.path], { allowFailure: true });
|
|
5563
6056
|
}
|
|
5564
|
-
if (
|
|
6057
|
+
if (existsSync19(candidate.path)) {
|
|
5565
6058
|
rmSync2(candidate.path, { recursive: true, force: true });
|
|
5566
6059
|
}
|
|
5567
6060
|
return {
|
|
@@ -5580,21 +6073,37 @@ function removeWorktree(candidate, execute) {
|
|
|
5580
6073
|
};
|
|
5581
6074
|
}
|
|
5582
6075
|
}
|
|
6076
|
+
function isHarnessDependencyCachePath(targetPath, harnessRoot, worktreesDir, cacheDirName) {
|
|
6077
|
+
const resolved = path30.resolve(targetPath);
|
|
6078
|
+
const suffix = `${path30.sep}${cacheDirName}`;
|
|
6079
|
+
const cachePath = resolved.endsWith(suffix) ? resolved : null;
|
|
6080
|
+
if (!cachePath) return "path_outside_harness";
|
|
6081
|
+
const rel = path30.relative(worktreesDir, cachePath);
|
|
6082
|
+
if (rel.startsWith("..") || path30.isAbsolute(rel)) return "path_outside_harness";
|
|
6083
|
+
const parts = rel.split(path30.sep);
|
|
6084
|
+
if (parts.length < 3 || parts[parts.length - 1] !== cacheDirName) return "path_outside_harness";
|
|
6085
|
+
if (!resolved.startsWith(path30.resolve(harnessRoot))) return "path_outside_harness";
|
|
6086
|
+
return null;
|
|
6087
|
+
}
|
|
5583
6088
|
function isHarnessNodeModulesPath(targetPath, harnessRoot, worktreesDir) {
|
|
5584
|
-
|
|
5585
|
-
|
|
5586
|
-
|
|
5587
|
-
|
|
5588
|
-
|
|
5589
|
-
|
|
5590
|
-
|
|
5591
|
-
|
|
6089
|
+
return isHarnessDependencyCachePath(targetPath, harnessRoot, worktreesDir, "node_modules");
|
|
6090
|
+
}
|
|
6091
|
+
function isHarnessNextCachePath(targetPath, harnessRoot, worktreesDir) {
|
|
6092
|
+
return isHarnessDependencyCachePath(targetPath, harnessRoot, worktreesDir, ".next");
|
|
6093
|
+
}
|
|
6094
|
+
function isHarnessBuildCachePath(targetPath, harnessRoot, worktreesDir) {
|
|
6095
|
+
const resolved = path30.resolve(targetPath);
|
|
6096
|
+
const relToWt = path30.relative(worktreesDir, resolved);
|
|
6097
|
+
if (relToWt.startsWith("..") || path30.isAbsolute(relToWt)) return "path_outside_harness";
|
|
6098
|
+
const parts = relToWt.split(path30.sep);
|
|
6099
|
+
if (parts.length < 3) return "path_outside_harness";
|
|
6100
|
+
if (!resolved.startsWith(path30.resolve(harnessRoot))) return "path_outside_harness";
|
|
5592
6101
|
return null;
|
|
5593
6102
|
}
|
|
5594
6103
|
|
|
5595
6104
|
// src/cleanup-scan.ts
|
|
5596
|
-
import { existsSync as
|
|
5597
|
-
import
|
|
6105
|
+
import { existsSync as existsSync20, readdirSync as readdirSync6, statSync as statSync3 } from "node:fs";
|
|
6106
|
+
import path31 from "node:path";
|
|
5598
6107
|
function pathAgeMs(target, now) {
|
|
5599
6108
|
try {
|
|
5600
6109
|
const mtime = statSync3(target).mtimeMs;
|
|
@@ -5604,50 +6113,57 @@ function pathAgeMs(target, now) {
|
|
|
5604
6113
|
}
|
|
5605
6114
|
}
|
|
5606
6115
|
function isPathInside(child, parent) {
|
|
5607
|
-
const rel =
|
|
5608
|
-
return rel === "" || !rel.startsWith("..") && !
|
|
6116
|
+
const rel = path31.relative(parent, child);
|
|
6117
|
+
return rel === "" || !rel.startsWith("..") && !path31.isAbsolute(rel);
|
|
5609
6118
|
}
|
|
5610
|
-
function
|
|
5611
|
-
const
|
|
5612
|
-
const
|
|
5613
|
-
|
|
5614
|
-
|
|
5615
|
-
|
|
5616
|
-
|
|
5617
|
-
const resolved = path29.resolve(nm);
|
|
6119
|
+
function collectBuildCacheForWorktree(worktreePath, opts, seen, meta) {
|
|
6120
|
+
const out = [];
|
|
6121
|
+
for (const rel of HARNESS_BUILD_CACHE_RELATIVE_PATHS) {
|
|
6122
|
+
if (rel === ".next") continue;
|
|
6123
|
+
const target = path31.join(worktreePath, rel);
|
|
6124
|
+
if (!existsSync20(target)) continue;
|
|
6125
|
+
const resolved = path31.resolve(target);
|
|
5618
6126
|
if (seen.has(resolved)) continue;
|
|
6127
|
+
if (!isPathInside(resolved, opts.harnessRoot)) continue;
|
|
5619
6128
|
seen.add(resolved);
|
|
5620
|
-
|
|
5621
|
-
kind: "
|
|
6129
|
+
out.push({
|
|
6130
|
+
kind: "remove_build_cache",
|
|
5622
6131
|
path: resolved,
|
|
5623
6132
|
bytes: null,
|
|
5624
|
-
runId:
|
|
5625
|
-
worker:
|
|
5626
|
-
repo:
|
|
6133
|
+
runId: meta.runId,
|
|
6134
|
+
worker: meta.worker,
|
|
6135
|
+
repo: meta.repo,
|
|
5627
6136
|
ageMs: pathAgeMs(resolved, opts.now)
|
|
5628
6137
|
});
|
|
5629
6138
|
}
|
|
5630
|
-
|
|
6139
|
+
return out;
|
|
6140
|
+
}
|
|
6141
|
+
function scanBuildCacheCandidates(opts) {
|
|
6142
|
+
const candidates = [];
|
|
6143
|
+
const seen = /* @__PURE__ */ new Set();
|
|
6144
|
+
for (const entry of opts.index.values()) {
|
|
6145
|
+
if (opts.runIdFilter && entry.runId !== opts.runIdFilter) continue;
|
|
6146
|
+
candidates.push(
|
|
6147
|
+
...collectBuildCacheForWorktree(entry.worktreePath, opts, seen, {
|
|
6148
|
+
runId: entry.runId,
|
|
6149
|
+
worker: entry.workerName,
|
|
6150
|
+
repo: entry.run.repo
|
|
6151
|
+
})
|
|
6152
|
+
);
|
|
6153
|
+
}
|
|
6154
|
+
if (!opts.includeOrphans || !existsSync20(opts.worktreesDir)) return candidates;
|
|
5631
6155
|
for (const runEntry of readdirSync6(opts.worktreesDir, { withFileTypes: true })) {
|
|
5632
6156
|
if (!runEntry.isDirectory()) continue;
|
|
5633
|
-
const runPath =
|
|
6157
|
+
const runPath = path31.join(opts.worktreesDir, runEntry.name);
|
|
5634
6158
|
for (const workerEntry of readdirSync6(runPath, { withFileTypes: true })) {
|
|
5635
6159
|
if (!workerEntry.isDirectory()) continue;
|
|
5636
|
-
const worktreePath =
|
|
5637
|
-
|
|
5638
|
-
|
|
5639
|
-
|
|
5640
|
-
|
|
5641
|
-
|
|
5642
|
-
|
|
5643
|
-
candidates.push({
|
|
5644
|
-
kind: "remove_node_modules",
|
|
5645
|
-
path: resolved,
|
|
5646
|
-
bytes: null,
|
|
5647
|
-
runId: runEntry.name,
|
|
5648
|
-
worker: workerEntry.name,
|
|
5649
|
-
ageMs: pathAgeMs(resolved, opts.now)
|
|
5650
|
-
});
|
|
6160
|
+
const worktreePath = path31.join(runPath, workerEntry.name);
|
|
6161
|
+
candidates.push(
|
|
6162
|
+
...collectBuildCacheForWorktree(worktreePath, opts, seen, {
|
|
6163
|
+
runId: runEntry.name,
|
|
6164
|
+
worker: workerEntry.name
|
|
6165
|
+
})
|
|
6166
|
+
);
|
|
5651
6167
|
}
|
|
5652
6168
|
}
|
|
5653
6169
|
return candidates;
|
|
@@ -5662,7 +6178,7 @@ function scanWorktreeCandidates(opts) {
|
|
|
5662
6178
|
for (const entry of opts.index.values()) {
|
|
5663
6179
|
if (opts.runIdFilter && entry.runId !== opts.runIdFilter) continue;
|
|
5664
6180
|
const resolved = entry.worktreePath;
|
|
5665
|
-
if (!
|
|
6181
|
+
if (!existsSync20(resolved)) continue;
|
|
5666
6182
|
if (seen.has(resolved)) continue;
|
|
5667
6183
|
seen.add(resolved);
|
|
5668
6184
|
candidates.push({
|
|
@@ -5676,15 +6192,15 @@ function scanWorktreeCandidates(opts) {
|
|
|
5676
6192
|
});
|
|
5677
6193
|
}
|
|
5678
6194
|
}
|
|
5679
|
-
if (!orphanEnabled || !
|
|
6195
|
+
if (!orphanEnabled || !existsSync20(opts.worktreesDir)) return candidates;
|
|
5680
6196
|
const indexedPaths = /* @__PURE__ */ new Set();
|
|
5681
6197
|
for (const entry of opts.index.values()) {
|
|
5682
|
-
indexedPaths.add(
|
|
6198
|
+
indexedPaths.add(path31.resolve(entry.worktreePath));
|
|
5683
6199
|
}
|
|
5684
6200
|
for (const runEntry of readdirSync6(opts.worktreesDir, { withFileTypes: true })) {
|
|
5685
6201
|
if (!runEntry.isDirectory()) continue;
|
|
5686
6202
|
if (opts.runIdFilter && runEntry.name !== opts.runIdFilter) continue;
|
|
5687
|
-
const runPath =
|
|
6203
|
+
const runPath = path31.join(opts.worktreesDir, runEntry.name);
|
|
5688
6204
|
let workerEntries;
|
|
5689
6205
|
try {
|
|
5690
6206
|
workerEntries = readdirSync6(runPath, { withFileTypes: true });
|
|
@@ -5693,7 +6209,7 @@ function scanWorktreeCandidates(opts) {
|
|
|
5693
6209
|
}
|
|
5694
6210
|
for (const workerEntry of workerEntries) {
|
|
5695
6211
|
if (!workerEntry.isDirectory()) continue;
|
|
5696
|
-
const worktreePath =
|
|
6212
|
+
const worktreePath = path31.resolve(path31.join(runPath, workerEntry.name));
|
|
5697
6213
|
if (seen.has(worktreePath)) continue;
|
|
5698
6214
|
if (indexedPaths.has(worktreePath)) continue;
|
|
5699
6215
|
if (!isPathInside(worktreePath, opts.harnessRoot)) continue;
|
|
@@ -5711,18 +6227,191 @@ function scanWorktreeCandidates(opts) {
|
|
|
5711
6227
|
return candidates;
|
|
5712
6228
|
}
|
|
5713
6229
|
|
|
6230
|
+
// src/cleanup-dependency-scan.ts
|
|
6231
|
+
import { existsSync as existsSync21, readdirSync as readdirSync7, statSync as statSync4 } from "node:fs";
|
|
6232
|
+
import path32 from "node:path";
|
|
6233
|
+
var DEPENDENCY_CACHE_DIRS = [
|
|
6234
|
+
{ dirName: "node_modules", kind: "remove_node_modules" },
|
|
6235
|
+
{ dirName: ".next", kind: "remove_next_cache" }
|
|
6236
|
+
];
|
|
6237
|
+
function pathAgeMs2(target, now) {
|
|
6238
|
+
try {
|
|
6239
|
+
const mtime = statSync4(target).mtimeMs;
|
|
6240
|
+
return Math.max(0, now - mtime);
|
|
6241
|
+
} catch {
|
|
6242
|
+
return 0;
|
|
6243
|
+
}
|
|
6244
|
+
}
|
|
6245
|
+
function isPathInside2(child, parent) {
|
|
6246
|
+
const rel = path32.relative(parent, child);
|
|
6247
|
+
return rel === "" || !rel.startsWith("..") && !path32.isAbsolute(rel);
|
|
6248
|
+
}
|
|
6249
|
+
function pushCandidate2(candidates, seen, opts, targetPath, kind, meta) {
|
|
6250
|
+
if (!existsSync21(targetPath)) return;
|
|
6251
|
+
const resolved = path32.resolve(targetPath);
|
|
6252
|
+
if (seen.has(resolved)) return;
|
|
6253
|
+
if (!isPathInside2(resolved, opts.harnessRoot)) return;
|
|
6254
|
+
seen.add(resolved);
|
|
6255
|
+
candidates.push({
|
|
6256
|
+
kind,
|
|
6257
|
+
path: resolved,
|
|
6258
|
+
bytes: null,
|
|
6259
|
+
harnessRoot: opts.harnessRoot,
|
|
6260
|
+
runId: meta.runId,
|
|
6261
|
+
worker: meta.worker,
|
|
6262
|
+
repo: meta.repo,
|
|
6263
|
+
ageMs: pathAgeMs2(resolved, opts.now)
|
|
6264
|
+
});
|
|
6265
|
+
}
|
|
6266
|
+
function scanWorktreeDependencyCaches(candidates, seen, opts, worktreePath, meta) {
|
|
6267
|
+
for (const entry of DEPENDENCY_CACHE_DIRS) {
|
|
6268
|
+
pushCandidate2(candidates, seen, opts, path32.join(worktreePath, entry.dirName), entry.kind, meta);
|
|
6269
|
+
}
|
|
6270
|
+
}
|
|
6271
|
+
function scanDependencyCacheCandidates(opts) {
|
|
6272
|
+
const candidates = [];
|
|
6273
|
+
const seen = /* @__PURE__ */ new Set();
|
|
6274
|
+
for (const entry of opts.index.values()) {
|
|
6275
|
+
if (opts.runIdFilter && entry.runId !== opts.runIdFilter) continue;
|
|
6276
|
+
scanWorktreeDependencyCaches(candidates, seen, opts, entry.worktreePath, {
|
|
6277
|
+
runId: entry.runId,
|
|
6278
|
+
worker: entry.workerName,
|
|
6279
|
+
repo: entry.run.repo
|
|
6280
|
+
});
|
|
6281
|
+
}
|
|
6282
|
+
if (!existsSync21(opts.worktreesDir)) return candidates;
|
|
6283
|
+
for (const runEntry of readdirSync7(opts.worktreesDir, { withFileTypes: true })) {
|
|
6284
|
+
if (!runEntry.isDirectory()) continue;
|
|
6285
|
+
if (opts.runIdFilter && runEntry.name !== opts.runIdFilter) continue;
|
|
6286
|
+
const runPath = path32.join(opts.worktreesDir, runEntry.name);
|
|
6287
|
+
let workerEntries;
|
|
6288
|
+
try {
|
|
6289
|
+
workerEntries = readdirSync7(runPath, { withFileTypes: true });
|
|
6290
|
+
} catch {
|
|
6291
|
+
continue;
|
|
6292
|
+
}
|
|
6293
|
+
for (const workerEntry of workerEntries) {
|
|
6294
|
+
if (!workerEntry.isDirectory()) continue;
|
|
6295
|
+
const worktreePath = path32.join(runPath, workerEntry.name);
|
|
6296
|
+
scanWorktreeDependencyCaches(candidates, seen, opts, worktreePath, {
|
|
6297
|
+
runId: runEntry.name,
|
|
6298
|
+
worker: workerEntry.name
|
|
6299
|
+
});
|
|
6300
|
+
}
|
|
6301
|
+
}
|
|
6302
|
+
return candidates;
|
|
6303
|
+
}
|
|
6304
|
+
|
|
6305
|
+
// src/cleanup-duplicate-worktrees.ts
|
|
6306
|
+
import { existsSync as existsSync22, statSync as statSync5 } from "node:fs";
|
|
6307
|
+
import path33 from "node:path";
|
|
6308
|
+
function pathAgeMs3(target, now) {
|
|
6309
|
+
try {
|
|
6310
|
+
const mtime = statSync5(target).mtimeMs;
|
|
6311
|
+
return Math.max(0, now - mtime);
|
|
6312
|
+
} catch {
|
|
6313
|
+
return 0;
|
|
6314
|
+
}
|
|
6315
|
+
}
|
|
6316
|
+
function parseWorktreePorcelain(output) {
|
|
6317
|
+
const records = [];
|
|
6318
|
+
let current = null;
|
|
6319
|
+
for (const line of output.split("\n")) {
|
|
6320
|
+
if (!line.trim()) continue;
|
|
6321
|
+
const [key, ...rest] = line.split(" ");
|
|
6322
|
+
const value = rest.join(" ");
|
|
6323
|
+
if (key === "worktree") {
|
|
6324
|
+
if (current) records.push(current);
|
|
6325
|
+
current = { path: value };
|
|
6326
|
+
continue;
|
|
6327
|
+
}
|
|
6328
|
+
if (!current) continue;
|
|
6329
|
+
if (key === "branch") current.branch = value;
|
|
6330
|
+
if (key === "HEAD") current.head = value;
|
|
6331
|
+
if (key === "bare") current.bare = true;
|
|
6332
|
+
}
|
|
6333
|
+
if (current) records.push(current);
|
|
6334
|
+
return records;
|
|
6335
|
+
}
|
|
6336
|
+
function isUnderWorktreesDir(worktreePath, worktreesDir) {
|
|
6337
|
+
const rel = path33.relative(path33.resolve(worktreesDir), path33.resolve(worktreePath));
|
|
6338
|
+
return rel !== "" && !rel.startsWith("..") && !path33.isAbsolute(rel);
|
|
6339
|
+
}
|
|
6340
|
+
function isCleanWorktree(worktreePath, repoRoot) {
|
|
6341
|
+
try {
|
|
6342
|
+
const porcelain = git(repoRoot, ["-C", worktreePath, "status", "--porcelain"], {
|
|
6343
|
+
allowFailure: true
|
|
6344
|
+
});
|
|
6345
|
+
return !String(porcelain || "").trim();
|
|
6346
|
+
} catch {
|
|
6347
|
+
return false;
|
|
6348
|
+
}
|
|
6349
|
+
}
|
|
6350
|
+
function scanDuplicateWorktreeCandidates(opts) {
|
|
6351
|
+
if (!opts.includeOrphans || !existsSync22(opts.worktreesDir)) return [];
|
|
6352
|
+
const repos = /* @__PURE__ */ new Set();
|
|
6353
|
+
for (const entry of opts.index.values()) {
|
|
6354
|
+
if (entry.run.repo) repos.add(path33.resolve(entry.run.repo));
|
|
6355
|
+
}
|
|
6356
|
+
const indexedPaths = /* @__PURE__ */ new Set();
|
|
6357
|
+
for (const entry of opts.index.values()) {
|
|
6358
|
+
indexedPaths.add(path33.resolve(entry.worktreePath));
|
|
6359
|
+
}
|
|
6360
|
+
const candidates = [];
|
|
6361
|
+
const seen = /* @__PURE__ */ new Set();
|
|
6362
|
+
for (const repoRoot of repos) {
|
|
6363
|
+
let porcelain;
|
|
6364
|
+
try {
|
|
6365
|
+
porcelain = git(repoRoot, ["worktree", "list", "--porcelain"], { allowFailure: true });
|
|
6366
|
+
} catch {
|
|
6367
|
+
continue;
|
|
6368
|
+
}
|
|
6369
|
+
const worktrees = parseWorktreePorcelain(porcelain);
|
|
6370
|
+
for (const wt of worktrees) {
|
|
6371
|
+
const resolved = path33.resolve(wt.path);
|
|
6372
|
+
if (resolved === path33.resolve(repoRoot)) continue;
|
|
6373
|
+
if (!isUnderWorktreesDir(resolved, opts.worktreesDir)) continue;
|
|
6374
|
+
if (indexedPaths.has(resolved)) continue;
|
|
6375
|
+
if (seen.has(resolved)) continue;
|
|
6376
|
+
if (!existsSync22(resolved)) continue;
|
|
6377
|
+
if (!isCleanWorktree(resolved, repoRoot)) continue;
|
|
6378
|
+
const rel = path33.relative(opts.worktreesDir, resolved);
|
|
6379
|
+
const parts = rel.split(path33.sep);
|
|
6380
|
+
const runId = parts[0];
|
|
6381
|
+
const worker = parts[1] ?? "unknown";
|
|
6382
|
+
seen.add(resolved);
|
|
6383
|
+
candidates.push({
|
|
6384
|
+
kind: "remove_worktree",
|
|
6385
|
+
path: resolved,
|
|
6386
|
+
bytes: null,
|
|
6387
|
+
runId,
|
|
6388
|
+
worker,
|
|
6389
|
+
repo: repoRoot,
|
|
6390
|
+
ageMs: pathAgeMs3(resolved, opts.now)
|
|
6391
|
+
});
|
|
6392
|
+
}
|
|
6393
|
+
}
|
|
6394
|
+
return candidates;
|
|
6395
|
+
}
|
|
6396
|
+
|
|
5714
6397
|
// src/cleanup-worktree-index.ts
|
|
5715
|
-
import
|
|
5716
|
-
function
|
|
6398
|
+
import path34 from "node:path";
|
|
6399
|
+
function buildWorktreeIndexAt(harnessRoot) {
|
|
5717
6400
|
const index = /* @__PURE__ */ new Map();
|
|
5718
|
-
for (const run of
|
|
6401
|
+
for (const run of listRunRecordsForHarnessRoot(harnessRoot)) {
|
|
5719
6402
|
for (const name of Object.keys(run.workers || {})) {
|
|
5720
|
-
const workerPath =
|
|
6403
|
+
const workerPath = path34.join(
|
|
6404
|
+
runDirectoryAt(harnessRoot, run.id),
|
|
6405
|
+
"workers",
|
|
6406
|
+
safeSlug(name),
|
|
6407
|
+
"worker.json"
|
|
6408
|
+
);
|
|
5721
6409
|
const worker = readJson(workerPath, void 0);
|
|
5722
6410
|
if (!worker?.worktreePath) continue;
|
|
5723
6411
|
const status = computeWorkerStatus(worker, { base: run.base, baseCommit: run.baseCommit });
|
|
5724
|
-
index.set(
|
|
5725
|
-
|
|
6412
|
+
index.set(path34.resolve(worker.worktreePath), {
|
|
6413
|
+
harnessRoot,
|
|
6414
|
+
worktreePath: path34.resolve(worker.worktreePath),
|
|
5726
6415
|
runId: run.id,
|
|
5727
6416
|
workerName: name,
|
|
5728
6417
|
run,
|
|
@@ -5781,15 +6470,15 @@ function resolvePipelineHarnessRetention(runId) {
|
|
|
5781
6470
|
}
|
|
5782
6471
|
|
|
5783
6472
|
// src/cleanup-orphan-safety.ts
|
|
5784
|
-
import { existsSync as
|
|
5785
|
-
import
|
|
6473
|
+
import { existsSync as existsSync23, statSync as statSync6 } from "node:fs";
|
|
6474
|
+
import path35 from "node:path";
|
|
5786
6475
|
var DEFAULT_HEARTBEAT_FRESH_MS = 30 * 60 * 1e3;
|
|
5787
6476
|
function assessOrphanWorktreeSafety(input) {
|
|
5788
6477
|
const now = input.now ?? Date.now();
|
|
5789
6478
|
const heartbeatFreshMs = input.heartbeatFreshMs ?? DEFAULT_HEARTBEAT_FRESH_MS;
|
|
5790
|
-
if (!
|
|
6479
|
+
if (!existsSync23(input.worktreePath)) return null;
|
|
5791
6480
|
if (input.runId && input.workerName) {
|
|
5792
|
-
const heartbeatPath =
|
|
6481
|
+
const heartbeatPath = path35.join(
|
|
5793
6482
|
input.harnessRoot,
|
|
5794
6483
|
"runs",
|
|
5795
6484
|
input.runId,
|
|
@@ -5798,13 +6487,13 @@ function assessOrphanWorktreeSafety(input) {
|
|
|
5798
6487
|
"heartbeat.jsonl"
|
|
5799
6488
|
);
|
|
5800
6489
|
try {
|
|
5801
|
-
const mtime =
|
|
6490
|
+
const mtime = statSync6(heartbeatPath).mtimeMs;
|
|
5802
6491
|
if (now - mtime < heartbeatFreshMs) return "active_worker";
|
|
5803
6492
|
} catch {
|
|
5804
6493
|
}
|
|
5805
6494
|
}
|
|
5806
|
-
const gitDir =
|
|
5807
|
-
if (!
|
|
6495
|
+
const gitDir = path35.join(input.worktreePath, ".git");
|
|
6496
|
+
if (!existsSync23(gitDir)) return null;
|
|
5808
6497
|
const porcelain = gitCapture(input.worktreePath, ["status", "--porcelain"]);
|
|
5809
6498
|
if (porcelain.status !== 0) return "pr_or_unmerged_commits";
|
|
5810
6499
|
const dirtyLines = porcelain.stdout.split("\n").map((line) => line.trim()).filter((line) => line.length > 0);
|
|
@@ -5833,14 +6522,14 @@ function assessOrphanWorktreeSafety(input) {
|
|
|
5833
6522
|
}
|
|
5834
6523
|
|
|
5835
6524
|
// src/harness-storage-snapshot.ts
|
|
5836
|
-
import { existsSync as
|
|
5837
|
-
import
|
|
6525
|
+
import { existsSync as existsSync24, readdirSync as readdirSync8, statSync as statSync7 } from "node:fs";
|
|
6526
|
+
import path36 from "node:path";
|
|
5838
6527
|
function harnessStorageSnapshot(opts = {}) {
|
|
5839
6528
|
const harnessRoot = opts.harnessRoot ?? resolveHarnessRoot();
|
|
5840
|
-
const worktreesDir =
|
|
6529
|
+
const worktreesDir = path36.join(harnessRoot, "worktrees");
|
|
5841
6530
|
const now = opts.now ?? Date.now();
|
|
5842
6531
|
const scannedAt = new Date(now).toISOString();
|
|
5843
|
-
if (!
|
|
6532
|
+
if (!existsSync24(worktreesDir)) {
|
|
5844
6533
|
return {
|
|
5845
6534
|
harnessRoot,
|
|
5846
6535
|
worktreesDir,
|
|
@@ -5857,7 +6546,7 @@ function harnessStorageSnapshot(opts = {}) {
|
|
|
5857
6546
|
let oldestMs = null;
|
|
5858
6547
|
let entries;
|
|
5859
6548
|
try {
|
|
5860
|
-
entries =
|
|
6549
|
+
entries = readdirSync8(worktreesDir, { withFileTypes: true });
|
|
5861
6550
|
} catch {
|
|
5862
6551
|
return {
|
|
5863
6552
|
harnessRoot,
|
|
@@ -5872,14 +6561,14 @@ function harnessStorageSnapshot(opts = {}) {
|
|
|
5872
6561
|
for (const runEntry of entries) {
|
|
5873
6562
|
if (!runEntry.isDirectory()) continue;
|
|
5874
6563
|
runCount += 1;
|
|
5875
|
-
const runPath =
|
|
6564
|
+
const runPath = path36.join(worktreesDir, runEntry.name);
|
|
5876
6565
|
try {
|
|
5877
|
-
const st =
|
|
6566
|
+
const st = statSync7(runPath);
|
|
5878
6567
|
oldestMs = oldestMs === null ? st.mtimeMs : Math.min(oldestMs, st.mtimeMs);
|
|
5879
6568
|
} catch {
|
|
5880
6569
|
}
|
|
5881
6570
|
try {
|
|
5882
|
-
for (const workerEntry of
|
|
6571
|
+
for (const workerEntry of readdirSync8(runPath, { withFileTypes: true })) {
|
|
5883
6572
|
if (workerEntry.isDirectory()) workerCount += 1;
|
|
5884
6573
|
}
|
|
5885
6574
|
} catch {
|
|
@@ -5906,12 +6595,110 @@ function harnessStorageSnapshot(opts = {}) {
|
|
|
5906
6595
|
};
|
|
5907
6596
|
}
|
|
5908
6597
|
|
|
6598
|
+
// src/cleanup-harness-roots.ts
|
|
6599
|
+
import { existsSync as existsSync25 } from "node:fs";
|
|
6600
|
+
import { homedir as homedir6 } from "node:os";
|
|
6601
|
+
import path37 from "node:path";
|
|
6602
|
+
var WELL_KNOWN_HARNESS_SCAN_ROOTS = [
|
|
6603
|
+
"/var/tmp/kynver-harness",
|
|
6604
|
+
path37.join(homedir6(), ".openclaw", "harness")
|
|
6605
|
+
];
|
|
6606
|
+
function addRoot(seen, roots, candidate) {
|
|
6607
|
+
if (!candidate?.trim()) return;
|
|
6608
|
+
const resolved = path37.resolve(resolveUserPath(candidate.trim()));
|
|
6609
|
+
if (seen.has(resolved)) return;
|
|
6610
|
+
seen.add(resolved);
|
|
6611
|
+
roots.push(resolved);
|
|
6612
|
+
}
|
|
6613
|
+
function shouldScanWellKnownRoots(options) {
|
|
6614
|
+
if (options.scanWellKnown != null) return options.scanWellKnown;
|
|
6615
|
+
if (process.env.VITEST === "true") return false;
|
|
6616
|
+
return process.env.KYNVER_CLEANUP_SCAN_WELL_KNOWN !== "0" && !["0", "false", "no"].includes((process.env.KYNVER_CLEANUP_SCAN_WELL_KNOWN ?? "").toLowerCase());
|
|
6617
|
+
}
|
|
6618
|
+
function resolveHarnessScanRoots(options = {}) {
|
|
6619
|
+
const seen = /* @__PURE__ */ new Set();
|
|
6620
|
+
const roots = [];
|
|
6621
|
+
addRoot(seen, roots, options.harnessRoot ?? resolveHarnessRoot());
|
|
6622
|
+
const extra = process.env.KYNVER_CLEANUP_EXTRA_ROOTS?.split(",").map((part) => part.trim()).filter(Boolean);
|
|
6623
|
+
for (const candidate of extra ?? []) addRoot(seen, roots, candidate);
|
|
6624
|
+
if (shouldScanWellKnownRoots(options)) {
|
|
6625
|
+
for (const candidate of WELL_KNOWN_HARNESS_SCAN_ROOTS) {
|
|
6626
|
+
const resolved = path37.resolve(candidate);
|
|
6627
|
+
if (!seen.has(resolved) && existsSync25(resolved)) addRoot(seen, roots, resolved);
|
|
6628
|
+
}
|
|
6629
|
+
}
|
|
6630
|
+
return roots;
|
|
6631
|
+
}
|
|
6632
|
+
|
|
6633
|
+
// src/cleanup-active-worktrees.ts
|
|
6634
|
+
import path38 from "node:path";
|
|
6635
|
+
function isActiveHarnessWorker2(worker, runBase, runBaseCommit) {
|
|
6636
|
+
const status = computeWorkerStatus(worker, { base: runBase, baseCommit: runBaseCommit });
|
|
6637
|
+
return status.alive && !status.finalResult && status.attention.state !== "done";
|
|
6638
|
+
}
|
|
6639
|
+
function collectActiveWorktreeGuards(harnessRoots) {
|
|
6640
|
+
const activeWorktreePaths = /* @__PURE__ */ new Set();
|
|
6641
|
+
const liveRunKeys = /* @__PURE__ */ new Set();
|
|
6642
|
+
for (const harnessRoot of harnessRoots) {
|
|
6643
|
+
for (const run of listRunRecordsForHarnessRoot(harnessRoot)) {
|
|
6644
|
+
let runHasLive = false;
|
|
6645
|
+
for (const name of Object.keys(run.workers || {})) {
|
|
6646
|
+
const worker = readJson(
|
|
6647
|
+
path38.join(runDirectoryAt(harnessRoot, run.id), "workers", safeSlug(name), "worker.json"),
|
|
6648
|
+
void 0
|
|
6649
|
+
);
|
|
6650
|
+
if (!worker?.worktreePath) continue;
|
|
6651
|
+
const worktreePath = path38.resolve(worker.worktreePath);
|
|
6652
|
+
if (!isActiveHarnessWorker2(worker, run.base, run.baseCommit)) continue;
|
|
6653
|
+
runHasLive = true;
|
|
6654
|
+
activeWorktreePaths.add(worktreePath);
|
|
6655
|
+
}
|
|
6656
|
+
if (runHasLive) liveRunKeys.add(`${harnessRoot}\0${run.id}`);
|
|
6657
|
+
}
|
|
6658
|
+
}
|
|
6659
|
+
return { activeWorktreePaths, liveRunKeys };
|
|
6660
|
+
}
|
|
6661
|
+
|
|
6662
|
+
// src/cleanup-disk-pressure.ts
|
|
6663
|
+
function envFlag2(name) {
|
|
6664
|
+
const v = process.env[name];
|
|
6665
|
+
return v === "1" || v === "true" || v === "yes";
|
|
6666
|
+
}
|
|
6667
|
+
function envNumber(name, fallback) {
|
|
6668
|
+
const raw = process.env[name];
|
|
6669
|
+
if (!raw) return fallback;
|
|
6670
|
+
const n = Number(raw);
|
|
6671
|
+
return Number.isFinite(n) ? n : fallback;
|
|
6672
|
+
}
|
|
6673
|
+
function observeCleanupDiskPressure(input = {}) {
|
|
6674
|
+
const diskPath = input.diskPath?.trim() || process.env.KYNVER_DISK_GUARD_PATH?.trim() || "/";
|
|
6675
|
+
const maxUsedPercent = envNumber("KYNVER_DISK_GUARD_MAX_USED_PERCENT", 75);
|
|
6676
|
+
const diskGate = observeRunnerDiskGate({
|
|
6677
|
+
...input,
|
|
6678
|
+
diskPath,
|
|
6679
|
+
diskMaxUsedPercent: input.diskMaxUsedPercent ?? maxUsedPercent
|
|
6680
|
+
});
|
|
6681
|
+
const pressured = !diskGate.ok || diskGate.usedPercent >= maxUsedPercent;
|
|
6682
|
+
return { diskGate, pressured, maxUsedPercent };
|
|
6683
|
+
}
|
|
6684
|
+
function applyDiskPressureToRetention(retention, pressure) {
|
|
6685
|
+
if (!pressure.pressured) return retention;
|
|
6686
|
+
const executeOnPressure = retention.execute || envFlag2("KYNVER_CLEANUP_EXECUTE_ON_PRESSURE");
|
|
6687
|
+
return {
|
|
6688
|
+
...retention,
|
|
6689
|
+
execute: executeOnPressure,
|
|
6690
|
+
nodeModulesAgeMs: 0,
|
|
6691
|
+
diskPressure: true,
|
|
6692
|
+
diskGate: pressure.diskGate
|
|
6693
|
+
};
|
|
6694
|
+
}
|
|
6695
|
+
|
|
5909
6696
|
// src/cleanup.ts
|
|
5910
6697
|
function resolvePaths(options = {}) {
|
|
5911
6698
|
const harnessRoot = options.harnessRoot ? resolveUserPath(options.harnessRoot) : resolveHarnessRoot();
|
|
5912
|
-
const
|
|
6699
|
+
const scanRoots = resolveHarnessScanRoots({ harnessRoot });
|
|
5913
6700
|
const now = options.now ?? Date.now();
|
|
5914
|
-
return { harnessRoot,
|
|
6701
|
+
return { harnessRoot, scanRoots, now };
|
|
5915
6702
|
}
|
|
5916
6703
|
function normalizeGuardSkip(skip) {
|
|
5917
6704
|
if (typeof skip === "string") return { reason: skip };
|
|
@@ -5936,72 +6723,145 @@ function tallySkipReasons(actions, skips) {
|
|
|
5936
6723
|
}
|
|
5937
6724
|
return counts;
|
|
5938
6725
|
}
|
|
6726
|
+
function removeDependencyCacheAction(candidate, execute) {
|
|
6727
|
+
if (candidate.kind === "remove_next_cache") return removeNextCache(candidate, execute);
|
|
6728
|
+
return removeNodeModules(candidate, execute);
|
|
6729
|
+
}
|
|
6730
|
+
function pathGuardForDependencyCache(candidate, harnessRoot, worktreesDir) {
|
|
6731
|
+
if (candidate.kind === "remove_next_cache") {
|
|
6732
|
+
return isHarnessNextCachePath(candidate.path, harnessRoot, worktreesDir);
|
|
6733
|
+
}
|
|
6734
|
+
return isHarnessNodeModulesPath(candidate.path, harnessRoot, worktreesDir);
|
|
6735
|
+
}
|
|
6736
|
+
function mergeWorktreeIndexes(scanRoots) {
|
|
6737
|
+
const merged = /* @__PURE__ */ new Map();
|
|
6738
|
+
for (const root of scanRoots) {
|
|
6739
|
+
for (const [key, value] of buildWorktreeIndexAt(root)) merged.set(key, value);
|
|
6740
|
+
}
|
|
6741
|
+
return merged;
|
|
6742
|
+
}
|
|
6743
|
+
function worktreePathForCandidate(candidate, worktreesDir) {
|
|
6744
|
+
if (candidate.runId && candidate.worker) {
|
|
6745
|
+
return path39.join(worktreesDir, candidate.runId, candidate.worker);
|
|
6746
|
+
}
|
|
6747
|
+
return path39.resolve(candidate.path, "..");
|
|
6748
|
+
}
|
|
5939
6749
|
function runHarnessCleanup(options = {}) {
|
|
5940
|
-
|
|
6750
|
+
let retention = resolveHarnessRetention(options);
|
|
6751
|
+
const diskPressure = observeCleanupDiskPressure();
|
|
6752
|
+
retention = applyDiskPressureToRetention(retention, diskPressure);
|
|
5941
6753
|
const paths = resolvePaths(options);
|
|
6754
|
+
const activeGuards = collectActiveWorktreeGuards(paths.scanRoots);
|
|
6755
|
+
const index = mergeWorktreeIndexes(paths.scanRoots);
|
|
5942
6756
|
const finalizedRuns = retention.finalizeStaleRuns ? finalizeStaleRuns().map((f) => ({ runId: f.runId, from: f.from, to: f.to })) : [];
|
|
5943
|
-
const index = buildWorktreeIndex();
|
|
5944
|
-
const scanOpts = {
|
|
5945
|
-
harnessRoot: paths.harnessRoot,
|
|
5946
|
-
worktreesDir: paths.worktreesDir,
|
|
5947
|
-
nodeModulesAgeMs: retention.nodeModulesAgeMs,
|
|
5948
|
-
worktreesAgeMs: retention.worktreesAgeMs,
|
|
5949
|
-
includeOrphans: retention.includeOrphans,
|
|
5950
|
-
runIdFilter: retention.runIdFilter,
|
|
5951
|
-
index,
|
|
5952
|
-
now: paths.now
|
|
5953
|
-
};
|
|
5954
6757
|
const skips = [];
|
|
5955
6758
|
const actions = [];
|
|
5956
|
-
|
|
5957
|
-
|
|
5958
|
-
const
|
|
5959
|
-
|
|
5960
|
-
|
|
5961
|
-
|
|
5962
|
-
continue;
|
|
5963
|
-
}
|
|
5964
|
-
const worktreePath = path33.resolve(candidate.path, "..");
|
|
5965
|
-
const indexed = index.get(worktreePath) ?? null;
|
|
5966
|
-
const guardReason = skipNodeModulesRemoval({
|
|
5967
|
-
indexed,
|
|
5968
|
-
includeOrphans: retention.includeOrphans,
|
|
6759
|
+
const processedPaths = /* @__PURE__ */ new Set();
|
|
6760
|
+
for (const harnessRoot of paths.scanRoots) {
|
|
6761
|
+
const worktreesDir = path39.join(harnessRoot, "worktrees");
|
|
6762
|
+
const scanOpts = {
|
|
6763
|
+
harnessRoot,
|
|
6764
|
+
worktreesDir,
|
|
5969
6765
|
nodeModulesAgeMs: retention.nodeModulesAgeMs,
|
|
5970
|
-
ageMs: candidate.ageMs
|
|
5971
|
-
});
|
|
5972
|
-
if (guardReason) {
|
|
5973
|
-
recordSkip(skips, candidate.path, guardReason);
|
|
5974
|
-
actions.push({ ...candidate, executed: false, skipped: true, skipReason: guardReason });
|
|
5975
|
-
continue;
|
|
5976
|
-
}
|
|
5977
|
-
actions.push(removeNodeModules(candidate, retention.execute));
|
|
5978
|
-
}
|
|
5979
|
-
for (const raw of scanWorktreeCandidates(scanOpts)) {
|
|
5980
|
-
const candidate = attachCandidateBytes(raw, retention.accountBytes);
|
|
5981
|
-
const indexed = index.get(path33.resolve(candidate.path)) ?? null;
|
|
5982
|
-
const orphanSafety = indexed ? null : assessOrphanWorktreeSafety({
|
|
5983
|
-
worktreePath: candidate.path,
|
|
5984
|
-
harnessRoot: paths.harnessRoot,
|
|
5985
|
-
runId: candidate.runId,
|
|
5986
|
-
workerName: candidate.worker,
|
|
5987
|
-
now: paths.now
|
|
5988
|
-
});
|
|
5989
|
-
const guardSkip = skipWorktreeRemoval({
|
|
5990
|
-
indexed,
|
|
5991
|
-
worktreePath: path33.resolve(candidate.path),
|
|
5992
|
-
includeOrphans: retention.includeOrphans,
|
|
5993
6766
|
worktreesAgeMs: retention.worktreesAgeMs,
|
|
5994
|
-
|
|
5995
|
-
|
|
5996
|
-
|
|
5997
|
-
|
|
5998
|
-
|
|
5999
|
-
|
|
6000
|
-
|
|
6001
|
-
|
|
6002
|
-
continue;
|
|
6767
|
+
includeOrphans: retention.includeOrphans,
|
|
6768
|
+
runIdFilter: retention.runIdFilter,
|
|
6769
|
+
index,
|
|
6770
|
+
now: paths.now
|
|
6771
|
+
};
|
|
6772
|
+
for (const raw of scanDependencyCacheCandidates(scanOpts)) {
|
|
6773
|
+
const candidate = attachCandidateBytes(raw, retention.accountBytes);
|
|
6774
|
+
const resolved = path39.resolve(candidate.path);
|
|
6775
|
+
if (processedPaths.has(resolved)) continue;
|
|
6776
|
+
processedPaths.add(resolved);
|
|
6777
|
+
const pathSkip = pathGuardForDependencyCache(candidate, harnessRoot, worktreesDir);
|
|
6778
|
+
if (pathSkip) {
|
|
6779
|
+
recordSkip(skips, candidate.path, pathSkip);
|
|
6780
|
+
actions.push({ ...candidate, executed: false, skipped: true, skipReason: pathSkip });
|
|
6781
|
+
continue;
|
|
6782
|
+
}
|
|
6783
|
+
const worktreePath = worktreePathForCandidate(candidate, worktreesDir);
|
|
6784
|
+
const indexed = index.get(path39.resolve(worktreePath)) ?? null;
|
|
6785
|
+
const guardReason = skipDependencyCacheRemoval({
|
|
6786
|
+
indexed,
|
|
6787
|
+
includeOrphans: true,
|
|
6788
|
+
nodeModulesAgeMs: retention.nodeModulesAgeMs,
|
|
6789
|
+
ageMs: candidate.ageMs,
|
|
6790
|
+
worktreePath,
|
|
6791
|
+
activeWorktreePaths: activeGuards.activeWorktreePaths,
|
|
6792
|
+
diskPressure: retention.diskPressure
|
|
6793
|
+
});
|
|
6794
|
+
if (guardReason) {
|
|
6795
|
+
recordSkip(skips, candidate.path, guardReason);
|
|
6796
|
+
actions.push({ ...candidate, executed: false, skipped: true, skipReason: guardReason });
|
|
6797
|
+
continue;
|
|
6798
|
+
}
|
|
6799
|
+
actions.push(removeDependencyCacheAction(candidate, retention.execute));
|
|
6800
|
+
}
|
|
6801
|
+
for (const raw of scanBuildCacheCandidates(scanOpts)) {
|
|
6802
|
+
const candidate = attachCandidateBytes(raw, retention.accountBytes);
|
|
6803
|
+
const resolved = path39.resolve(candidate.path);
|
|
6804
|
+
if (processedPaths.has(resolved)) continue;
|
|
6805
|
+
processedPaths.add(resolved);
|
|
6806
|
+
const pathSkip = isHarnessBuildCachePath(candidate.path, harnessRoot, worktreesDir);
|
|
6807
|
+
if (pathSkip) {
|
|
6808
|
+
recordSkip(skips, candidate.path, pathSkip);
|
|
6809
|
+
actions.push({ ...candidate, executed: false, skipped: true, skipReason: pathSkip });
|
|
6810
|
+
continue;
|
|
6811
|
+
}
|
|
6812
|
+
const worktreePath = worktreePathForCandidate(candidate, worktreesDir);
|
|
6813
|
+
const indexed = index.get(path39.resolve(worktreePath)) ?? null;
|
|
6814
|
+
const guardReason = skipBuildCacheRemoval({
|
|
6815
|
+
indexed,
|
|
6816
|
+
includeOrphans: true,
|
|
6817
|
+
nodeModulesAgeMs: retention.nodeModulesAgeMs,
|
|
6818
|
+
ageMs: candidate.ageMs,
|
|
6819
|
+
worktreePath,
|
|
6820
|
+
activeWorktreePaths: activeGuards.activeWorktreePaths,
|
|
6821
|
+
diskPressure: retention.diskPressure
|
|
6822
|
+
});
|
|
6823
|
+
if (guardReason) {
|
|
6824
|
+
recordSkip(skips, candidate.path, guardReason);
|
|
6825
|
+
actions.push({ ...candidate, executed: false, skipped: true, skipReason: guardReason });
|
|
6826
|
+
continue;
|
|
6827
|
+
}
|
|
6828
|
+
actions.push(removeBuildCache(candidate, retention.execute));
|
|
6829
|
+
}
|
|
6830
|
+
const worktreeCandidates = [
|
|
6831
|
+
...scanWorktreeCandidates(scanOpts),
|
|
6832
|
+
...scanDuplicateWorktreeCandidates(scanOpts)
|
|
6833
|
+
];
|
|
6834
|
+
const worktreeSeen = /* @__PURE__ */ new Set();
|
|
6835
|
+
for (const raw of worktreeCandidates) {
|
|
6836
|
+
const resolved = path39.resolve(raw.path);
|
|
6837
|
+
if (worktreeSeen.has(resolved)) continue;
|
|
6838
|
+
worktreeSeen.add(resolved);
|
|
6839
|
+
const candidate = attachCandidateBytes({ ...raw, path: resolved }, retention.accountBytes);
|
|
6840
|
+
const indexed = index.get(path39.resolve(candidate.path)) ?? null;
|
|
6841
|
+
const orphanSafety = indexed ? null : assessOrphanWorktreeSafety({
|
|
6842
|
+
worktreePath: candidate.path,
|
|
6843
|
+
harnessRoot,
|
|
6844
|
+
runId: candidate.runId,
|
|
6845
|
+
workerName: candidate.worker,
|
|
6846
|
+
now: paths.now
|
|
6847
|
+
});
|
|
6848
|
+
const guardSkip = skipWorktreeRemoval({
|
|
6849
|
+
indexed,
|
|
6850
|
+
worktreePath: path39.resolve(candidate.path),
|
|
6851
|
+
includeOrphans: retention.includeOrphans,
|
|
6852
|
+
worktreesAgeMs: retention.worktreesAgeMs,
|
|
6853
|
+
ageMs: candidate.ageMs,
|
|
6854
|
+
orphanSafety,
|
|
6855
|
+
worktreeRemovalGuard: options.worktreeRemovalGuard
|
|
6856
|
+
});
|
|
6857
|
+
if (guardSkip) {
|
|
6858
|
+
const { reason: guardReason, detail: guardDetail } = normalizeGuardSkip(guardSkip);
|
|
6859
|
+
recordSkip(skips, candidate.path, guardReason, guardDetail);
|
|
6860
|
+
actions.push({ ...candidate, executed: false, skipped: true, skipReason: guardReason });
|
|
6861
|
+
continue;
|
|
6862
|
+
}
|
|
6863
|
+
actions.push(removeWorktree(candidate, retention.execute));
|
|
6003
6864
|
}
|
|
6004
|
-
actions.push(removeWorktree(candidate, retention.execute));
|
|
6005
6865
|
}
|
|
6006
6866
|
let candidateBytes = 0;
|
|
6007
6867
|
let reclaimableBytes = 0;
|
|
@@ -6022,11 +6882,20 @@ function runHarnessCleanup(options = {}) {
|
|
|
6022
6882
|
const storage = retention.accountBytes ? harnessStorageSnapshot({ harnessRoot: paths.harnessRoot, now: paths.now }) : void 0;
|
|
6023
6883
|
return {
|
|
6024
6884
|
harnessRoot: paths.harnessRoot,
|
|
6885
|
+
scanRoots: paths.scanRoots,
|
|
6025
6886
|
dryRun: !retention.execute,
|
|
6026
6887
|
execute: retention.execute,
|
|
6027
6888
|
nodeModulesAgeMs: retention.nodeModulesAgeMs,
|
|
6028
6889
|
worktreesAgeMs: retention.worktreesAgeMs,
|
|
6029
6890
|
includeOrphans: retention.includeOrphans,
|
|
6891
|
+
diskPressure: retention.diskPressure,
|
|
6892
|
+
diskGate: retention.diskGate ? {
|
|
6893
|
+
ok: retention.diskGate.ok,
|
|
6894
|
+
path: retention.diskGate.path,
|
|
6895
|
+
freeBytes: retention.diskGate.freeBytes,
|
|
6896
|
+
usedPercent: retention.diskGate.usedPercent,
|
|
6897
|
+
reason: retention.diskGate.reason
|
|
6898
|
+
} : void 0,
|
|
6030
6899
|
scannedAt: new Date(paths.now).toISOString(),
|
|
6031
6900
|
finalizedRuns,
|
|
6032
6901
|
actions,
|
|
@@ -6060,8 +6929,8 @@ function isPipelineCleanupEnabled() {
|
|
|
6060
6929
|
|
|
6061
6930
|
// src/installed-package-versions.ts
|
|
6062
6931
|
import { readFile } from "node:fs/promises";
|
|
6063
|
-
import { homedir as
|
|
6064
|
-
import
|
|
6932
|
+
import { homedir as homedir7 } from "node:os";
|
|
6933
|
+
import path40 from "node:path";
|
|
6065
6934
|
var MANAGED_PACKAGES = [
|
|
6066
6935
|
"@kynver-app/runtime",
|
|
6067
6936
|
"@kynver-app/openclaw-agent-os",
|
|
@@ -6075,13 +6944,13 @@ function unique(values) {
|
|
|
6075
6944
|
return [...new Set(values.filter((value) => Boolean(value)))];
|
|
6076
6945
|
}
|
|
6077
6946
|
function moduleRoots() {
|
|
6078
|
-
const home =
|
|
6079
|
-
const openClawPrefix = trim(process.env.KYNVER_OPENCLAW_NPM_ROOT) ?? trim(process.env.OPENCLAW_NPM_ROOT) ??
|
|
6080
|
-
const npmGlobalRoot = trim(process.env.KYNVER_NPM_GLOBAL_ROOT) ?? trim(process.env.KYNVER_NPM_GLOBAL_MODULES_ROOT) ?? (trim(process.env.NPM_CONFIG_PREFIX) ?
|
|
6947
|
+
const home = homedir7();
|
|
6948
|
+
const openClawPrefix = trim(process.env.KYNVER_OPENCLAW_NPM_ROOT) ?? trim(process.env.OPENCLAW_NPM_ROOT) ?? path40.join(home, ".openclaw", "npm");
|
|
6949
|
+
const npmGlobalRoot = trim(process.env.KYNVER_NPM_GLOBAL_ROOT) ?? trim(process.env.KYNVER_NPM_GLOBAL_MODULES_ROOT) ?? (trim(process.env.NPM_CONFIG_PREFIX) ? path40.join(trim(process.env.NPM_CONFIG_PREFIX), "lib", "node_modules") : path40.join(home, ".npm-global", "lib", "node_modules"));
|
|
6081
6950
|
return unique([
|
|
6082
|
-
|
|
6083
|
-
|
|
6084
|
-
npmGlobalRoot.endsWith("node_modules") ? npmGlobalRoot :
|
|
6951
|
+
path40.join(openClawPrefix, "lib", "node_modules"),
|
|
6952
|
+
path40.join(openClawPrefix, "node_modules"),
|
|
6953
|
+
npmGlobalRoot.endsWith("node_modules") ? npmGlobalRoot : path40.join(npmGlobalRoot, "lib", "node_modules")
|
|
6085
6954
|
]);
|
|
6086
6955
|
}
|
|
6087
6956
|
async function readVersion(packageJsonPath) {
|
|
@@ -6097,7 +6966,7 @@ async function collectInstalledPackageVersions(observedAt = (/* @__PURE__ */ new
|
|
|
6097
6966
|
const out = {};
|
|
6098
6967
|
for (const packageName of MANAGED_PACKAGES) {
|
|
6099
6968
|
for (const root of roots) {
|
|
6100
|
-
const packageJsonPath =
|
|
6969
|
+
const packageJsonPath = path40.join(root, packageName, "package.json");
|
|
6101
6970
|
const version = await readVersion(packageJsonPath);
|
|
6102
6971
|
if (!version) continue;
|
|
6103
6972
|
out[packageName] = { version, observedAt, path: packageJsonPath };
|
|
@@ -6113,7 +6982,7 @@ async function completeFinishedWorkers(runId, args) {
|
|
|
6113
6982
|
const outcomes = [];
|
|
6114
6983
|
for (const name of Object.keys(run.workers || {})) {
|
|
6115
6984
|
const worker = readJson(
|
|
6116
|
-
|
|
6985
|
+
path41.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
|
|
6117
6986
|
void 0
|
|
6118
6987
|
);
|
|
6119
6988
|
if (!worker?.taskId || worker.localOnly) continue;
|
|
@@ -6140,18 +7009,38 @@ async function completeFinishedWorkers(runId, args) {
|
|
|
6140
7009
|
}
|
|
6141
7010
|
return outcomes;
|
|
6142
7011
|
}
|
|
6143
|
-
async function postOperatorTick(agentOsId, runId, resourceGate, args) {
|
|
7012
|
+
async function postOperatorTick(agentOsId, runId, resourceGate, args, harnessCleanup) {
|
|
6144
7013
|
const base = resolveBaseUrl(args.baseUrl ? String(args.baseUrl) : void 0);
|
|
6145
7014
|
const secret = await resolveCallbackSecretWithMint(args.secret ? String(args.secret) : void 0, agentOsId, { baseUrl: base });
|
|
6146
7015
|
const url = `${base}/api/agent-os/by-id/${encodeURIComponent(agentOsId)}/operator/tick`;
|
|
6147
7016
|
const packageVersions = await collectInstalledPackageVersions();
|
|
7017
|
+
const activeHarnessWorkers = [];
|
|
7018
|
+
const run = loadRun(runId);
|
|
7019
|
+
for (const name of Object.keys(run.workers || {})) {
|
|
7020
|
+
const worker = readJson(
|
|
7021
|
+
path41.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
|
|
7022
|
+
void 0
|
|
7023
|
+
);
|
|
7024
|
+
if (!worker?.taskId) continue;
|
|
7025
|
+
activeHarnessWorkers.push({
|
|
7026
|
+
runId: run.id,
|
|
7027
|
+
workerName: name,
|
|
7028
|
+
taskId: worker.taskId,
|
|
7029
|
+
pid: worker.pid
|
|
7030
|
+
});
|
|
7031
|
+
}
|
|
6148
7032
|
const res = await postJson(url, secret, {
|
|
6149
7033
|
agentOsId,
|
|
6150
7034
|
runId,
|
|
6151
7035
|
ingestHarness: true,
|
|
6152
7036
|
harnessBoardSnapshot: buildRunBoard(runId),
|
|
6153
7037
|
resourceGate,
|
|
6154
|
-
|
|
7038
|
+
boxResourceSnapshot: buildBoxResourceSnapshotFromGate(resourceGate, { harnessRunId: runId }),
|
|
7039
|
+
packageVersions,
|
|
7040
|
+
...harnessCleanup ? { harnessCleanup } : {},
|
|
7041
|
+
runnerPresence: resolveRunnerPresencePayload({ runId }),
|
|
7042
|
+
activeHarnessWorkers,
|
|
7043
|
+
...harnessCleanup ? { harnessCleanup } : {}
|
|
6155
7044
|
});
|
|
6156
7045
|
return { ok: res.ok, httpStatus: res.status, response: res.response };
|
|
6157
7046
|
}
|
|
@@ -6165,12 +7054,12 @@ async function runPipelineTick(args) {
|
|
|
6165
7054
|
runId,
|
|
6166
7055
|
configuredMaxWorkersOverride: workspacePrefs?.maxConcurrentWorkers
|
|
6167
7056
|
});
|
|
6168
|
-
const
|
|
7057
|
+
const harnessCleanup = isPipelineCleanupEnabled() ? runPipelineHarnessCleanup(runId) : void 0;
|
|
7058
|
+
const operatorTick = await postOperatorTick(agentOsId, runId, resourceGate, args, harnessCleanup);
|
|
6169
7059
|
const completionAckSync = syncCompletionAcknowledgedFromOperatorTick(runId, operatorTick);
|
|
6170
7060
|
const leaseRenewal = await renewActiveTaskLeases(runId, args);
|
|
6171
7061
|
const completedWorkers = await completeFinishedWorkers(runId, args);
|
|
6172
7062
|
const staleReconcile = reconcileStaleWorkers();
|
|
6173
|
-
const harnessCleanup = isPipelineCleanupEnabled() ? runPipelineHarnessCleanup(runId) : void 0;
|
|
6174
7063
|
const planProgressSync = await syncActiveWorkerPlanProgress(runId, args);
|
|
6175
7064
|
const maxStartsAdvice = resolvePipelineMaxStarts(resourceGate, operatorTick);
|
|
6176
7065
|
let maxStarts = maxStartsAdvice.maxStarts;
|
|
@@ -6251,7 +7140,7 @@ async function runDaemon(args) {
|
|
|
6251
7140
|
}
|
|
6252
7141
|
|
|
6253
7142
|
// src/plan-progress.ts
|
|
6254
|
-
import
|
|
7143
|
+
import path42 from "node:path";
|
|
6255
7144
|
|
|
6256
7145
|
// src/bounded-build/constants.ts
|
|
6257
7146
|
var DEFAULT_BUILD_MEM_BUDGET_BYTES = 1536 * 1024 * 1024;
|
|
@@ -6501,7 +7390,8 @@ async function emitPlanProgress(args) {
|
|
|
6501
7390
|
const secret = await resolveCallbackSecretWithMint(args.secret ? String(args.secret) : void 0, agentOsId, { baseUrl: base });
|
|
6502
7391
|
const url = `${base}/api/agent-os/by-id/${encodeURIComponent(agentOsId)}/plans/${encodeURIComponent(planId)}/progress-events`;
|
|
6503
7392
|
const cfg = loadUserConfig();
|
|
6504
|
-
const
|
|
7393
|
+
const workerProvider = resolveConfiguredWorkerProvider(cfg.workerProvider, DEFAULT_WORKER_PROVIDER);
|
|
7394
|
+
const provider = `provider:${workerProvider}`;
|
|
6505
7395
|
const explicitProposed = args.proposed === true || args.proposed === "true" ? true : args.proposed === false || args.proposed === "false" ? false : void 0;
|
|
6506
7396
|
const proposed = explicitProposed ?? (status !== "done" && (roleLane === "implementer" || roleLane === "repair_implementer"));
|
|
6507
7397
|
const body = {
|
|
@@ -6537,7 +7427,7 @@ async function emitPlanProgress(args) {
|
|
|
6537
7427
|
}
|
|
6538
7428
|
function verifyPlanLocal(args) {
|
|
6539
7429
|
const worktree = required(args.worktree ? String(args.worktree) : void 0, "worktree");
|
|
6540
|
-
const cwd =
|
|
7430
|
+
const cwd = path42.resolve(worktree);
|
|
6541
7431
|
const summary = runHarnessVerifyCommands(cwd);
|
|
6542
7432
|
const emitJson = args.json === true || args.json === "true";
|
|
6543
7433
|
const payload = { passed: summary.passed, worktree: cwd, steps: summary.steps };
|
|
@@ -6586,9 +7476,9 @@ async function verifyPlan(args) {
|
|
|
6586
7476
|
}
|
|
6587
7477
|
|
|
6588
7478
|
// src/harness-verify-cli.ts
|
|
6589
|
-
import
|
|
7479
|
+
import path43 from "node:path";
|
|
6590
7480
|
function runHarnessVerifyCli(args) {
|
|
6591
|
-
const cwd =
|
|
7481
|
+
const cwd = path43.resolve(required(args.worktree ? String(args.worktree) : void 0, "worktree"));
|
|
6592
7482
|
const emitJson = args.json === true || args.json === "true" || args.emitJson === true || args.emitJson === "true";
|
|
6593
7483
|
const commands = [];
|
|
6594
7484
|
const rawCmd = args.command;
|
|
@@ -6632,7 +7522,7 @@ function runHarnessVerifyCli(args) {
|
|
|
6632
7522
|
}
|
|
6633
7523
|
|
|
6634
7524
|
// src/plan-persist-cli.ts
|
|
6635
|
-
import { readFileSync as
|
|
7525
|
+
import { readFileSync as readFileSync10 } from "node:fs";
|
|
6636
7526
|
var OPERATIONS = ["create", "add_version", "update_metadata"];
|
|
6637
7527
|
var FAILURE_KINDS = [
|
|
6638
7528
|
"approval_guard",
|
|
@@ -6644,7 +7534,7 @@ var FAILURE_KINDS = [
|
|
|
6644
7534
|
function readBodyArg(args) {
|
|
6645
7535
|
const bodyFile = args.bodyFile ? String(args.bodyFile) : void 0;
|
|
6646
7536
|
if (bodyFile) {
|
|
6647
|
-
return { body:
|
|
7537
|
+
return { body: readFileSync10(bodyFile, "utf8"), bodyPathHint: bodyFile };
|
|
6648
7538
|
}
|
|
6649
7539
|
const inline = args.body ? String(args.body) : void 0;
|
|
6650
7540
|
if (inline) return { body: inline };
|
|
@@ -6733,8 +7623,62 @@ function runCleanupCli(args) {
|
|
|
6733
7623
|
}
|
|
6734
7624
|
}
|
|
6735
7625
|
|
|
7626
|
+
// src/harness-notice/harness-notice.parse.ts
|
|
7627
|
+
var MAX_DIAGNOSTIC_CHARS = 2400;
|
|
7628
|
+
function diagnosticJson(value, maxChars = MAX_DIAGNOSTIC_CHARS) {
|
|
7629
|
+
if (value === void 0 || value === null) return void 0;
|
|
7630
|
+
const raw = typeof value === "string" ? value : JSON.stringify(value, null, 2);
|
|
7631
|
+
const trimmed = raw.trim();
|
|
7632
|
+
if (!trimmed) return void 0;
|
|
7633
|
+
if (trimmed.length <= maxChars) return trimmed;
|
|
7634
|
+
return `${trimmed.slice(0, maxChars - 1).trimEnd()}\u2026`;
|
|
7635
|
+
}
|
|
7636
|
+
|
|
7637
|
+
// src/harness-notice/harness-notice.monitor-tick.ts
|
|
7638
|
+
function formatMonitorTickNotice(tick) {
|
|
7639
|
+
const lines = [];
|
|
7640
|
+
const monitorId = typeof tick.monitorId === "string" ? tick.monitorId : void 0;
|
|
7641
|
+
lines.push(
|
|
7642
|
+
monitorId ? `Harness monitor tick \xB7 ${tick.runId} (${monitorId})` : `Harness monitor tick \xB7 ${tick.runId}`
|
|
7643
|
+
);
|
|
7644
|
+
if (!tick.workers.length) {
|
|
7645
|
+
lines.push("No workers in scope for this poll.");
|
|
7646
|
+
return { primary: lines.join("\n"), diagnostic: diagnosticJson(tick) };
|
|
7647
|
+
}
|
|
7648
|
+
for (const view of tick.workers) {
|
|
7649
|
+
const auto = view.autoComplete.eligible ? "eligible for auto-complete" : "not auto-completing";
|
|
7650
|
+
const blockers = view.autoComplete.blockers.length > 0 ? ` (${view.autoComplete.blockers.slice(0, 2).join("; ")})` : "";
|
|
7651
|
+
lines.push(
|
|
7652
|
+
`\u2022 ${view.worker}: ${view.workerStatus}, ${view.health}${view.healthReason ? ` \u2014 ${view.healthReason}` : ""}; ${auto}${blockers}`
|
|
7653
|
+
);
|
|
7654
|
+
if (view.taskStatus) {
|
|
7655
|
+
lines.push(` Board task: ${view.taskStatus}${view.leaseOwner ? ` (lease: ${view.leaseOwner})` : ""}`);
|
|
7656
|
+
}
|
|
7657
|
+
}
|
|
7658
|
+
const completed = tick.autoCompleted?.filter((a) => a.outcome === "completed" && a.ok) ?? [];
|
|
7659
|
+
const blocked = tick.autoCompleted?.filter((a) => !a.ok && a.outcome !== "skipped") ?? [];
|
|
7660
|
+
if (completed.length) {
|
|
7661
|
+
lines.push(
|
|
7662
|
+
`Auto-completed: ${completed.map((c) => c.worker).join(", ")} \u2014 AgentOS completion should be posted.`
|
|
7663
|
+
);
|
|
7664
|
+
}
|
|
7665
|
+
if (blocked.length) {
|
|
7666
|
+
lines.push(
|
|
7667
|
+
`Auto-complete blocked: ${blocked.map((c) => `${c.worker}${c.reason ? ` (${c.reason})` : ""}`).join("; ")}`
|
|
7668
|
+
);
|
|
7669
|
+
}
|
|
7670
|
+
if (tick.leaseRenewal?.failed?.length) {
|
|
7671
|
+
lines.push(`Lease renew failed for: ${tick.leaseRenewal.failed.map((f) => f.worker).join(", ")}`);
|
|
7672
|
+
}
|
|
7673
|
+
const allDone = tick.workers.length > 0 && tick.workers.every((w) => w.autoComplete.terminalVerified) && (tick.autoCompleted?.every((a) => a.ok || a.outcome === "skipped") ?? true);
|
|
7674
|
+
lines.push(
|
|
7675
|
+
allDone ? "Next: monitor loop should stop \u2014 all workers terminal and handled." : "Next: monitor will poll again until workers are terminal-verified or max time elapses."
|
|
7676
|
+
);
|
|
7677
|
+
return { primary: lines.join("\n"), diagnostic: diagnosticJson(tick) };
|
|
7678
|
+
}
|
|
7679
|
+
|
|
6736
7680
|
// src/monitor/monitor.service.ts
|
|
6737
|
-
import
|
|
7681
|
+
import path45 from "node:path";
|
|
6738
7682
|
|
|
6739
7683
|
// src/monitor/monitor.classify.ts
|
|
6740
7684
|
function expectedLeaseOwner(runId) {
|
|
@@ -6790,11 +7734,11 @@ function classifyWorkerHealth(input) {
|
|
|
6790
7734
|
}
|
|
6791
7735
|
|
|
6792
7736
|
// src/monitor/monitor.store.ts
|
|
6793
|
-
import { existsSync as
|
|
6794
|
-
import
|
|
7737
|
+
import { existsSync as existsSync26, mkdirSync as mkdirSync6, readdirSync as readdirSync9, unlinkSync as unlinkSync2 } from "node:fs";
|
|
7738
|
+
import path44 from "node:path";
|
|
6795
7739
|
function monitorsDir() {
|
|
6796
7740
|
const { harnessRoot } = getHarnessPaths();
|
|
6797
|
-
const dir =
|
|
7741
|
+
const dir = path44.join(harnessRoot, "monitors");
|
|
6798
7742
|
mkdirSync6(dir, { recursive: true });
|
|
6799
7743
|
return dir;
|
|
6800
7744
|
}
|
|
@@ -6802,7 +7746,7 @@ function monitorIdFor(runId, workerName) {
|
|
|
6802
7746
|
return workerName ? `${safeSlug(runId)}--${safeSlug(workerName)}` : safeSlug(runId);
|
|
6803
7747
|
}
|
|
6804
7748
|
function monitorPath(monitorId) {
|
|
6805
|
-
return
|
|
7749
|
+
return path44.join(monitorsDir(), `${monitorId}.json`);
|
|
6806
7750
|
}
|
|
6807
7751
|
function loadMonitorSession(monitorId) {
|
|
6808
7752
|
return readJson(monitorPath(monitorId), void 0);
|
|
@@ -6812,18 +7756,18 @@ function saveMonitorSession(session) {
|
|
|
6812
7756
|
}
|
|
6813
7757
|
function deleteMonitorSession(monitorId) {
|
|
6814
7758
|
const file = monitorPath(monitorId);
|
|
6815
|
-
if (!
|
|
7759
|
+
if (!existsSync26(file)) return false;
|
|
6816
7760
|
unlinkSync2(file);
|
|
6817
7761
|
return true;
|
|
6818
7762
|
}
|
|
6819
7763
|
function listMonitorSessions() {
|
|
6820
7764
|
const dir = monitorsDir();
|
|
6821
|
-
if (!
|
|
7765
|
+
if (!existsSync26(dir)) return [];
|
|
6822
7766
|
const entries = [];
|
|
6823
|
-
for (const name of
|
|
7767
|
+
for (const name of readdirSync9(dir)) {
|
|
6824
7768
|
if (!name.endsWith(".json")) continue;
|
|
6825
7769
|
const session = readJson(
|
|
6826
|
-
|
|
7770
|
+
path44.join(dir, name),
|
|
6827
7771
|
void 0
|
|
6828
7772
|
);
|
|
6829
7773
|
if (!session?.monitorId) continue;
|
|
@@ -6914,7 +7858,7 @@ async function fetchTaskLeasesForWorkers(input) {
|
|
|
6914
7858
|
// src/monitor/monitor.service.ts
|
|
6915
7859
|
function workerRecord2(runId, name) {
|
|
6916
7860
|
return readJson(
|
|
6917
|
-
|
|
7861
|
+
path45.join(runDirectory(runId), "workers", safeSlug(name), "worker.json"),
|
|
6918
7862
|
void 0
|
|
6919
7863
|
);
|
|
6920
7864
|
}
|
|
@@ -7099,7 +8043,11 @@ async function runMonitorLoop(args) {
|
|
|
7099
8043
|
autoComplete: args.autoComplete ?? true,
|
|
7100
8044
|
renewLeases: args.renewLeases ?? true
|
|
7101
8045
|
});
|
|
7102
|
-
|
|
8046
|
+
const notice = formatMonitorTickNotice({ monitorId, phase: "tick", ...tick });
|
|
8047
|
+
console.log(notice.primary);
|
|
8048
|
+
if (notice.diagnostic) {
|
|
8049
|
+
console.error(`[monitor diagnostic] ${notice.diagnostic}`);
|
|
8050
|
+
}
|
|
7103
8051
|
const allTerminal = tick.workers.length > 0 && tick.workers.every(
|
|
7104
8052
|
(w) => w.autoComplete.terminalVerified && (w.autoComplete.eligible || w.autoComplete.blockers.some((b) => b.includes("already acknowledged")))
|
|
7105
8053
|
);
|
|
@@ -7116,18 +8064,18 @@ async function runMonitorLoop(args) {
|
|
|
7116
8064
|
|
|
7117
8065
|
// src/monitor/monitor-spawn.ts
|
|
7118
8066
|
import { spawn as spawn4 } from "node:child_process";
|
|
7119
|
-
import { closeSync as closeSync4, existsSync as
|
|
7120
|
-
import
|
|
8067
|
+
import { closeSync as closeSync4, existsSync as existsSync27, openSync as openSync4 } from "node:fs";
|
|
8068
|
+
import path46 from "node:path";
|
|
7121
8069
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
7122
8070
|
function resolveDefaultCliPath2() {
|
|
7123
|
-
return
|
|
8071
|
+
return path46.join(fileURLToPath3(new URL(".", import.meta.url)), "cli.js");
|
|
7124
8072
|
}
|
|
7125
8073
|
function spawnMonitorSidecar(opts) {
|
|
7126
8074
|
const cliPath = opts.cliPath ?? resolveDefaultCliPath2();
|
|
7127
|
-
if (!
|
|
8075
|
+
if (!existsSync27(cliPath)) return void 0;
|
|
7128
8076
|
const monitorId = monitorIdFor(opts.runId, opts.workerName);
|
|
7129
8077
|
const { harnessRoot } = getHarnessPaths();
|
|
7130
|
-
const logPath =
|
|
8078
|
+
const logPath = path46.join(harnessRoot, "monitors", `${monitorId}.log`);
|
|
7131
8079
|
let logFd;
|
|
7132
8080
|
try {
|
|
7133
8081
|
logFd = openSync4(logPath, "a");
|
|
@@ -7247,13 +8195,13 @@ async function monitorTickCli(args) {
|
|
|
7247
8195
|
}
|
|
7248
8196
|
|
|
7249
8197
|
// src/package-version.ts
|
|
7250
|
-
import { existsSync as
|
|
8198
|
+
import { existsSync as existsSync28, readFileSync as readFileSync11 } from "node:fs";
|
|
7251
8199
|
import { dirname, join } from "node:path";
|
|
7252
8200
|
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
7253
8201
|
function resolvePackageRoot(moduleUrl) {
|
|
7254
8202
|
let dir = dirname(fileURLToPath4(moduleUrl));
|
|
7255
8203
|
for (let depth = 0; depth < 6; depth += 1) {
|
|
7256
|
-
if (
|
|
8204
|
+
if (existsSync28(join(dir, "package.json"))) return dir;
|
|
7257
8205
|
const parent = dirname(dir);
|
|
7258
8206
|
if (parent === dir) break;
|
|
7259
8207
|
dir = parent;
|
|
@@ -7262,7 +8210,7 @@ function resolvePackageRoot(moduleUrl) {
|
|
|
7262
8210
|
}
|
|
7263
8211
|
function readOwnPackageVersion(moduleUrl = import.meta.url) {
|
|
7264
8212
|
const pkgPath = join(resolvePackageRoot(moduleUrl), "package.json");
|
|
7265
|
-
const pkg = JSON.parse(
|
|
8213
|
+
const pkg = JSON.parse(readFileSync11(pkgPath, "utf8"));
|
|
7266
8214
|
if (typeof pkg.version !== "string" || !pkg.version.trim()) {
|
|
7267
8215
|
throw new Error(`Missing package.json version at ${pkgPath}`);
|
|
7268
8216
|
}
|
|
@@ -7283,12 +8231,12 @@ function handleCliVersionFlag(argv, moduleUrl = import.meta.url, binName) {
|
|
|
7283
8231
|
}
|
|
7284
8232
|
|
|
7285
8233
|
// src/doctor/runtime-takeover.ts
|
|
7286
|
-
import
|
|
8234
|
+
import path48 from "node:path";
|
|
7287
8235
|
|
|
7288
8236
|
// src/doctor/runtime-takeover.probes.ts
|
|
7289
|
-
import { accessSync, constants, existsSync as
|
|
7290
|
-
import { homedir as
|
|
7291
|
-
import
|
|
8237
|
+
import { accessSync, constants, existsSync as existsSync29, readFileSync as readFileSync12 } from "node:fs";
|
|
8238
|
+
import { homedir as homedir8 } from "node:os";
|
|
8239
|
+
import path47 from "node:path";
|
|
7292
8240
|
import { spawnSync as spawnSync6 } from "node:child_process";
|
|
7293
8241
|
function captureCommand(bin, args) {
|
|
7294
8242
|
try {
|
|
@@ -7317,7 +8265,7 @@ function tokenPrefix(token) {
|
|
|
7317
8265
|
return trimmed.length <= 12 ? `${trimmed}\u2026` : `${trimmed.slice(0, 12)}\u2026`;
|
|
7318
8266
|
}
|
|
7319
8267
|
function isWritable(target) {
|
|
7320
|
-
if (!
|
|
8268
|
+
if (!existsSync29(target)) return false;
|
|
7321
8269
|
try {
|
|
7322
8270
|
accessSync(target, constants.W_OK);
|
|
7323
8271
|
return true;
|
|
@@ -7330,15 +8278,15 @@ var defaultRuntimeTakeoverProbes = {
|
|
|
7330
8278
|
commandOnPath: (bin) => captureCommand(process.platform === "win32" ? "where" : "which", [bin]),
|
|
7331
8279
|
kynverVersion: (bin) => captureCommand(bin, ["--version"]),
|
|
7332
8280
|
loadConfig: () => loadUserConfig(),
|
|
7333
|
-
configFilePath: () =>
|
|
7334
|
-
credentialsFilePath: () =>
|
|
8281
|
+
configFilePath: () => path47.join(homedir8(), ".kynver", "config.json"),
|
|
8282
|
+
credentialsFilePath: () => path47.join(homedir8(), ".kynver", "credentials"),
|
|
7335
8283
|
readCredentials: () => {
|
|
7336
|
-
const credPath =
|
|
7337
|
-
if (!
|
|
8284
|
+
const credPath = path47.join(homedir8(), ".kynver", "credentials");
|
|
8285
|
+
if (!existsSync29(credPath)) {
|
|
7338
8286
|
return { hasApiKey: false };
|
|
7339
8287
|
}
|
|
7340
8288
|
try {
|
|
7341
|
-
const parsed = JSON.parse(
|
|
8289
|
+
const parsed = JSON.parse(readFileSync12(credPath, "utf8"));
|
|
7342
8290
|
return {
|
|
7343
8291
|
hasApiKey: Boolean(parsed.apiKey?.trim()),
|
|
7344
8292
|
runnerTokenPrefix: tokenPrefix(parsed.runnerToken),
|
|
@@ -7365,8 +8313,8 @@ var defaultRuntimeTakeoverProbes = {
|
|
|
7365
8313
|
})()
|
|
7366
8314
|
}),
|
|
7367
8315
|
harnessRoot: () => resolveHarnessRoot(),
|
|
7368
|
-
legacyOpenclawHarnessRoot: () =>
|
|
7369
|
-
pathExists: (target) =>
|
|
8316
|
+
legacyOpenclawHarnessRoot: () => path47.join(homedir8(), ".openclaw", "harness"),
|
|
8317
|
+
pathExists: (target) => existsSync29(target),
|
|
7370
8318
|
pathWritable: (target) => isWritable(target),
|
|
7371
8319
|
vercelVersion: () => captureCommand("vercel", ["--version"]),
|
|
7372
8320
|
vercelWhoami: () => captureCommand("vercel", ["whoami"])
|
|
@@ -7414,7 +8362,7 @@ function assessRuntimeTakeoverScheduler(env, ctx) {
|
|
|
7414
8362
|
label: "Scheduler provider (runtime daemon vs OpenClaw cron)",
|
|
7415
8363
|
status: "warn",
|
|
7416
8364
|
summary: `OpenClaw local cron still active (${parts.join("; ")})`,
|
|
7417
|
-
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;
|
|
8365
|
+
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; after Vercel env is updated run `kynver scheduler attest-cutover`.",
|
|
7418
8366
|
details: schedulerDetails
|
|
7419
8367
|
});
|
|
7420
8368
|
}
|
|
@@ -7694,8 +8642,8 @@ function assessVercelCli(probes) {
|
|
|
7694
8642
|
}
|
|
7695
8643
|
function assessHarnessDirs(probes) {
|
|
7696
8644
|
const harnessRoot = probes.harnessRoot();
|
|
7697
|
-
const runsDir =
|
|
7698
|
-
const worktreesDir =
|
|
8645
|
+
const runsDir = path48.join(harnessRoot, "runs");
|
|
8646
|
+
const worktreesDir = path48.join(harnessRoot, "worktrees");
|
|
7699
8647
|
const displayHarnessRoot = redactHomePath(harnessRoot);
|
|
7700
8648
|
const displayRunsDir = redactHomePath(runsDir);
|
|
7701
8649
|
const displayWorktreesDir = redactHomePath(worktreesDir);
|
|
@@ -7883,6 +8831,131 @@ async function runCommandCenterContractCli(args) {
|
|
|
7883
8831
|
console.log(JSON.stringify(res.response, null, 2));
|
|
7884
8832
|
}
|
|
7885
8833
|
|
|
8834
|
+
// src/scheduler-cutover.ts
|
|
8835
|
+
var DEPLOYMENT_SCHEDULER_CUTOVER_STEPS = [
|
|
8836
|
+
"Vercel/hosted: set KYNVER_SCHEDULER_PROVIDER=qstash",
|
|
8837
|
+
"Vercel/hosted: ensure QSTASH_TOKEN (and QStash signing keys) are configured",
|
|
8838
|
+
"Vercel/hosted: unset OPENCLAW_CRON_STORE_PATH if present"
|
|
8839
|
+
];
|
|
8840
|
+
var RUNNER_SCHEDULER_CUTOVER_STEPS = [
|
|
8841
|
+
"User runner: unset KYNVER_SCHEDULER_PROVIDER (scheduling is deployment-owned)",
|
|
8842
|
+
"User runner: unset OPENCLAW_CRON_STORE_PATH, OPENCLAW_CRON_SECRET, OPENCLAW_CRON_FIRE_BASE_URL",
|
|
8843
|
+
'User runner: after deployment cutover, run `kynver scheduler attest-cutover` (or set deploymentSchedulerProvider to "qstash" in ~/.kynver/config.json)',
|
|
8844
|
+
"Verify: kynver doctor runtime-takeover \u2014 hotspot_openclaw_scheduler should pass"
|
|
8845
|
+
];
|
|
8846
|
+
function readSchedulerCutoverEnv(env = process.env) {
|
|
8847
|
+
return {
|
|
8848
|
+
kynverSchedulerProvider: env.KYNVER_SCHEDULER_PROVIDER?.trim() || null,
|
|
8849
|
+
openclawCronStorePath: env.OPENCLAW_CRON_STORE_PATH?.trim() || null,
|
|
8850
|
+
openclawCronSecret: Boolean(env.OPENCLAW_CRON_SECRET?.trim()),
|
|
8851
|
+
openclawCronFireBaseUrl: env.OPENCLAW_CRON_FIRE_BASE_URL?.trim() || null
|
|
8852
|
+
};
|
|
8853
|
+
}
|
|
8854
|
+
function assessSchedulerCutover(config, env = readSchedulerCutoverEnv()) {
|
|
8855
|
+
const blockers = [];
|
|
8856
|
+
if (env.kynverSchedulerProvider === "openclaw-cron") {
|
|
8857
|
+
blockers.push("Runner still has KYNVER_SCHEDULER_PROVIDER=openclaw-cron");
|
|
8858
|
+
}
|
|
8859
|
+
if (env.openclawCronStorePath) {
|
|
8860
|
+
blockers.push("Runner still has OPENCLAW_CRON_STORE_PATH");
|
|
8861
|
+
}
|
|
8862
|
+
if (config.deploymentSchedulerProvider === "openclaw-cron") {
|
|
8863
|
+
blockers.push("~/.kynver/config.json deploymentSchedulerProvider is still openclaw-cron");
|
|
8864
|
+
}
|
|
8865
|
+
return {
|
|
8866
|
+
ok: blockers.length === 0,
|
|
8867
|
+
blockers,
|
|
8868
|
+
runnerEnv: env,
|
|
8869
|
+
deploymentSchedulerProvider: config.deploymentSchedulerProvider ?? null,
|
|
8870
|
+
deploymentSteps: [...DEPLOYMENT_SCHEDULER_CUTOVER_STEPS],
|
|
8871
|
+
runnerSteps: [...RUNNER_SCHEDULER_CUTOVER_STEPS]
|
|
8872
|
+
};
|
|
8873
|
+
}
|
|
8874
|
+
function applySchedulerCutoverAttestation(config) {
|
|
8875
|
+
return {
|
|
8876
|
+
...config,
|
|
8877
|
+
deploymentSchedulerProvider: "qstash"
|
|
8878
|
+
};
|
|
8879
|
+
}
|
|
8880
|
+
|
|
8881
|
+
// src/scheduler-cutover-cli.ts
|
|
8882
|
+
import path49 from "node:path";
|
|
8883
|
+
import { homedir as homedir9 } from "node:os";
|
|
8884
|
+
var CONFIG_FILE2 = path49.join(homedir9(), ".kynver", "config.json");
|
|
8885
|
+
function runSchedulerCutoverCheckCli(json = false) {
|
|
8886
|
+
const config = loadUserConfig();
|
|
8887
|
+
const report = assessSchedulerCutover(config);
|
|
8888
|
+
const payload = {
|
|
8889
|
+
...report,
|
|
8890
|
+
configPath: displayUserPath(CONFIG_FILE2),
|
|
8891
|
+
configAttestationExample: { deploymentSchedulerProvider: "qstash" }
|
|
8892
|
+
};
|
|
8893
|
+
if (json) {
|
|
8894
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
8895
|
+
if (!report.ok) process.exitCode = 1;
|
|
8896
|
+
return;
|
|
8897
|
+
}
|
|
8898
|
+
console.log("AgentOS scheduler provider cutover checklist\n");
|
|
8899
|
+
console.log("Deployment (Vercel):");
|
|
8900
|
+
for (const step of DEPLOYMENT_SCHEDULER_CUTOVER_STEPS) console.log(` - ${step}`);
|
|
8901
|
+
console.log("\nUser runner:");
|
|
8902
|
+
for (const step of RUNNER_SCHEDULER_CUTOVER_STEPS) console.log(` - ${step}`);
|
|
8903
|
+
console.log("\nThis host:");
|
|
8904
|
+
console.log(` config: ${payload.configPath}`);
|
|
8905
|
+
console.log(
|
|
8906
|
+
` deploymentSchedulerProvider: ${report.deploymentSchedulerProvider ?? "(unset)"}`
|
|
8907
|
+
);
|
|
8908
|
+
console.log(
|
|
8909
|
+
` KYNVER_SCHEDULER_PROVIDER: ${report.runnerEnv.kynverSchedulerProvider ?? "(unset)"}`
|
|
8910
|
+
);
|
|
8911
|
+
console.log(
|
|
8912
|
+
` OPENCLAW_CRON_STORE_PATH: ${report.runnerEnv.openclawCronStorePath ?? "(unset)"}`
|
|
8913
|
+
);
|
|
8914
|
+
if (report.blockers.length) {
|
|
8915
|
+
console.log("\nBlockers:");
|
|
8916
|
+
for (const b of report.blockers) console.log(` ! ${b}`);
|
|
8917
|
+
process.exitCode = 1;
|
|
8918
|
+
return;
|
|
8919
|
+
}
|
|
8920
|
+
console.log("\nNo local blockers detected on this runner.");
|
|
8921
|
+
}
|
|
8922
|
+
function runSchedulerAttestCutoverCli(json = false) {
|
|
8923
|
+
const existing = loadUserConfig();
|
|
8924
|
+
const report = assessSchedulerCutover(existing);
|
|
8925
|
+
if (!report.ok) {
|
|
8926
|
+
const payload2 = {
|
|
8927
|
+
ok: false,
|
|
8928
|
+
attested: false,
|
|
8929
|
+
blockers: report.blockers,
|
|
8930
|
+
remediation: "Clear local OpenClaw scheduler blockers before attesting qstash cutover."
|
|
8931
|
+
};
|
|
8932
|
+
if (json) {
|
|
8933
|
+
console.log(JSON.stringify(payload2, null, 2));
|
|
8934
|
+
} else {
|
|
8935
|
+
console.error("Cannot attest scheduler cutover \u2014 local blockers remain:");
|
|
8936
|
+
for (const b of report.blockers) console.error(` ! ${b}`);
|
|
8937
|
+
}
|
|
8938
|
+
process.exitCode = 1;
|
|
8939
|
+
return;
|
|
8940
|
+
}
|
|
8941
|
+
const next = applySchedulerCutoverAttestation(existing);
|
|
8942
|
+
saveUserConfig(next);
|
|
8943
|
+
const payload = {
|
|
8944
|
+
ok: true,
|
|
8945
|
+
attested: true,
|
|
8946
|
+
configPath: displayUserPath(CONFIG_FILE2),
|
|
8947
|
+
deploymentSchedulerProvider: "qstash",
|
|
8948
|
+
config: presentUserConfig(next),
|
|
8949
|
+
note: "Recorded deploymentSchedulerProvider=qstash in ~/.kynver/config.json. Confirm Vercel has KYNVER_SCHEDULER_PROVIDER=qstash and QSTASH_TOKEN before relying on hosted schedules."
|
|
8950
|
+
};
|
|
8951
|
+
if (json) {
|
|
8952
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
8953
|
+
return;
|
|
8954
|
+
}
|
|
8955
|
+
console.log(payload.note);
|
|
8956
|
+
console.log(` config: ${payload.configPath}`);
|
|
8957
|
+
}
|
|
8958
|
+
|
|
7886
8959
|
// src/cli.ts
|
|
7887
8960
|
function isHelpFlag(arg) {
|
|
7888
8961
|
return arg === "help" || arg === "--help" || arg === "-h";
|
|
@@ -7930,6 +9003,8 @@ function usage(code = 0) {
|
|
|
7930
9003
|
" kynver monitor auto-complete --run RUN_ID --name worker [--agent-os-id AOS_ID] [--base-url URL] [--secret SECRET]",
|
|
7931
9004
|
" kynver monitor run-loop --run RUN_ID --monitor-id ID [--name worker] [--agent-os-id AOS_ID] [--poll-ms MS] [--auto-complete] [--renew-leases]",
|
|
7932
9005
|
" kynver doctor runtime-takeover",
|
|
9006
|
+
" kynver scheduler cutover-check [--json]",
|
|
9007
|
+
" kynver scheduler attest-cutover [--json]",
|
|
7933
9008
|
" kynver board contract [--agent-os-id ID] [--base-url URL] [--since ISO] [--limit N]"
|
|
7934
9009
|
].join("\n")
|
|
7935
9010
|
);
|
|
@@ -7941,7 +9016,7 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
7941
9016
|
const scope = argv.shift();
|
|
7942
9017
|
let action;
|
|
7943
9018
|
let rest;
|
|
7944
|
-
if (scope === "run" || scope === "worker" || scope === "plan" || scope === "runner" || scope === "harness" || scope === "monitor" || scope === "doctor" || scope === "board") {
|
|
9019
|
+
if (scope === "run" || scope === "worker" || scope === "plan" || scope === "runner" || scope === "harness" || scope === "monitor" || scope === "doctor" || scope === "scheduler" || scope === "board") {
|
|
7945
9020
|
action = argv.shift();
|
|
7946
9021
|
rest = argv;
|
|
7947
9022
|
} else {
|
|
@@ -7968,6 +9043,12 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
7968
9043
|
}
|
|
7969
9044
|
if (scope === "cleanup") return runCleanupCli(args);
|
|
7970
9045
|
if (scope === "doctor" && action === "runtime-takeover") return runRuntimeTakeoverDoctorCli();
|
|
9046
|
+
if (scope === "scheduler" && action === "cutover-check") {
|
|
9047
|
+
return runSchedulerCutoverCheckCli(args.json === true);
|
|
9048
|
+
}
|
|
9049
|
+
if (scope === "scheduler" && action === "attest-cutover") {
|
|
9050
|
+
return runSchedulerAttestCutoverCli(args.json === true);
|
|
9051
|
+
}
|
|
7971
9052
|
if (scope === "board" && action === "contract") {
|
|
7972
9053
|
return void await runCommandCenterContractCli(args);
|
|
7973
9054
|
}
|