@kynver-app/runtime 0.1.24 → 0.1.25
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/cli.js
CHANGED
|
@@ -421,12 +421,12 @@ var DEFAULT_CRITICAL_FREE_BYTES = 15 * 1024 * 1024 * 1024;
|
|
|
421
421
|
var DEFAULT_MAX_USED_PERCENT = 80;
|
|
422
422
|
var DEFAULT_HARD_MAX_USED_PERCENT = 90;
|
|
423
423
|
function observeRunnerDiskGate(input = {}) {
|
|
424
|
-
const
|
|
424
|
+
const path23 = input.diskPath?.trim() || "/";
|
|
425
425
|
const warnBelowBytes = input.diskFreeWarnBytes ?? DEFAULT_WARN_FREE_BYTES;
|
|
426
426
|
const criticalBelowBytes = input.diskFreeCriticalBytes ?? DEFAULT_CRITICAL_FREE_BYTES;
|
|
427
427
|
const maxUsedPercent = input.diskMaxUsedPercent ?? DEFAULT_MAX_USED_PERCENT;
|
|
428
428
|
const hardMaxUsedPercent = input.diskHardMaxUsedPercent ?? DEFAULT_HARD_MAX_USED_PERCENT;
|
|
429
|
-
const stats = statfsSync(
|
|
429
|
+
const stats = statfsSync(path23);
|
|
430
430
|
const freeBytes = Number(stats.bavail) * Number(stats.bsize);
|
|
431
431
|
const totalBytes = Number(stats.blocks) * Number(stats.bsize);
|
|
432
432
|
const usedPercent = totalBytes > 0 ? (totalBytes - freeBytes) / totalBytes * 100 : 100;
|
|
@@ -446,7 +446,7 @@ function observeRunnerDiskGate(input = {}) {
|
|
|
446
446
|
}
|
|
447
447
|
return {
|
|
448
448
|
ok,
|
|
449
|
-
path:
|
|
449
|
+
path: path23,
|
|
450
450
|
freeBytes,
|
|
451
451
|
totalBytes,
|
|
452
452
|
usedPercent,
|
|
@@ -1699,7 +1699,7 @@ async function tryCompleteWorker(args) {
|
|
|
1699
1699
|
let secret = await resolveCallbackSecretWithMint(explicitSecret, agentOsId, { baseUrl: base });
|
|
1700
1700
|
const url = `${base}/api/agent-os/by-id/${encodeURIComponent(agentOsId)}/harness/completion`;
|
|
1701
1701
|
const body = {
|
|
1702
|
-
source: "
|
|
1702
|
+
source: "kynver-harness",
|
|
1703
1703
|
agentOsId,
|
|
1704
1704
|
runId: worker.runId,
|
|
1705
1705
|
workerName: worker.name,
|
|
@@ -2189,7 +2189,7 @@ function spawnWorkerProcess(run, opts) {
|
|
|
2189
2189
|
}
|
|
2190
2190
|
return worker;
|
|
2191
2191
|
}
|
|
2192
|
-
function startWorker(args) {
|
|
2192
|
+
async function startWorker(args) {
|
|
2193
2193
|
const run = loadRun(String(args.run));
|
|
2194
2194
|
const name = typeof args.name === "string" ? args.name.trim() : "";
|
|
2195
2195
|
if (!name) {
|
|
@@ -2201,8 +2201,10 @@ function startWorker(args) {
|
|
|
2201
2201
|
console.error("missing --task or --task-file");
|
|
2202
2202
|
process.exit(1);
|
|
2203
2203
|
}
|
|
2204
|
+
const wait = args.wait === true || args.wait === "true";
|
|
2205
|
+
let worker;
|
|
2204
2206
|
try {
|
|
2205
|
-
|
|
2207
|
+
worker = spawnWorkerProcess(run, {
|
|
2206
2208
|
name,
|
|
2207
2209
|
task,
|
|
2208
2210
|
ownedPaths: args.owned ? String(args.owned).split(",").map((s) => s.trim()).filter(Boolean) : [],
|
|
@@ -2230,6 +2232,18 @@ function startWorker(args) {
|
|
|
2230
2232
|
console.error(`worker start failed: ${error.message}`);
|
|
2231
2233
|
process.exit(1);
|
|
2232
2234
|
}
|
|
2235
|
+
if (!wait || !worker) return;
|
|
2236
|
+
const outcome = await autoCompleteWorker({
|
|
2237
|
+
run: String(args.run),
|
|
2238
|
+
name: worker.name,
|
|
2239
|
+
...worker.agentOsId ? { agentOsId: worker.agentOsId } : {},
|
|
2240
|
+
...args.baseUrl ? { baseUrl: String(args.baseUrl) } : {},
|
|
2241
|
+
...args.secret ? { secret: String(args.secret) } : {}
|
|
2242
|
+
});
|
|
2243
|
+
console.error(JSON.stringify({ event: "start_wait_outcome", ...outcome }));
|
|
2244
|
+
if (outcome.outcome === "timed_out") {
|
|
2245
|
+
process.exitCode = 1;
|
|
2246
|
+
}
|
|
2233
2247
|
}
|
|
2234
2248
|
|
|
2235
2249
|
// src/dispatch.ts
|
|
@@ -2253,7 +2267,7 @@ async function dispatchRun(args) {
|
|
|
2253
2267
|
const secret = await resolveCallbackSecretWithMint(args.secret ? String(args.secret) : void 0, agentOsId, { baseUrl: base });
|
|
2254
2268
|
const execute = args.execute === true || args.execute === "true";
|
|
2255
2269
|
const dryRun = !execute;
|
|
2256
|
-
const leaseOwner = `
|
|
2270
|
+
const leaseOwner = `kynver-harness:${run.id}`;
|
|
2257
2271
|
const runnerDiskGate = args.diskPath ? observeRunnerDiskGate({ diskPath: String(args.diskPath) }) : observeRunnerDiskGate({ diskPath: run.repo });
|
|
2258
2272
|
const runnerResourceGate = observeRunnerResourceGate({ runId: run.id });
|
|
2259
2273
|
const requestedStarts = Number(args.maxStarts) > 0 ? Math.floor(Number(args.maxStarts)) : 1;
|
|
@@ -2418,7 +2432,7 @@ async function sweepRun(args) {
|
|
|
2418
2432
|
const agentOsId = String(required(String(args.agentOsId || ""), "--agent-os-id"));
|
|
2419
2433
|
const base = resolveBaseUrl(args.baseUrl ? String(args.baseUrl) : void 0);
|
|
2420
2434
|
const secret = await resolveCallbackSecretWithMint(args.secret ? String(args.secret) : void 0, agentOsId, { baseUrl: base });
|
|
2421
|
-
const leaseOwner = `
|
|
2435
|
+
const leaseOwner = `kynver-harness:${run.id}`;
|
|
2422
2436
|
const snapshotPublished = await publishHarnessBoardSnapshot({ run: run.id, agentOsId, ...args }, "run_sweep");
|
|
2423
2437
|
const releasedLocalOrphans = [];
|
|
2424
2438
|
for (const name of Object.keys(run.workers || {})) {
|
|
@@ -2523,7 +2537,7 @@ function failExists(message) {
|
|
|
2523
2537
|
}
|
|
2524
2538
|
|
|
2525
2539
|
// src/pipeline-tick.ts
|
|
2526
|
-
import
|
|
2540
|
+
import path22 from "node:path";
|
|
2527
2541
|
|
|
2528
2542
|
// src/stale-reconcile.ts
|
|
2529
2543
|
import path15 from "node:path";
|
|
@@ -2726,13 +2740,433 @@ async function fetchWorkspaceRuntimePreferences(agentOsId, args) {
|
|
|
2726
2740
|
}
|
|
2727
2741
|
}
|
|
2728
2742
|
|
|
2743
|
+
// src/cleanup.ts
|
|
2744
|
+
import path21 from "node:path";
|
|
2745
|
+
|
|
2746
|
+
// src/cleanup-types.ts
|
|
2747
|
+
var DEFAULT_NODE_MODULES_AGE_MS = 6 * 60 * 60 * 1e3;
|
|
2748
|
+
var DEFAULT_WORKTREES_AGE_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
2749
|
+
|
|
2750
|
+
// src/cleanup-guards.ts
|
|
2751
|
+
var ACTIVE_RUN_STATUSES2 = /* @__PURE__ */ new Set(["running", "dispatching", "pending", "queued", "needs_attention"]);
|
|
2752
|
+
var TERMINAL_RUN_STATUSES = /* @__PURE__ */ new Set(["completed", "failed", "cancelled"]);
|
|
2753
|
+
function prUrlFromFinalResult(finalResult) {
|
|
2754
|
+
if (typeof finalResult === "string") {
|
|
2755
|
+
const match = finalResult.match(/https:\/\/github\.com\/[^\s]+\/pull\/\d+/i);
|
|
2756
|
+
return match?.[0] ?? null;
|
|
2757
|
+
}
|
|
2758
|
+
if (finalResult && typeof finalResult === "object") {
|
|
2759
|
+
const obj = finalResult;
|
|
2760
|
+
for (const key of ["prUrl", "pr_url", "pullRequestUrl"]) {
|
|
2761
|
+
const value = obj[key];
|
|
2762
|
+
if (typeof value === "string" && value.trim()) return value.trim();
|
|
2763
|
+
}
|
|
2764
|
+
}
|
|
2765
|
+
return null;
|
|
2766
|
+
}
|
|
2767
|
+
function isPrOrUnmergedWork(status) {
|
|
2768
|
+
if (prUrlFromFinalResult(status.finalResult)) return true;
|
|
2769
|
+
const relation = status.gitAncestry?.relation;
|
|
2770
|
+
if (relation === "ahead" || relation === "diverged") return true;
|
|
2771
|
+
if (status.changedFiles.length > 0 && status.finalResult) return true;
|
|
2772
|
+
return false;
|
|
2773
|
+
}
|
|
2774
|
+
function skipWorktreeRemoval(input) {
|
|
2775
|
+
const { indexed, includeOrphans, worktreesAgeMs, ageMs } = input;
|
|
2776
|
+
if (worktreesAgeMs <= 0) return "worktrees_disabled";
|
|
2777
|
+
if (ageMs < worktreesAgeMs) return "below_age_threshold";
|
|
2778
|
+
if (!indexed) return includeOrphans ? null : "orphan_without_flag";
|
|
2779
|
+
if (ACTIVE_RUN_STATUSES2.has(indexed.run.status)) return "run_still_active";
|
|
2780
|
+
if (!TERMINAL_RUN_STATUSES.has(indexed.run.status)) return "run_still_active";
|
|
2781
|
+
if (indexed.status.alive) return "active_worker";
|
|
2782
|
+
if (indexed.worker.status === "running") return "active_worker";
|
|
2783
|
+
if (indexed.worker.completionBlocker) return "completion_blocked";
|
|
2784
|
+
if (isPrOrUnmergedWork(indexed.status)) return "pr_or_unmerged_commits";
|
|
2785
|
+
if (indexed.status.changedFiles.length > 0) return "dirty_worktree";
|
|
2786
|
+
const landing = assessWorkerLanding({
|
|
2787
|
+
finalResult: indexed.status.finalResult,
|
|
2788
|
+
changedFiles: indexed.status.changedFiles,
|
|
2789
|
+
gitAncestry: indexed.status.gitAncestry,
|
|
2790
|
+
prUrl: prUrlFromFinalResult(indexed.status.finalResult)
|
|
2791
|
+
});
|
|
2792
|
+
if (landing.blocked) return "landing_blocked";
|
|
2793
|
+
return null;
|
|
2794
|
+
}
|
|
2795
|
+
function skipNodeModulesRemoval(input) {
|
|
2796
|
+
const { indexed, includeOrphans, nodeModulesAgeMs, ageMs } = input;
|
|
2797
|
+
if (ageMs < nodeModulesAgeMs) return "below_age_threshold";
|
|
2798
|
+
if (!indexed) return includeOrphans ? null : "orphan_without_flag";
|
|
2799
|
+
if (indexed.status.alive) return "active_worker";
|
|
2800
|
+
if (indexed.worker.status === "running") return "active_worker";
|
|
2801
|
+
if (indexed.worker.completionBlocker) return "completion_blocked";
|
|
2802
|
+
if (isPrOrUnmergedWork(indexed.status)) return "pr_or_unmerged_commits";
|
|
2803
|
+
if (indexed.status.changedFiles.length > 0) return "dirty_worktree";
|
|
2804
|
+
const landing = assessWorkerLanding({
|
|
2805
|
+
finalResult: indexed.status.finalResult,
|
|
2806
|
+
changedFiles: indexed.status.changedFiles,
|
|
2807
|
+
gitAncestry: indexed.status.gitAncestry,
|
|
2808
|
+
prUrl: prUrlFromFinalResult(indexed.status.finalResult)
|
|
2809
|
+
});
|
|
2810
|
+
if (landing.blocked) return "landing_blocked";
|
|
2811
|
+
return null;
|
|
2812
|
+
}
|
|
2813
|
+
|
|
2814
|
+
// src/cleanup-execute.ts
|
|
2815
|
+
import { existsSync as existsSync13, rmSync } from "node:fs";
|
|
2816
|
+
import path18 from "node:path";
|
|
2817
|
+
|
|
2818
|
+
// src/cleanup-dir-size.ts
|
|
2819
|
+
import { existsSync as existsSync12, readdirSync as readdirSync4, statSync as statSync2 } from "node:fs";
|
|
2820
|
+
import path17 from "node:path";
|
|
2821
|
+
function directorySizeBytes(root, maxEntries = 5e4) {
|
|
2822
|
+
if (!existsSync12(root)) return 0;
|
|
2823
|
+
let total = 0;
|
|
2824
|
+
let seen = 0;
|
|
2825
|
+
const stack = [root];
|
|
2826
|
+
while (stack.length > 0) {
|
|
2827
|
+
const current = stack.pop();
|
|
2828
|
+
let entries;
|
|
2829
|
+
try {
|
|
2830
|
+
entries = readdirSync4(current);
|
|
2831
|
+
} catch {
|
|
2832
|
+
continue;
|
|
2833
|
+
}
|
|
2834
|
+
for (const name of entries) {
|
|
2835
|
+
if (seen++ > maxEntries) return null;
|
|
2836
|
+
const full = path17.join(current, name);
|
|
2837
|
+
let st;
|
|
2838
|
+
try {
|
|
2839
|
+
st = statSync2(full);
|
|
2840
|
+
} catch {
|
|
2841
|
+
continue;
|
|
2842
|
+
}
|
|
2843
|
+
if (st.isDirectory()) stack.push(full);
|
|
2844
|
+
else total += st.size;
|
|
2845
|
+
}
|
|
2846
|
+
}
|
|
2847
|
+
return total;
|
|
2848
|
+
}
|
|
2849
|
+
|
|
2850
|
+
// src/cleanup-execute.ts
|
|
2851
|
+
function removeNodeModules(candidate, execute) {
|
|
2852
|
+
if (!existsSync13(candidate.path)) {
|
|
2853
|
+
return {
|
|
2854
|
+
...candidate,
|
|
2855
|
+
executed: false,
|
|
2856
|
+
skipped: true,
|
|
2857
|
+
skipReason: "missing_worktree"
|
|
2858
|
+
};
|
|
2859
|
+
}
|
|
2860
|
+
if (!execute) {
|
|
2861
|
+
return { ...candidate, executed: false, skipped: true, skipReason: "dry_run" };
|
|
2862
|
+
}
|
|
2863
|
+
try {
|
|
2864
|
+
const bytesBefore = candidate.bytes ?? directorySizeBytes(candidate.path);
|
|
2865
|
+
rmSync(candidate.path, { recursive: true, force: true });
|
|
2866
|
+
return {
|
|
2867
|
+
...candidate,
|
|
2868
|
+
bytes: bytesBefore,
|
|
2869
|
+
executed: true,
|
|
2870
|
+
skipped: false
|
|
2871
|
+
};
|
|
2872
|
+
} catch (error) {
|
|
2873
|
+
return {
|
|
2874
|
+
...candidate,
|
|
2875
|
+
executed: false,
|
|
2876
|
+
skipped: true,
|
|
2877
|
+
skipReason: "remove_failed",
|
|
2878
|
+
error: error.message
|
|
2879
|
+
};
|
|
2880
|
+
}
|
|
2881
|
+
}
|
|
2882
|
+
function removeWorktree(candidate, execute) {
|
|
2883
|
+
if (!existsSync13(candidate.path)) {
|
|
2884
|
+
return {
|
|
2885
|
+
...candidate,
|
|
2886
|
+
executed: false,
|
|
2887
|
+
skipped: true,
|
|
2888
|
+
skipReason: "missing_worktree"
|
|
2889
|
+
};
|
|
2890
|
+
}
|
|
2891
|
+
if (!execute) {
|
|
2892
|
+
return { ...candidate, executed: false, skipped: true, skipReason: "dry_run" };
|
|
2893
|
+
}
|
|
2894
|
+
const repo = candidate.repo;
|
|
2895
|
+
try {
|
|
2896
|
+
const bytesBefore = candidate.bytes ?? directorySizeBytes(candidate.path);
|
|
2897
|
+
if (repo) {
|
|
2898
|
+
git(repo, ["worktree", "remove", "--force", candidate.path], { allowFailure: true });
|
|
2899
|
+
}
|
|
2900
|
+
if (existsSync13(candidate.path)) {
|
|
2901
|
+
rmSync(candidate.path, { recursive: true, force: true });
|
|
2902
|
+
}
|
|
2903
|
+
return {
|
|
2904
|
+
...candidate,
|
|
2905
|
+
bytes: bytesBefore,
|
|
2906
|
+
executed: true,
|
|
2907
|
+
skipped: false
|
|
2908
|
+
};
|
|
2909
|
+
} catch (error) {
|
|
2910
|
+
return {
|
|
2911
|
+
...candidate,
|
|
2912
|
+
executed: false,
|
|
2913
|
+
skipped: true,
|
|
2914
|
+
skipReason: "remove_failed",
|
|
2915
|
+
error: error.message
|
|
2916
|
+
};
|
|
2917
|
+
}
|
|
2918
|
+
}
|
|
2919
|
+
function isHarnessNodeModulesPath(targetPath, harnessRoot, worktreesDir) {
|
|
2920
|
+
const resolved = path18.resolve(targetPath);
|
|
2921
|
+
const nm = resolved.endsWith(`${path18.sep}node_modules`) ? resolved : null;
|
|
2922
|
+
if (!nm) return "path_outside_harness";
|
|
2923
|
+
const rel = path18.relative(worktreesDir, nm);
|
|
2924
|
+
if (rel.startsWith("..") || path18.isAbsolute(rel)) return "path_outside_harness";
|
|
2925
|
+
const parts = rel.split(path18.sep);
|
|
2926
|
+
if (parts.length < 3 || parts[parts.length - 1] !== "node_modules") return "path_outside_harness";
|
|
2927
|
+
if (!resolved.startsWith(path18.resolve(harnessRoot))) return "path_outside_harness";
|
|
2928
|
+
return null;
|
|
2929
|
+
}
|
|
2930
|
+
|
|
2931
|
+
// src/cleanup-scan.ts
|
|
2932
|
+
import { existsSync as existsSync14, readdirSync as readdirSync5, statSync as statSync3 } from "node:fs";
|
|
2933
|
+
import path19 from "node:path";
|
|
2934
|
+
function pathAgeMs(target, now) {
|
|
2935
|
+
try {
|
|
2936
|
+
const mtime = statSync3(target).mtimeMs;
|
|
2937
|
+
return Math.max(0, now - mtime);
|
|
2938
|
+
} catch {
|
|
2939
|
+
return 0;
|
|
2940
|
+
}
|
|
2941
|
+
}
|
|
2942
|
+
function isPathInside(child, parent) {
|
|
2943
|
+
const rel = path19.relative(parent, child);
|
|
2944
|
+
return rel === "" || !rel.startsWith("..") && !path19.isAbsolute(rel);
|
|
2945
|
+
}
|
|
2946
|
+
function scanNodeModulesCandidates(opts) {
|
|
2947
|
+
const candidates = [];
|
|
2948
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2949
|
+
for (const entry of opts.index.values()) {
|
|
2950
|
+
if (opts.runIdFilter && entry.runId !== opts.runIdFilter) continue;
|
|
2951
|
+
const nm = path19.join(entry.worktreePath, "node_modules");
|
|
2952
|
+
if (!existsSync14(nm)) continue;
|
|
2953
|
+
const resolved = path19.resolve(nm);
|
|
2954
|
+
if (seen.has(resolved)) continue;
|
|
2955
|
+
seen.add(resolved);
|
|
2956
|
+
candidates.push({
|
|
2957
|
+
kind: "remove_node_modules",
|
|
2958
|
+
path: resolved,
|
|
2959
|
+
bytes: null,
|
|
2960
|
+
runId: entry.runId,
|
|
2961
|
+
worker: entry.workerName,
|
|
2962
|
+
repo: entry.run.repo,
|
|
2963
|
+
ageMs: pathAgeMs(resolved, opts.now)
|
|
2964
|
+
});
|
|
2965
|
+
}
|
|
2966
|
+
if (!opts.includeOrphans || !existsSync14(opts.worktreesDir)) return candidates;
|
|
2967
|
+
for (const runEntry of readdirSync5(opts.worktreesDir, { withFileTypes: true })) {
|
|
2968
|
+
if (!runEntry.isDirectory()) continue;
|
|
2969
|
+
const runPath = path19.join(opts.worktreesDir, runEntry.name);
|
|
2970
|
+
for (const workerEntry of readdirSync5(runPath, { withFileTypes: true })) {
|
|
2971
|
+
if (!workerEntry.isDirectory()) continue;
|
|
2972
|
+
const worktreePath = path19.join(runPath, workerEntry.name);
|
|
2973
|
+
const nm = path19.join(worktreePath, "node_modules");
|
|
2974
|
+
if (!existsSync14(nm)) continue;
|
|
2975
|
+
const resolved = path19.resolve(nm);
|
|
2976
|
+
if (seen.has(resolved)) continue;
|
|
2977
|
+
if (!isPathInside(resolved, opts.harnessRoot)) continue;
|
|
2978
|
+
seen.add(resolved);
|
|
2979
|
+
candidates.push({
|
|
2980
|
+
kind: "remove_node_modules",
|
|
2981
|
+
path: resolved,
|
|
2982
|
+
bytes: null,
|
|
2983
|
+
runId: runEntry.name,
|
|
2984
|
+
worker: workerEntry.name,
|
|
2985
|
+
ageMs: pathAgeMs(resolved, opts.now)
|
|
2986
|
+
});
|
|
2987
|
+
}
|
|
2988
|
+
}
|
|
2989
|
+
return candidates;
|
|
2990
|
+
}
|
|
2991
|
+
function scanWorktreeCandidates(opts) {
|
|
2992
|
+
if (opts.worktreesAgeMs <= 0) return [];
|
|
2993
|
+
const candidates = [];
|
|
2994
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2995
|
+
for (const entry of opts.index.values()) {
|
|
2996
|
+
if (opts.runIdFilter && entry.runId !== opts.runIdFilter) continue;
|
|
2997
|
+
const resolved = entry.worktreePath;
|
|
2998
|
+
if (!existsSync14(resolved)) continue;
|
|
2999
|
+
if (seen.has(resolved)) continue;
|
|
3000
|
+
seen.add(resolved);
|
|
3001
|
+
candidates.push({
|
|
3002
|
+
kind: "remove_worktree",
|
|
3003
|
+
path: resolved,
|
|
3004
|
+
bytes: null,
|
|
3005
|
+
runId: entry.runId,
|
|
3006
|
+
worker: entry.workerName,
|
|
3007
|
+
repo: entry.run.repo,
|
|
3008
|
+
ageMs: pathAgeMs(resolved, opts.now)
|
|
3009
|
+
});
|
|
3010
|
+
}
|
|
3011
|
+
return candidates;
|
|
3012
|
+
}
|
|
3013
|
+
|
|
3014
|
+
// src/cleanup-worktree-index.ts
|
|
3015
|
+
import path20 from "node:path";
|
|
3016
|
+
function buildWorktreeIndex() {
|
|
3017
|
+
const index = /* @__PURE__ */ new Map();
|
|
3018
|
+
for (const run of listRunRecords()) {
|
|
3019
|
+
for (const name of Object.keys(run.workers || {})) {
|
|
3020
|
+
const workerPath = path20.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json");
|
|
3021
|
+
const worker = readJson(workerPath, void 0);
|
|
3022
|
+
if (!worker?.worktreePath) continue;
|
|
3023
|
+
const status = computeWorkerStatus(worker, { base: run.base, baseCommit: run.baseCommit });
|
|
3024
|
+
index.set(path20.resolve(worker.worktreePath), {
|
|
3025
|
+
worktreePath: path20.resolve(worker.worktreePath),
|
|
3026
|
+
runId: run.id,
|
|
3027
|
+
workerName: name,
|
|
3028
|
+
run,
|
|
3029
|
+
worker,
|
|
3030
|
+
status
|
|
3031
|
+
});
|
|
3032
|
+
}
|
|
3033
|
+
}
|
|
3034
|
+
return index;
|
|
3035
|
+
}
|
|
3036
|
+
|
|
3037
|
+
// src/cleanup.ts
|
|
3038
|
+
function resolveOptions(options = {}) {
|
|
3039
|
+
const harnessRoot = options.harnessRoot ? path21.resolve(options.harnessRoot) : resolveHarnessRoot();
|
|
3040
|
+
const { worktreesDir } = options.harnessRoot ? { worktreesDir: path21.join(harnessRoot, "worktrees") } : getHarnessPaths();
|
|
3041
|
+
const execute = options.execute === true;
|
|
3042
|
+
const nodeModulesAgeMs = options.nodeModulesAgeMs ?? DEFAULT_NODE_MODULES_AGE_MS;
|
|
3043
|
+
const worktreesAgeMs = options.worktreesAgeMs ?? 0;
|
|
3044
|
+
const includeOrphans = options.includeOrphans === true;
|
|
3045
|
+
const runIdFilter = options.runIdFilter ? String(options.runIdFilter) : void 0;
|
|
3046
|
+
const now = options.now ?? Date.now();
|
|
3047
|
+
return {
|
|
3048
|
+
harnessRoot,
|
|
3049
|
+
worktreesDir,
|
|
3050
|
+
execute,
|
|
3051
|
+
dryRun: !execute,
|
|
3052
|
+
nodeModulesAgeMs,
|
|
3053
|
+
worktreesAgeMs,
|
|
3054
|
+
includeOrphans,
|
|
3055
|
+
runIdFilter,
|
|
3056
|
+
now
|
|
3057
|
+
};
|
|
3058
|
+
}
|
|
3059
|
+
function recordSkip(skips, pathValue, reason, detail) {
|
|
3060
|
+
skips.push({ path: pathValue, reason, ...detail ? { detail } : {} });
|
|
3061
|
+
}
|
|
3062
|
+
function runHarnessCleanup(options = {}) {
|
|
3063
|
+
const resolved = resolveOptions(options);
|
|
3064
|
+
const index = buildWorktreeIndex();
|
|
3065
|
+
const scanOpts = {
|
|
3066
|
+
harnessRoot: resolved.harnessRoot,
|
|
3067
|
+
worktreesDir: resolved.worktreesDir,
|
|
3068
|
+
nodeModulesAgeMs: resolved.nodeModulesAgeMs,
|
|
3069
|
+
worktreesAgeMs: resolved.worktreesAgeMs,
|
|
3070
|
+
includeOrphans: resolved.includeOrphans,
|
|
3071
|
+
runIdFilter: resolved.runIdFilter,
|
|
3072
|
+
index,
|
|
3073
|
+
now: resolved.now
|
|
3074
|
+
};
|
|
3075
|
+
const skips = [];
|
|
3076
|
+
const actions = [];
|
|
3077
|
+
for (const candidate of scanNodeModulesCandidates(scanOpts)) {
|
|
3078
|
+
const pathSkip = isHarnessNodeModulesPath(candidate.path, resolved.harnessRoot, resolved.worktreesDir);
|
|
3079
|
+
if (pathSkip) {
|
|
3080
|
+
recordSkip(skips, candidate.path, pathSkip);
|
|
3081
|
+
actions.push({ ...candidate, executed: false, skipped: true, skipReason: pathSkip });
|
|
3082
|
+
continue;
|
|
3083
|
+
}
|
|
3084
|
+
const worktreePath = path21.resolve(candidate.path, "..");
|
|
3085
|
+
const indexed = index.get(worktreePath) ?? null;
|
|
3086
|
+
const guardReason = skipNodeModulesRemoval({
|
|
3087
|
+
indexed,
|
|
3088
|
+
includeOrphans: resolved.includeOrphans,
|
|
3089
|
+
nodeModulesAgeMs: resolved.nodeModulesAgeMs,
|
|
3090
|
+
ageMs: candidate.ageMs
|
|
3091
|
+
});
|
|
3092
|
+
if (guardReason) {
|
|
3093
|
+
recordSkip(skips, candidate.path, guardReason);
|
|
3094
|
+
actions.push({ ...candidate, executed: false, skipped: true, skipReason: guardReason });
|
|
3095
|
+
continue;
|
|
3096
|
+
}
|
|
3097
|
+
actions.push(removeNodeModules(candidate, resolved.execute));
|
|
3098
|
+
}
|
|
3099
|
+
for (const candidate of scanWorktreeCandidates(scanOpts)) {
|
|
3100
|
+
const indexed = index.get(path21.resolve(candidate.path)) ?? null;
|
|
3101
|
+
const guardReason = skipWorktreeRemoval({
|
|
3102
|
+
indexed,
|
|
3103
|
+
includeOrphans: resolved.includeOrphans,
|
|
3104
|
+
worktreesAgeMs: resolved.worktreesAgeMs,
|
|
3105
|
+
ageMs: candidate.ageMs
|
|
3106
|
+
});
|
|
3107
|
+
if (guardReason) {
|
|
3108
|
+
recordSkip(skips, candidate.path, guardReason);
|
|
3109
|
+
actions.push({ ...candidate, executed: false, skipped: true, skipReason: guardReason });
|
|
3110
|
+
continue;
|
|
3111
|
+
}
|
|
3112
|
+
actions.push(removeWorktree(candidate, resolved.execute));
|
|
3113
|
+
}
|
|
3114
|
+
let candidateBytes = 0;
|
|
3115
|
+
let removedBytes = 0;
|
|
3116
|
+
let removedPaths = 0;
|
|
3117
|
+
let skippedPaths = 0;
|
|
3118
|
+
for (const action of actions) {
|
|
3119
|
+
if (action.bytes) candidateBytes += action.bytes;
|
|
3120
|
+
if (action.executed) {
|
|
3121
|
+
removedPaths += 1;
|
|
3122
|
+
removedBytes += action.bytes ?? 0;
|
|
3123
|
+
} else if (action.skipped) {
|
|
3124
|
+
skippedPaths += 1;
|
|
3125
|
+
}
|
|
3126
|
+
}
|
|
3127
|
+
return {
|
|
3128
|
+
harnessRoot: resolved.harnessRoot,
|
|
3129
|
+
dryRun: resolved.dryRun,
|
|
3130
|
+
execute: resolved.execute,
|
|
3131
|
+
nodeModulesAgeMs: resolved.nodeModulesAgeMs,
|
|
3132
|
+
worktreesAgeMs: resolved.worktreesAgeMs,
|
|
3133
|
+
includeOrphans: resolved.includeOrphans,
|
|
3134
|
+
scannedAt: new Date(resolved.now).toISOString(),
|
|
3135
|
+
actions,
|
|
3136
|
+
skips,
|
|
3137
|
+
totals: {
|
|
3138
|
+
candidateBytes,
|
|
3139
|
+
removedBytes,
|
|
3140
|
+
removedPaths,
|
|
3141
|
+
skippedPaths
|
|
3142
|
+
}
|
|
3143
|
+
};
|
|
3144
|
+
}
|
|
3145
|
+
function runPipelineHarnessCleanup(runId) {
|
|
3146
|
+
const nodeModulesAgeMs = Number(process.env.KYNVER_CLEANUP_NODE_MODULES_AGE_MS) || DEFAULT_NODE_MODULES_AGE_MS;
|
|
3147
|
+
const worktreesAgeMs = Number(process.env.KYNVER_CLEANUP_WORKTREES_AGE_MS) || 0;
|
|
3148
|
+
const execute = process.env.KYNVER_CLEANUP_EXECUTE === "1";
|
|
3149
|
+
const includeOrphans = process.env.KYNVER_CLEANUP_INCLUDE_ORPHANS === "1";
|
|
3150
|
+
const scopeAll = process.env.KYNVER_CLEANUP_SCOPE === "all";
|
|
3151
|
+
return runHarnessCleanup({
|
|
3152
|
+
execute,
|
|
3153
|
+
nodeModulesAgeMs,
|
|
3154
|
+
worktreesAgeMs,
|
|
3155
|
+
includeOrphans,
|
|
3156
|
+
runIdFilter: scopeAll ? void 0 : runId
|
|
3157
|
+
});
|
|
3158
|
+
}
|
|
3159
|
+
function isPipelineCleanupEnabled() {
|
|
3160
|
+
return process.env.KYNVER_PIPELINE_CLEANUP !== "0";
|
|
3161
|
+
}
|
|
3162
|
+
|
|
2729
3163
|
// src/pipeline-tick.ts
|
|
2730
3164
|
async function completeFinishedWorkers(runId, args) {
|
|
2731
3165
|
const run = loadRun(runId);
|
|
2732
3166
|
const outcomes = [];
|
|
2733
3167
|
for (const name of Object.keys(run.workers || {})) {
|
|
2734
3168
|
const worker = readJson(
|
|
2735
|
-
|
|
3169
|
+
path22.join(runDirectory(run.id), "workers", safeSlug(name), "worker.json"),
|
|
2736
3170
|
void 0
|
|
2737
3171
|
);
|
|
2738
3172
|
if (!worker?.taskId) continue;
|
|
@@ -2781,6 +3215,7 @@ async function runPipelineTick(args) {
|
|
|
2781
3215
|
const operatorTick = await postOperatorTick(agentOsId, runId, resourceGate, args);
|
|
2782
3216
|
const completedWorkers = await completeFinishedWorkers(runId, args);
|
|
2783
3217
|
const staleReconcile = reconcileStaleWorkers();
|
|
3218
|
+
const harnessCleanup = isPipelineCleanupEnabled() ? runPipelineHarnessCleanup(runId) : void 0;
|
|
2784
3219
|
const planProgressSync = await syncActiveWorkerPlanProgress(runId, args);
|
|
2785
3220
|
let maxStarts = resourceGate.slotsAvailable;
|
|
2786
3221
|
const tickBody = operatorTick;
|
|
@@ -2816,6 +3251,7 @@ async function runPipelineTick(args) {
|
|
|
2816
3251
|
resourceGate,
|
|
2817
3252
|
completedWorkers,
|
|
2818
3253
|
staleReconcile,
|
|
3254
|
+
harnessCleanup,
|
|
2819
3255
|
planProgressSync,
|
|
2820
3256
|
operatorTick,
|
|
2821
3257
|
sweep,
|
|
@@ -2954,6 +3390,26 @@ async function verifyPlan(args) {
|
|
|
2954
3390
|
console.log(JSON.stringify(parsed, null, 2));
|
|
2955
3391
|
}
|
|
2956
3392
|
|
|
3393
|
+
// src/cleanup-cli.ts
|
|
3394
|
+
function runCleanupCli(args) {
|
|
3395
|
+
const execute = args.execute === true || args.execute === "true";
|
|
3396
|
+
const nodeModulesAgeMs = args.nodeModulesAgeMs ? Number(args.nodeModulesAgeMs) : DEFAULT_NODE_MODULES_AGE_MS;
|
|
3397
|
+
const worktreesAgeMs = args.worktreesAgeMs ? Number(args.worktreesAgeMs) : 0;
|
|
3398
|
+
const includeOrphans = args.includeOrphans === true || args.includeOrphans === "true";
|
|
3399
|
+
const harnessRoot = args.harnessRoot ? String(args.harnessRoot) : void 0;
|
|
3400
|
+
const summary = runHarnessCleanup({
|
|
3401
|
+
execute,
|
|
3402
|
+
nodeModulesAgeMs: Number.isFinite(nodeModulesAgeMs) ? nodeModulesAgeMs : DEFAULT_NODE_MODULES_AGE_MS,
|
|
3403
|
+
worktreesAgeMs: Number.isFinite(worktreesAgeMs) ? worktreesAgeMs : 0,
|
|
3404
|
+
includeOrphans,
|
|
3405
|
+
harnessRoot
|
|
3406
|
+
});
|
|
3407
|
+
console.log(JSON.stringify(summary, null, 2));
|
|
3408
|
+
if (execute && summary.totals.removedPaths === 0 && summary.actions.length === 0) {
|
|
3409
|
+
process.exitCode = 0;
|
|
3410
|
+
}
|
|
3411
|
+
}
|
|
3412
|
+
|
|
2957
3413
|
// src/cli.ts
|
|
2958
3414
|
function isHelpFlag(arg) {
|
|
2959
3415
|
return arg === "help" || arg === "--help" || arg === "-h";
|
|
@@ -2977,14 +3433,15 @@ function usage(code = 0) {
|
|
|
2977
3433
|
" kynver run status --run RUN_ID",
|
|
2978
3434
|
" 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 /]",
|
|
2979
3435
|
" kynver run sweep --run RUN_ID --agent-os-id AOS_ID [--base-url URL] [--secret SECRET] [--grace-ms MS]",
|
|
2980
|
-
' 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]',
|
|
3436
|
+
' 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]',
|
|
2981
3437
|
" kynver worker status --run RUN_ID --name worker",
|
|
2982
3438
|
" kynver worker tail --run RUN_ID --name worker [--lines 40] [--raw]",
|
|
2983
3439
|
" kynver worker stop --run RUN_ID --name worker",
|
|
2984
3440
|
" kynver worker complete --run RUN_ID --name worker [--agent-os-id AOS_ID] [--task-id TASK_ID] [--base-url URL] [--secret SECRET]",
|
|
2985
3441
|
" 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]",
|
|
2986
3442
|
" 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]",
|
|
2987
|
-
" kynver plan verify --plan PLAN_ID [--worktree PATH] [--task TASK_ID] [--human-override]"
|
|
3443
|
+
" kynver plan verify --plan PLAN_ID [--worktree PATH] [--task TASK_ID] [--human-override]",
|
|
3444
|
+
" kynver cleanup [--execute] [--node-modules-age-ms MS] [--worktrees-age-ms MS] [--harness-root PATH] [--include-orphans]"
|
|
2988
3445
|
].join("\n")
|
|
2989
3446
|
);
|
|
2990
3447
|
process.exit(code);
|
|
@@ -3011,12 +3468,13 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
3011
3468
|
if (scope === "daemon") return void await runDaemon(args);
|
|
3012
3469
|
if (scope === "plan" && action === "progress") return void await emitPlanProgress(args);
|
|
3013
3470
|
if (scope === "plan" && action === "verify") return void await verifyPlan(args);
|
|
3471
|
+
if (scope === "cleanup") return runCleanupCli(args);
|
|
3014
3472
|
if (scope === "run" && action === "create") return createRun(args);
|
|
3015
3473
|
if (scope === "run" && action === "list") return listRuns();
|
|
3016
3474
|
if (scope === "run" && action === "status") return runStatus(args);
|
|
3017
3475
|
if (scope === "run" && action === "dispatch") return void await dispatchRun(args);
|
|
3018
3476
|
if (scope === "run" && action === "sweep") return void await sweepRun(args);
|
|
3019
|
-
if (scope === "worker" && action === "start") return startWorker(args);
|
|
3477
|
+
if (scope === "worker" && action === "start") return void await startWorker(args);
|
|
3020
3478
|
if (scope === "worker" && action === "status") return workerStatus(args);
|
|
3021
3479
|
if (scope === "worker" && action === "tail") return tailWorker(args);
|
|
3022
3480
|
if (scope === "worker" && action === "stop") return stopWorker(args);
|