@kynver-app/runtime 0.1.24 → 0.1.27
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +471 -13
- package/dist/cli.js.map +4 -4
- package/dist/index.js +471 -13
- package/dist/index.js.map +4 -4
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -422,12 +422,12 @@ var DEFAULT_CRITICAL_FREE_BYTES = 15 * 1024 * 1024 * 1024;
|
|
|
422
422
|
var DEFAULT_MAX_USED_PERCENT = 80;
|
|
423
423
|
var DEFAULT_HARD_MAX_USED_PERCENT = 90;
|
|
424
424
|
function observeRunnerDiskGate(input = {}) {
|
|
425
|
-
const
|
|
425
|
+
const path23 = input.diskPath?.trim() || "/";
|
|
426
426
|
const warnBelowBytes = input.diskFreeWarnBytes ?? DEFAULT_WARN_FREE_BYTES;
|
|
427
427
|
const criticalBelowBytes = input.diskFreeCriticalBytes ?? DEFAULT_CRITICAL_FREE_BYTES;
|
|
428
428
|
const maxUsedPercent = input.diskMaxUsedPercent ?? DEFAULT_MAX_USED_PERCENT;
|
|
429
429
|
const hardMaxUsedPercent = input.diskHardMaxUsedPercent ?? DEFAULT_HARD_MAX_USED_PERCENT;
|
|
430
|
-
const stats = statfsSync(
|
|
430
|
+
const stats = statfsSync(path23);
|
|
431
431
|
const freeBytes = Number(stats.bavail) * Number(stats.bsize);
|
|
432
432
|
const totalBytes = Number(stats.blocks) * Number(stats.bsize);
|
|
433
433
|
const usedPercent = totalBytes > 0 ? (totalBytes - freeBytes) / totalBytes * 100 : 100;
|
|
@@ -447,7 +447,7 @@ function observeRunnerDiskGate(input = {}) {
|
|
|
447
447
|
}
|
|
448
448
|
return {
|
|
449
449
|
ok,
|
|
450
|
-
path:
|
|
450
|
+
path: path23,
|
|
451
451
|
freeBytes,
|
|
452
452
|
totalBytes,
|
|
453
453
|
usedPercent,
|
|
@@ -1700,7 +1700,7 @@ async function tryCompleteWorker(args) {
|
|
|
1700
1700
|
let secret = await resolveCallbackSecretWithMint(explicitSecret, agentOsId, { baseUrl: base });
|
|
1701
1701
|
const url = `${base}/api/agent-os/by-id/${encodeURIComponent(agentOsId)}/harness/completion`;
|
|
1702
1702
|
const body = {
|
|
1703
|
-
source: "
|
|
1703
|
+
source: "kynver-harness",
|
|
1704
1704
|
agentOsId,
|
|
1705
1705
|
runId: worker.runId,
|
|
1706
1706
|
workerName: worker.name,
|
|
@@ -2190,7 +2190,7 @@ function spawnWorkerProcess(run, opts) {
|
|
|
2190
2190
|
}
|
|
2191
2191
|
return worker;
|
|
2192
2192
|
}
|
|
2193
|
-
function startWorker(args) {
|
|
2193
|
+
async function startWorker(args) {
|
|
2194
2194
|
const run = loadRun(String(args.run));
|
|
2195
2195
|
const name = typeof args.name === "string" ? args.name.trim() : "";
|
|
2196
2196
|
if (!name) {
|
|
@@ -2202,8 +2202,10 @@ function startWorker(args) {
|
|
|
2202
2202
|
console.error("missing --task or --task-file");
|
|
2203
2203
|
process.exit(1);
|
|
2204
2204
|
}
|
|
2205
|
+
const wait = args.wait === true || args.wait === "true";
|
|
2206
|
+
let worker;
|
|
2205
2207
|
try {
|
|
2206
|
-
|
|
2208
|
+
worker = spawnWorkerProcess(run, {
|
|
2207
2209
|
name,
|
|
2208
2210
|
task,
|
|
2209
2211
|
ownedPaths: args.owned ? String(args.owned).split(",").map((s) => s.trim()).filter(Boolean) : [],
|
|
@@ -2231,6 +2233,18 @@ function startWorker(args) {
|
|
|
2231
2233
|
console.error(`worker start failed: ${error.message}`);
|
|
2232
2234
|
process.exit(1);
|
|
2233
2235
|
}
|
|
2236
|
+
if (!wait || !worker) return;
|
|
2237
|
+
const outcome = await autoCompleteWorker({
|
|
2238
|
+
run: String(args.run),
|
|
2239
|
+
name: worker.name,
|
|
2240
|
+
...worker.agentOsId ? { agentOsId: worker.agentOsId } : {},
|
|
2241
|
+
...args.baseUrl ? { baseUrl: String(args.baseUrl) } : {},
|
|
2242
|
+
...args.secret ? { secret: String(args.secret) } : {}
|
|
2243
|
+
});
|
|
2244
|
+
console.error(JSON.stringify({ event: "start_wait_outcome", ...outcome }));
|
|
2245
|
+
if (outcome.outcome === "timed_out") {
|
|
2246
|
+
process.exitCode = 1;
|
|
2247
|
+
}
|
|
2234
2248
|
}
|
|
2235
2249
|
|
|
2236
2250
|
// src/dispatch.ts
|
|
@@ -2254,7 +2268,7 @@ async function dispatchRun(args) {
|
|
|
2254
2268
|
const secret = await resolveCallbackSecretWithMint(args.secret ? String(args.secret) : void 0, agentOsId, { baseUrl: base });
|
|
2255
2269
|
const execute = args.execute === true || args.execute === "true";
|
|
2256
2270
|
const dryRun = !execute;
|
|
2257
|
-
const leaseOwner = `
|
|
2271
|
+
const leaseOwner = `kynver-harness:${run.id}`;
|
|
2258
2272
|
const runnerDiskGate = args.diskPath ? observeRunnerDiskGate({ diskPath: String(args.diskPath) }) : observeRunnerDiskGate({ diskPath: run.repo });
|
|
2259
2273
|
const runnerResourceGate = observeRunnerResourceGate({ runId: run.id });
|
|
2260
2274
|
const requestedStarts = Number(args.maxStarts) > 0 ? Math.floor(Number(args.maxStarts)) : 1;
|
|
@@ -2503,7 +2517,7 @@ async function sweepRun(args) {
|
|
|
2503
2517
|
const agentOsId = String(required(String(args.agentOsId || ""), "--agent-os-id"));
|
|
2504
2518
|
const base = resolveBaseUrl(args.baseUrl ? String(args.baseUrl) : void 0);
|
|
2505
2519
|
const secret = await resolveCallbackSecretWithMint(args.secret ? String(args.secret) : void 0, agentOsId, { baseUrl: base });
|
|
2506
|
-
const leaseOwner = `
|
|
2520
|
+
const leaseOwner = `kynver-harness:${run.id}`;
|
|
2507
2521
|
const snapshotPublished = await publishHarnessBoardSnapshot({ run: run.id, agentOsId, ...args }, "run_sweep");
|
|
2508
2522
|
const releasedLocalOrphans = [];
|
|
2509
2523
|
for (const name of Object.keys(run.workers || {})) {
|
|
@@ -2555,7 +2569,7 @@ import { mkdirSync as mkdirSync5, realpathSync } from "node:fs";
|
|
|
2555
2569
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
2556
2570
|
|
|
2557
2571
|
// src/pipeline-tick.ts
|
|
2558
|
-
import
|
|
2572
|
+
import path22 from "node:path";
|
|
2559
2573
|
|
|
2560
2574
|
// src/stale-reconcile.ts
|
|
2561
2575
|
import path15 from "node:path";
|
|
@@ -2758,13 +2772,433 @@ async function fetchWorkspaceRuntimePreferences(agentOsId, args) {
|
|
|
2758
2772
|
}
|
|
2759
2773
|
}
|
|
2760
2774
|
|
|
2775
|
+
// src/cleanup.ts
|
|
2776
|
+
import path21 from "node:path";
|
|
2777
|
+
|
|
2778
|
+
// src/cleanup-types.ts
|
|
2779
|
+
var DEFAULT_NODE_MODULES_AGE_MS = 6 * 60 * 60 * 1e3;
|
|
2780
|
+
var DEFAULT_WORKTREES_AGE_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
2781
|
+
|
|
2782
|
+
// src/cleanup-guards.ts
|
|
2783
|
+
var ACTIVE_RUN_STATUSES2 = /* @__PURE__ */ new Set(["running", "dispatching", "pending", "queued", "needs_attention"]);
|
|
2784
|
+
var TERMINAL_RUN_STATUSES = /* @__PURE__ */ new Set(["completed", "failed", "cancelled"]);
|
|
2785
|
+
function prUrlFromFinalResult(finalResult) {
|
|
2786
|
+
if (typeof finalResult === "string") {
|
|
2787
|
+
const match = finalResult.match(/https:\/\/github\.com\/[^\s]+\/pull\/\d+/i);
|
|
2788
|
+
return match?.[0] ?? null;
|
|
2789
|
+
}
|
|
2790
|
+
if (finalResult && typeof finalResult === "object") {
|
|
2791
|
+
const obj = finalResult;
|
|
2792
|
+
for (const key of ["prUrl", "pr_url", "pullRequestUrl"]) {
|
|
2793
|
+
const value = obj[key];
|
|
2794
|
+
if (typeof value === "string" && value.trim()) return value.trim();
|
|
2795
|
+
}
|
|
2796
|
+
}
|
|
2797
|
+
return null;
|
|
2798
|
+
}
|
|
2799
|
+
function isPrOrUnmergedWork(status) {
|
|
2800
|
+
if (prUrlFromFinalResult(status.finalResult)) return true;
|
|
2801
|
+
const relation = status.gitAncestry?.relation;
|
|
2802
|
+
if (relation === "ahead" || relation === "diverged") return true;
|
|
2803
|
+
if (status.changedFiles.length > 0 && status.finalResult) return true;
|
|
2804
|
+
return false;
|
|
2805
|
+
}
|
|
2806
|
+
function skipWorktreeRemoval(input) {
|
|
2807
|
+
const { indexed, includeOrphans, worktreesAgeMs, ageMs } = input;
|
|
2808
|
+
if (worktreesAgeMs <= 0) return "worktrees_disabled";
|
|
2809
|
+
if (ageMs < worktreesAgeMs) return "below_age_threshold";
|
|
2810
|
+
if (!indexed) return includeOrphans ? null : "orphan_without_flag";
|
|
2811
|
+
if (ACTIVE_RUN_STATUSES2.has(indexed.run.status)) return "run_still_active";
|
|
2812
|
+
if (!TERMINAL_RUN_STATUSES.has(indexed.run.status)) return "run_still_active";
|
|
2813
|
+
if (indexed.status.alive) return "active_worker";
|
|
2814
|
+
if (indexed.worker.status === "running") return "active_worker";
|
|
2815
|
+
if (indexed.worker.completionBlocker) return "completion_blocked";
|
|
2816
|
+
if (isPrOrUnmergedWork(indexed.status)) return "pr_or_unmerged_commits";
|
|
2817
|
+
if (indexed.status.changedFiles.length > 0) return "dirty_worktree";
|
|
2818
|
+
const landing = assessWorkerLanding({
|
|
2819
|
+
finalResult: indexed.status.finalResult,
|
|
2820
|
+
changedFiles: indexed.status.changedFiles,
|
|
2821
|
+
gitAncestry: indexed.status.gitAncestry,
|
|
2822
|
+
prUrl: prUrlFromFinalResult(indexed.status.finalResult)
|
|
2823
|
+
});
|
|
2824
|
+
if (landing.blocked) return "landing_blocked";
|
|
2825
|
+
return null;
|
|
2826
|
+
}
|
|
2827
|
+
function skipNodeModulesRemoval(input) {
|
|
2828
|
+
const { indexed, includeOrphans, nodeModulesAgeMs, ageMs } = input;
|
|
2829
|
+
if (ageMs < nodeModulesAgeMs) return "below_age_threshold";
|
|
2830
|
+
if (!indexed) return includeOrphans ? null : "orphan_without_flag";
|
|
2831
|
+
if (indexed.status.alive) return "active_worker";
|
|
2832
|
+
if (indexed.worker.status === "running") return "active_worker";
|
|
2833
|
+
if (indexed.worker.completionBlocker) return "completion_blocked";
|
|
2834
|
+
if (isPrOrUnmergedWork(indexed.status)) return "pr_or_unmerged_commits";
|
|
2835
|
+
if (indexed.status.changedFiles.length > 0) return "dirty_worktree";
|
|
2836
|
+
const landing = assessWorkerLanding({
|
|
2837
|
+
finalResult: indexed.status.finalResult,
|
|
2838
|
+
changedFiles: indexed.status.changedFiles,
|
|
2839
|
+
gitAncestry: indexed.status.gitAncestry,
|
|
2840
|
+
prUrl: prUrlFromFinalResult(indexed.status.finalResult)
|
|
2841
|
+
});
|
|
2842
|
+
if (landing.blocked) return "landing_blocked";
|
|
2843
|
+
return null;
|
|
2844
|
+
}
|
|
2845
|
+
|
|
2846
|
+
// src/cleanup-execute.ts
|
|
2847
|
+
import { existsSync as existsSync13, rmSync } from "node:fs";
|
|
2848
|
+
import path18 from "node:path";
|
|
2849
|
+
|
|
2850
|
+
// src/cleanup-dir-size.ts
|
|
2851
|
+
import { existsSync as existsSync12, readdirSync as readdirSync4, statSync as statSync2 } from "node:fs";
|
|
2852
|
+
import path17 from "node:path";
|
|
2853
|
+
function directorySizeBytes(root, maxEntries = 5e4) {
|
|
2854
|
+
if (!existsSync12(root)) return 0;
|
|
2855
|
+
let total = 0;
|
|
2856
|
+
let seen = 0;
|
|
2857
|
+
const stack = [root];
|
|
2858
|
+
while (stack.length > 0) {
|
|
2859
|
+
const current = stack.pop();
|
|
2860
|
+
let entries;
|
|
2861
|
+
try {
|
|
2862
|
+
entries = readdirSync4(current);
|
|
2863
|
+
} catch {
|
|
2864
|
+
continue;
|
|
2865
|
+
}
|
|
2866
|
+
for (const name of entries) {
|
|
2867
|
+
if (seen++ > maxEntries) return null;
|
|
2868
|
+
const full = path17.join(current, name);
|
|
2869
|
+
let st;
|
|
2870
|
+
try {
|
|
2871
|
+
st = statSync2(full);
|
|
2872
|
+
} catch {
|
|
2873
|
+
continue;
|
|
2874
|
+
}
|
|
2875
|
+
if (st.isDirectory()) stack.push(full);
|
|
2876
|
+
else total += st.size;
|
|
2877
|
+
}
|
|
2878
|
+
}
|
|
2879
|
+
return total;
|
|
2880
|
+
}
|
|
2881
|
+
|
|
2882
|
+
// src/cleanup-execute.ts
|
|
2883
|
+
function removeNodeModules(candidate, execute) {
|
|
2884
|
+
if (!existsSync13(candidate.path)) {
|
|
2885
|
+
return {
|
|
2886
|
+
...candidate,
|
|
2887
|
+
executed: false,
|
|
2888
|
+
skipped: true,
|
|
2889
|
+
skipReason: "missing_worktree"
|
|
2890
|
+
};
|
|
2891
|
+
}
|
|
2892
|
+
if (!execute) {
|
|
2893
|
+
return { ...candidate, executed: false, skipped: true, skipReason: "dry_run" };
|
|
2894
|
+
}
|
|
2895
|
+
try {
|
|
2896
|
+
const bytesBefore = candidate.bytes ?? directorySizeBytes(candidate.path);
|
|
2897
|
+
rmSync(candidate.path, { recursive: true, force: true });
|
|
2898
|
+
return {
|
|
2899
|
+
...candidate,
|
|
2900
|
+
bytes: bytesBefore,
|
|
2901
|
+
executed: true,
|
|
2902
|
+
skipped: false
|
|
2903
|
+
};
|
|
2904
|
+
} catch (error) {
|
|
2905
|
+
return {
|
|
2906
|
+
...candidate,
|
|
2907
|
+
executed: false,
|
|
2908
|
+
skipped: true,
|
|
2909
|
+
skipReason: "remove_failed",
|
|
2910
|
+
error: error.message
|
|
2911
|
+
};
|
|
2912
|
+
}
|
|
2913
|
+
}
|
|
2914
|
+
function removeWorktree(candidate, execute) {
|
|
2915
|
+
if (!existsSync13(candidate.path)) {
|
|
2916
|
+
return {
|
|
2917
|
+
...candidate,
|
|
2918
|
+
executed: false,
|
|
2919
|
+
skipped: true,
|
|
2920
|
+
skipReason: "missing_worktree"
|
|
2921
|
+
};
|
|
2922
|
+
}
|
|
2923
|
+
if (!execute) {
|
|
2924
|
+
return { ...candidate, executed: false, skipped: true, skipReason: "dry_run" };
|
|
2925
|
+
}
|
|
2926
|
+
const repo = candidate.repo;
|
|
2927
|
+
try {
|
|
2928
|
+
const bytesBefore = candidate.bytes ?? directorySizeBytes(candidate.path);
|
|
2929
|
+
if (repo) {
|
|
2930
|
+
git(repo, ["worktree", "remove", "--force", candidate.path], { allowFailure: true });
|
|
2931
|
+
}
|
|
2932
|
+
if (existsSync13(candidate.path)) {
|
|
2933
|
+
rmSync(candidate.path, { recursive: true, force: true });
|
|
2934
|
+
}
|
|
2935
|
+
return {
|
|
2936
|
+
...candidate,
|
|
2937
|
+
bytes: bytesBefore,
|
|
2938
|
+
executed: true,
|
|
2939
|
+
skipped: false
|
|
2940
|
+
};
|
|
2941
|
+
} catch (error) {
|
|
2942
|
+
return {
|
|
2943
|
+
...candidate,
|
|
2944
|
+
executed: false,
|
|
2945
|
+
skipped: true,
|
|
2946
|
+
skipReason: "remove_failed",
|
|
2947
|
+
error: error.message
|
|
2948
|
+
};
|
|
2949
|
+
}
|
|
2950
|
+
}
|
|
2951
|
+
function isHarnessNodeModulesPath(targetPath, harnessRoot, worktreesDir) {
|
|
2952
|
+
const resolved = path18.resolve(targetPath);
|
|
2953
|
+
const nm = resolved.endsWith(`${path18.sep}node_modules`) ? resolved : null;
|
|
2954
|
+
if (!nm) return "path_outside_harness";
|
|
2955
|
+
const rel = path18.relative(worktreesDir, nm);
|
|
2956
|
+
if (rel.startsWith("..") || path18.isAbsolute(rel)) return "path_outside_harness";
|
|
2957
|
+
const parts = rel.split(path18.sep);
|
|
2958
|
+
if (parts.length < 3 || parts[parts.length - 1] !== "node_modules") return "path_outside_harness";
|
|
2959
|
+
if (!resolved.startsWith(path18.resolve(harnessRoot))) return "path_outside_harness";
|
|
2960
|
+
return null;
|
|
2961
|
+
}
|
|
2962
|
+
|
|
2963
|
+
// src/cleanup-scan.ts
|
|
2964
|
+
import { existsSync as existsSync14, readdirSync as readdirSync5, statSync as statSync3 } from "node:fs";
|
|
2965
|
+
import path19 from "node:path";
|
|
2966
|
+
function pathAgeMs(target, now) {
|
|
2967
|
+
try {
|
|
2968
|
+
const mtime = statSync3(target).mtimeMs;
|
|
2969
|
+
return Math.max(0, now - mtime);
|
|
2970
|
+
} catch {
|
|
2971
|
+
return 0;
|
|
2972
|
+
}
|
|
2973
|
+
}
|
|
2974
|
+
function isPathInside(child, parent) {
|
|
2975
|
+
const rel = path19.relative(parent, child);
|
|
2976
|
+
return rel === "" || !rel.startsWith("..") && !path19.isAbsolute(rel);
|
|
2977
|
+
}
|
|
2978
|
+
function scanNodeModulesCandidates(opts) {
|
|
2979
|
+
const candidates = [];
|
|
2980
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2981
|
+
for (const entry of opts.index.values()) {
|
|
2982
|
+
if (opts.runIdFilter && entry.runId !== opts.runIdFilter) continue;
|
|
2983
|
+
const nm = path19.join(entry.worktreePath, "node_modules");
|
|
2984
|
+
if (!existsSync14(nm)) continue;
|
|
2985
|
+
const resolved = path19.resolve(nm);
|
|
2986
|
+
if (seen.has(resolved)) continue;
|
|
2987
|
+
seen.add(resolved);
|
|
2988
|
+
candidates.push({
|
|
2989
|
+
kind: "remove_node_modules",
|
|
2990
|
+
path: resolved,
|
|
2991
|
+
bytes: null,
|
|
2992
|
+
runId: entry.runId,
|
|
2993
|
+
worker: entry.workerName,
|
|
2994
|
+
repo: entry.run.repo,
|
|
2995
|
+
ageMs: pathAgeMs(resolved, opts.now)
|
|
2996
|
+
});
|
|
2997
|
+
}
|
|
2998
|
+
if (!opts.includeOrphans || !existsSync14(opts.worktreesDir)) return candidates;
|
|
2999
|
+
for (const runEntry of readdirSync5(opts.worktreesDir, { withFileTypes: true })) {
|
|
3000
|
+
if (!runEntry.isDirectory()) continue;
|
|
3001
|
+
const runPath = path19.join(opts.worktreesDir, runEntry.name);
|
|
3002
|
+
for (const workerEntry of readdirSync5(runPath, { withFileTypes: true })) {
|
|
3003
|
+
if (!workerEntry.isDirectory()) continue;
|
|
3004
|
+
const worktreePath = path19.join(runPath, workerEntry.name);
|
|
3005
|
+
const nm = path19.join(worktreePath, "node_modules");
|
|
3006
|
+
if (!existsSync14(nm)) continue;
|
|
3007
|
+
const resolved = path19.resolve(nm);
|
|
3008
|
+
if (seen.has(resolved)) continue;
|
|
3009
|
+
if (!isPathInside(resolved, opts.harnessRoot)) continue;
|
|
3010
|
+
seen.add(resolved);
|
|
3011
|
+
candidates.push({
|
|
3012
|
+
kind: "remove_node_modules",
|
|
3013
|
+
path: resolved,
|
|
3014
|
+
bytes: null,
|
|
3015
|
+
runId: runEntry.name,
|
|
3016
|
+
worker: workerEntry.name,
|
|
3017
|
+
ageMs: pathAgeMs(resolved, opts.now)
|
|
3018
|
+
});
|
|
3019
|
+
}
|
|
3020
|
+
}
|
|
3021
|
+
return candidates;
|
|
3022
|
+
}
|
|
3023
|
+
function scanWorktreeCandidates(opts) {
|
|
3024
|
+
if (opts.worktreesAgeMs <= 0) return [];
|
|
3025
|
+
const candidates = [];
|
|
3026
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3027
|
+
for (const entry of opts.index.values()) {
|
|
3028
|
+
if (opts.runIdFilter && entry.runId !== opts.runIdFilter) continue;
|
|
3029
|
+
const resolved = entry.worktreePath;
|
|
3030
|
+
if (!existsSync14(resolved)) continue;
|
|
3031
|
+
if (seen.has(resolved)) continue;
|
|
3032
|
+
seen.add(resolved);
|
|
3033
|
+
candidates.push({
|
|
3034
|
+
kind: "remove_worktree",
|
|
3035
|
+
path: resolved,
|
|
3036
|
+
bytes: null,
|
|
3037
|
+
runId: entry.runId,
|
|
3038
|
+
worker: entry.workerName,
|
|
3039
|
+
repo: entry.run.repo,
|
|
3040
|
+
ageMs: pathAgeMs(resolved, opts.now)
|
|
3041
|
+
});
|
|
3042
|
+
}
|
|
3043
|
+
return candidates;
|
|
3044
|
+
}
|
|
3045
|
+
|
|
3046
|
+
// src/cleanup-worktree-index.ts
|
|
3047
|
+
import path20 from "node:path";
|
|
3048
|
+
function buildWorktreeIndex() {
|
|
3049
|
+
const index = /* @__PURE__ */ new Map();
|
|
3050
|
+
for (const run of listRunRecords()) {
|
|
3051
|
+
for (const name of Object.keys(run.workers || {})) {
|
|
3052
|
+
const workerPath = path20.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json");
|
|
3053
|
+
const worker = readJson(workerPath, void 0);
|
|
3054
|
+
if (!worker?.worktreePath) continue;
|
|
3055
|
+
const status = computeWorkerStatus(worker, { base: run.base, baseCommit: run.baseCommit });
|
|
3056
|
+
index.set(path20.resolve(worker.worktreePath), {
|
|
3057
|
+
worktreePath: path20.resolve(worker.worktreePath),
|
|
3058
|
+
runId: run.id,
|
|
3059
|
+
workerName: name,
|
|
3060
|
+
run,
|
|
3061
|
+
worker,
|
|
3062
|
+
status
|
|
3063
|
+
});
|
|
3064
|
+
}
|
|
3065
|
+
}
|
|
3066
|
+
return index;
|
|
3067
|
+
}
|
|
3068
|
+
|
|
3069
|
+
// src/cleanup.ts
|
|
3070
|
+
function resolveOptions(options = {}) {
|
|
3071
|
+
const harnessRoot = options.harnessRoot ? path21.resolve(options.harnessRoot) : resolveHarnessRoot();
|
|
3072
|
+
const { worktreesDir } = options.harnessRoot ? { worktreesDir: path21.join(harnessRoot, "worktrees") } : getHarnessPaths();
|
|
3073
|
+
const execute = options.execute === true;
|
|
3074
|
+
const nodeModulesAgeMs = options.nodeModulesAgeMs ?? DEFAULT_NODE_MODULES_AGE_MS;
|
|
3075
|
+
const worktreesAgeMs = options.worktreesAgeMs ?? 0;
|
|
3076
|
+
const includeOrphans = options.includeOrphans === true;
|
|
3077
|
+
const runIdFilter = options.runIdFilter ? String(options.runIdFilter) : void 0;
|
|
3078
|
+
const now = options.now ?? Date.now();
|
|
3079
|
+
return {
|
|
3080
|
+
harnessRoot,
|
|
3081
|
+
worktreesDir,
|
|
3082
|
+
execute,
|
|
3083
|
+
dryRun: !execute,
|
|
3084
|
+
nodeModulesAgeMs,
|
|
3085
|
+
worktreesAgeMs,
|
|
3086
|
+
includeOrphans,
|
|
3087
|
+
runIdFilter,
|
|
3088
|
+
now
|
|
3089
|
+
};
|
|
3090
|
+
}
|
|
3091
|
+
function recordSkip(skips, pathValue, reason, detail) {
|
|
3092
|
+
skips.push({ path: pathValue, reason, ...detail ? { detail } : {} });
|
|
3093
|
+
}
|
|
3094
|
+
function runHarnessCleanup(options = {}) {
|
|
3095
|
+
const resolved = resolveOptions(options);
|
|
3096
|
+
const index = buildWorktreeIndex();
|
|
3097
|
+
const scanOpts = {
|
|
3098
|
+
harnessRoot: resolved.harnessRoot,
|
|
3099
|
+
worktreesDir: resolved.worktreesDir,
|
|
3100
|
+
nodeModulesAgeMs: resolved.nodeModulesAgeMs,
|
|
3101
|
+
worktreesAgeMs: resolved.worktreesAgeMs,
|
|
3102
|
+
includeOrphans: resolved.includeOrphans,
|
|
3103
|
+
runIdFilter: resolved.runIdFilter,
|
|
3104
|
+
index,
|
|
3105
|
+
now: resolved.now
|
|
3106
|
+
};
|
|
3107
|
+
const skips = [];
|
|
3108
|
+
const actions = [];
|
|
3109
|
+
for (const candidate of scanNodeModulesCandidates(scanOpts)) {
|
|
3110
|
+
const pathSkip = isHarnessNodeModulesPath(candidate.path, resolved.harnessRoot, resolved.worktreesDir);
|
|
3111
|
+
if (pathSkip) {
|
|
3112
|
+
recordSkip(skips, candidate.path, pathSkip);
|
|
3113
|
+
actions.push({ ...candidate, executed: false, skipped: true, skipReason: pathSkip });
|
|
3114
|
+
continue;
|
|
3115
|
+
}
|
|
3116
|
+
const worktreePath = path21.resolve(candidate.path, "..");
|
|
3117
|
+
const indexed = index.get(worktreePath) ?? null;
|
|
3118
|
+
const guardReason = skipNodeModulesRemoval({
|
|
3119
|
+
indexed,
|
|
3120
|
+
includeOrphans: resolved.includeOrphans,
|
|
3121
|
+
nodeModulesAgeMs: resolved.nodeModulesAgeMs,
|
|
3122
|
+
ageMs: candidate.ageMs
|
|
3123
|
+
});
|
|
3124
|
+
if (guardReason) {
|
|
3125
|
+
recordSkip(skips, candidate.path, guardReason);
|
|
3126
|
+
actions.push({ ...candidate, executed: false, skipped: true, skipReason: guardReason });
|
|
3127
|
+
continue;
|
|
3128
|
+
}
|
|
3129
|
+
actions.push(removeNodeModules(candidate, resolved.execute));
|
|
3130
|
+
}
|
|
3131
|
+
for (const candidate of scanWorktreeCandidates(scanOpts)) {
|
|
3132
|
+
const indexed = index.get(path21.resolve(candidate.path)) ?? null;
|
|
3133
|
+
const guardReason = skipWorktreeRemoval({
|
|
3134
|
+
indexed,
|
|
3135
|
+
includeOrphans: resolved.includeOrphans,
|
|
3136
|
+
worktreesAgeMs: resolved.worktreesAgeMs,
|
|
3137
|
+
ageMs: candidate.ageMs
|
|
3138
|
+
});
|
|
3139
|
+
if (guardReason) {
|
|
3140
|
+
recordSkip(skips, candidate.path, guardReason);
|
|
3141
|
+
actions.push({ ...candidate, executed: false, skipped: true, skipReason: guardReason });
|
|
3142
|
+
continue;
|
|
3143
|
+
}
|
|
3144
|
+
actions.push(removeWorktree(candidate, resolved.execute));
|
|
3145
|
+
}
|
|
3146
|
+
let candidateBytes = 0;
|
|
3147
|
+
let removedBytes = 0;
|
|
3148
|
+
let removedPaths = 0;
|
|
3149
|
+
let skippedPaths = 0;
|
|
3150
|
+
for (const action of actions) {
|
|
3151
|
+
if (action.bytes) candidateBytes += action.bytes;
|
|
3152
|
+
if (action.executed) {
|
|
3153
|
+
removedPaths += 1;
|
|
3154
|
+
removedBytes += action.bytes ?? 0;
|
|
3155
|
+
} else if (action.skipped) {
|
|
3156
|
+
skippedPaths += 1;
|
|
3157
|
+
}
|
|
3158
|
+
}
|
|
3159
|
+
return {
|
|
3160
|
+
harnessRoot: resolved.harnessRoot,
|
|
3161
|
+
dryRun: resolved.dryRun,
|
|
3162
|
+
execute: resolved.execute,
|
|
3163
|
+
nodeModulesAgeMs: resolved.nodeModulesAgeMs,
|
|
3164
|
+
worktreesAgeMs: resolved.worktreesAgeMs,
|
|
3165
|
+
includeOrphans: resolved.includeOrphans,
|
|
3166
|
+
scannedAt: new Date(resolved.now).toISOString(),
|
|
3167
|
+
actions,
|
|
3168
|
+
skips,
|
|
3169
|
+
totals: {
|
|
3170
|
+
candidateBytes,
|
|
3171
|
+
removedBytes,
|
|
3172
|
+
removedPaths,
|
|
3173
|
+
skippedPaths
|
|
3174
|
+
}
|
|
3175
|
+
};
|
|
3176
|
+
}
|
|
3177
|
+
function runPipelineHarnessCleanup(runId) {
|
|
3178
|
+
const nodeModulesAgeMs = Number(process.env.KYNVER_CLEANUP_NODE_MODULES_AGE_MS) || DEFAULT_NODE_MODULES_AGE_MS;
|
|
3179
|
+
const worktreesAgeMs = Number(process.env.KYNVER_CLEANUP_WORKTREES_AGE_MS) || 0;
|
|
3180
|
+
const execute = process.env.KYNVER_CLEANUP_EXECUTE === "1";
|
|
3181
|
+
const includeOrphans = process.env.KYNVER_CLEANUP_INCLUDE_ORPHANS === "1";
|
|
3182
|
+
const scopeAll = process.env.KYNVER_CLEANUP_SCOPE === "all";
|
|
3183
|
+
return runHarnessCleanup({
|
|
3184
|
+
execute,
|
|
3185
|
+
nodeModulesAgeMs,
|
|
3186
|
+
worktreesAgeMs,
|
|
3187
|
+
includeOrphans,
|
|
3188
|
+
runIdFilter: scopeAll ? void 0 : runId
|
|
3189
|
+
});
|
|
3190
|
+
}
|
|
3191
|
+
function isPipelineCleanupEnabled() {
|
|
3192
|
+
return process.env.KYNVER_PIPELINE_CLEANUP !== "0";
|
|
3193
|
+
}
|
|
3194
|
+
|
|
2761
3195
|
// src/pipeline-tick.ts
|
|
2762
3196
|
async function completeFinishedWorkers(runId, args) {
|
|
2763
3197
|
const run = loadRun(runId);
|
|
2764
3198
|
const outcomes = [];
|
|
2765
3199
|
for (const name of Object.keys(run.workers || {})) {
|
|
2766
3200
|
const worker = readJson(
|
|
2767
|
-
|
|
3201
|
+
path22.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
|
|
2768
3202
|
void 0
|
|
2769
3203
|
);
|
|
2770
3204
|
if (!worker?.taskId) continue;
|
|
@@ -2813,6 +3247,7 @@ async function runPipelineTick(args) {
|
|
|
2813
3247
|
const operatorTick = await postOperatorTick(agentOsId, runId, resourceGate, args);
|
|
2814
3248
|
const completedWorkers = await completeFinishedWorkers(runId, args);
|
|
2815
3249
|
const staleReconcile = reconcileStaleWorkers();
|
|
3250
|
+
const harnessCleanup = isPipelineCleanupEnabled() ? runPipelineHarnessCleanup(runId) : void 0;
|
|
2816
3251
|
const planProgressSync = await syncActiveWorkerPlanProgress(runId, args);
|
|
2817
3252
|
let maxStarts = resourceGate.slotsAvailable;
|
|
2818
3253
|
const tickBody = operatorTick;
|
|
@@ -2848,6 +3283,7 @@ async function runPipelineTick(args) {
|
|
|
2848
3283
|
resourceGate,
|
|
2849
3284
|
completedWorkers,
|
|
2850
3285
|
staleReconcile,
|
|
3286
|
+
harnessCleanup,
|
|
2851
3287
|
planProgressSync,
|
|
2852
3288
|
operatorTick,
|
|
2853
3289
|
sweep,
|
|
@@ -2986,6 +3422,26 @@ async function verifyPlan(args) {
|
|
|
2986
3422
|
console.log(JSON.stringify(parsed, null, 2));
|
|
2987
3423
|
}
|
|
2988
3424
|
|
|
3425
|
+
// src/cleanup-cli.ts
|
|
3426
|
+
function runCleanupCli(args) {
|
|
3427
|
+
const execute = args.execute === true || args.execute === "true";
|
|
3428
|
+
const nodeModulesAgeMs = args.nodeModulesAgeMs ? Number(args.nodeModulesAgeMs) : DEFAULT_NODE_MODULES_AGE_MS;
|
|
3429
|
+
const worktreesAgeMs = args.worktreesAgeMs ? Number(args.worktreesAgeMs) : 0;
|
|
3430
|
+
const includeOrphans = args.includeOrphans === true || args.includeOrphans === "true";
|
|
3431
|
+
const harnessRoot = args.harnessRoot ? String(args.harnessRoot) : void 0;
|
|
3432
|
+
const summary = runHarnessCleanup({
|
|
3433
|
+
execute,
|
|
3434
|
+
nodeModulesAgeMs: Number.isFinite(nodeModulesAgeMs) ? nodeModulesAgeMs : DEFAULT_NODE_MODULES_AGE_MS,
|
|
3435
|
+
worktreesAgeMs: Number.isFinite(worktreesAgeMs) ? worktreesAgeMs : 0,
|
|
3436
|
+
includeOrphans,
|
|
3437
|
+
harnessRoot
|
|
3438
|
+
});
|
|
3439
|
+
console.log(JSON.stringify(summary, null, 2));
|
|
3440
|
+
if (execute && summary.totals.removedPaths === 0 && summary.actions.length === 0) {
|
|
3441
|
+
process.exitCode = 0;
|
|
3442
|
+
}
|
|
3443
|
+
}
|
|
3444
|
+
|
|
2989
3445
|
// src/cli.ts
|
|
2990
3446
|
function isHelpFlag(arg) {
|
|
2991
3447
|
return arg === "help" || arg === "--help" || arg === "-h";
|
|
@@ -3009,14 +3465,15 @@ function usage(code = 0) {
|
|
|
3009
3465
|
" kynver run status --run RUN_ID",
|
|
3010
3466
|
" kynver run dispatch --run RUN_ID --agent-os-id AOS_ID [--base-url URL] [--secret SECRET] [--execute] [--lane any|implementation|review|landing] [--executor harness] [--max-starts 1] [--lease-ms MS] [--owned path[,path]] [--model claude-opus-4-7] [--disk-path /]",
|
|
3011
3467
|
" kynver run sweep --run RUN_ID --agent-os-id AOS_ID [--base-url URL] [--secret SECRET] [--grace-ms MS]",
|
|
3012
|
-
' kynver worker start --run RUN_ID --name worker --task "..." [--owned path[,path]] [--model MODEL] [--provider claude|cursor] [--agent-os-id AOS_ID] [--task-id TASK_ID]',
|
|
3468
|
+
' kynver worker start --run RUN_ID --name worker --task "..." [--owned path[,path]] [--model MODEL] [--provider claude|cursor] [--agent-os-id AOS_ID] [--task-id TASK_ID] [--wait]',
|
|
3013
3469
|
" kynver worker status --run RUN_ID --name worker",
|
|
3014
3470
|
" kynver worker tail --run RUN_ID --name worker [--lines 40] [--raw]",
|
|
3015
3471
|
" kynver worker stop --run RUN_ID --name worker",
|
|
3016
3472
|
" kynver worker complete --run RUN_ID --name worker [--agent-os-id AOS_ID] [--task-id TASK_ID] [--base-url URL] [--secret SECRET]",
|
|
3017
3473
|
" kynver worker auto-complete --run RUN_ID --name worker [--agent-os-id AOS_ID] [--poll-ms 5000] [--max-total-ms 21600000] [--complete-attempts 3] [--complete-backoff-ms 5000] [--base-url URL] [--secret SECRET]",
|
|
3018
3474
|
" kynver plan progress --plan PLAN_ID --row ROW_KEY --role ROLE --status STATUS [--task TASK_ID] [--note NOTE] [--evidence type:value] [--agent-os-id AOS_ID]",
|
|
3019
|
-
" kynver plan verify --plan PLAN_ID [--worktree PATH] [--task TASK_ID] [--human-override]"
|
|
3475
|
+
" kynver plan verify --plan PLAN_ID [--worktree PATH] [--task TASK_ID] [--human-override]",
|
|
3476
|
+
" kynver cleanup [--execute] [--node-modules-age-ms MS] [--worktrees-age-ms MS] [--harness-root PATH] [--include-orphans]"
|
|
3020
3477
|
].join("\n")
|
|
3021
3478
|
);
|
|
3022
3479
|
process.exit(code);
|
|
@@ -3043,12 +3500,13 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
3043
3500
|
if (scope === "daemon") return void await runDaemon(args);
|
|
3044
3501
|
if (scope === "plan" && action === "progress") return void await emitPlanProgress(args);
|
|
3045
3502
|
if (scope === "plan" && action === "verify") return void await verifyPlan(args);
|
|
3503
|
+
if (scope === "cleanup") return runCleanupCli(args);
|
|
3046
3504
|
if (scope === "run" && action === "create") return createRun(args);
|
|
3047
3505
|
if (scope === "run" && action === "list") return listRuns();
|
|
3048
3506
|
if (scope === "run" && action === "status") return runStatus(args);
|
|
3049
3507
|
if (scope === "run" && action === "dispatch") return void await dispatchRun(args);
|
|
3050
3508
|
if (scope === "run" && action === "sweep") return void await sweepRun(args);
|
|
3051
|
-
if (scope === "worker" && action === "start") return startWorker(args);
|
|
3509
|
+
if (scope === "worker" && action === "start") return void await startWorker(args);
|
|
3052
3510
|
if (scope === "worker" && action === "status") return workerStatus(args);
|
|
3053
3511
|
if (scope === "worker" && action === "tail") return tailWorker(args);
|
|
3054
3512
|
if (scope === "worker" && action === "stop") return stopWorker(args);
|