@kynver-app/runtime 0.1.50 → 0.1.51

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.
@@ -1,7 +1,10 @@
1
1
  import type { CleanupSkipReason } from "./cleanup-types.js";
2
2
  import type { IndexedWorktree } from "./cleanup-worktree-index.js";
3
3
  import type { RawHarnessWorkerStatus } from "./status.js";
4
+ /** Blocks whole-worktree removal when commits are not landed or tree is dirty. */
4
5
  export declare function isPrOrUnmergedWork(status: RawHarnessWorkerStatus): boolean;
6
+ /** Strip generated install trees from porcelain — they are what cleanup removes. */
7
+ export declare function materialWorktreeChanges(changedFiles: string[]): string[];
5
8
  export interface WorktreeGuardInput {
6
9
  indexed: IndexedWorktree | null;
7
10
  includeOrphans: boolean;
@@ -0,0 +1,14 @@
1
+ import { type HarnessCleanupOptions } from "./cleanup-types.js";
2
+ export interface ResolvedHarnessRetention {
3
+ execute: boolean;
4
+ finalizeStaleRuns: boolean;
5
+ nodeModulesAgeMs: number;
6
+ worktreesAgeMs: number;
7
+ includeOrphans: boolean;
8
+ runIdFilter?: string;
9
+ accountBytes: boolean;
10
+ }
11
+ /** Merge CLI options with operator env defaults (pipeline + `kynver cleanup`). */
12
+ export declare function resolveHarnessRetention(options?: HarnessCleanupOptions): ResolvedHarnessRetention;
13
+ /** Pipeline tick: dry-run by default; global scope when `KYNVER_CLEANUP_SCOPE=all`. */
14
+ export declare function resolvePipelineHarnessRetention(runId?: string): ResolvedHarnessRetention;
@@ -0,0 +1,13 @@
1
+ import type { IndexedWorktree } from "./cleanup-worktree-index.js";
2
+ /** True when the worker process is still live or marked running in worker.json. */
3
+ export declare function isWorkerProcessLive(indexed: IndexedWorktree): boolean;
4
+ /**
5
+ * Run record still marked active but every worker is finished — safe to treat as
6
+ * terminal for GC after `finalizeStaleRuns()` (or when deriveTerminalRunStatus is set).
7
+ */
8
+ export declare function isRunStaleActive(indexed: IndexedWorktree): boolean;
9
+ /**
10
+ * Whether the harness run still has unfinished work that should block whole-worktree removal.
11
+ * Does not block per-worker `node_modules` when only this worker is finished.
12
+ */
13
+ export declare function runBlocksWorktreeRemoval(indexed: IndexedWorktree): boolean;
@@ -15,6 +15,11 @@ export interface CleanupAction extends CleanupCandidate {
15
15
  skipReason?: CleanupSkipReason;
16
16
  error?: string;
17
17
  }
18
+ export interface RunFinalizeSummary {
19
+ runId: string;
20
+ from: string;
21
+ to: string;
22
+ }
18
23
  export interface HarnessCleanupSummary {
19
24
  harnessRoot: string;
20
25
  dryRun: boolean;
@@ -23,6 +28,7 @@ export interface HarnessCleanupSummary {
23
28
  worktreesAgeMs: number;
24
29
  includeOrphans: boolean;
25
30
  scannedAt: string;
31
+ finalizedRuns: RunFinalizeSummary[];
26
32
  actions: CleanupAction[];
27
33
  skips: Array<{
28
34
  path: string;
@@ -31,15 +37,21 @@ export interface HarnessCleanupSummary {
31
37
  }>;
32
38
  totals: {
33
39
  candidateBytes: number;
40
+ reclaimableBytes: number;
34
41
  removedBytes: number;
35
42
  removedPaths: number;
36
43
  skippedPaths: number;
44
+ skipReasons: Partial<Record<CleanupSkipReason, number>>;
37
45
  };
38
46
  }
39
47
  export interface HarnessCleanupOptions {
40
48
  harnessRoot?: string;
41
49
  /** When false (default), only report candidates. */
42
50
  execute?: boolean;
51
+ /** When true (default), call `finalizeStaleRuns()` before scanning. */
52
+ finalizeStaleRuns?: boolean;
53
+ /** When true (default), estimate candidate bytes in dry-run summaries. */
54
+ accountBytes?: boolean;
43
55
  /** Minimum age before removing generated `node_modules` (default 6h). */
44
56
  nodeModulesAgeMs?: number;
45
57
  /** When 0 or unset, worktree removal is disabled. */
package/dist/cleanup.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { type HarnessCleanupOptions, type HarnessCleanupSummary } from "./cleanup-types.js";
2
2
  export declare function runHarnessCleanup(options?: HarnessCleanupOptions): HarnessCleanupSummary;
3
- /** Pipeline-safe defaults: node_modules only, dry-run unless execute env is set. */
3
+ /** Pipeline-safe defaults: finalize stale runs, dry-run unless execute env is set. */
4
4
  export declare function runPipelineHarnessCleanup(runId?: string): HarnessCleanupSummary;
5
5
  export declare function isPipelineCleanupEnabled(): boolean;
package/dist/cli.js CHANGED
@@ -4813,8 +4813,15 @@ import path24 from "node:path";
4813
4813
 
4814
4814
  // src/finalize.ts
4815
4815
  import path23 from "node:path";
4816
- var ACTIVE_RUN_STATUSES = /* @__PURE__ */ new Set(["running", "dispatching", "pending", "queued"]);
4817
- function terminalStatusFor(run) {
4816
+ var ACTIVE_RUN_STATUSES = /* @__PURE__ */ new Set([
4817
+ "running",
4818
+ "dispatching",
4819
+ "pending",
4820
+ "queued",
4821
+ "needs_attention"
4822
+ ]);
4823
+ var TERMINAL_RUN_STATUSES = /* @__PURE__ */ new Set(["completed", "failed", "cancelled", "done"]);
4824
+ function deriveTerminalRunStatus(run) {
4818
4825
  const names = Object.keys(run.workers || {});
4819
4826
  if (names.length === 0) return "failed";
4820
4827
  let anyAlive = false;
@@ -4852,7 +4859,7 @@ function finalizeStaleRuns() {
4852
4859
  const finalized = [];
4853
4860
  for (const run of listRunRecords()) {
4854
4861
  if (!ACTIVE_RUN_STATUSES.has(run.status)) continue;
4855
- const next = terminalStatusFor(run);
4862
+ const next = deriveTerminalRunStatus(run);
4856
4863
  if (!next || next === run.status) continue;
4857
4864
  const from = run.status;
4858
4865
  run.status = next;
@@ -5044,13 +5051,26 @@ async function fetchWorkspaceRuntimePreferences(agentOsId, args) {
5044
5051
  // src/cleanup.ts
5045
5052
  import path30 from "node:path";
5046
5053
 
5047
- // src/cleanup-types.ts
5048
- var DEFAULT_NODE_MODULES_AGE_MS = 6 * 60 * 60 * 1e3;
5049
- var DEFAULT_WORKTREES_AGE_MS = 7 * 24 * 60 * 60 * 1e3;
5054
+ // src/cleanup-run-liveness.ts
5055
+ function isWorkerProcessLive(indexed) {
5056
+ if (indexed.status.alive) return true;
5057
+ if (indexed.worker.status === "running") return true;
5058
+ return false;
5059
+ }
5060
+ function isRunStaleActive(indexed) {
5061
+ if (TERMINAL_RUN_STATUSES.has(indexed.run.status)) return false;
5062
+ return deriveTerminalRunStatus(indexed.run) !== null;
5063
+ }
5064
+ function runBlocksWorktreeRemoval(indexed) {
5065
+ if (isWorkerProcessLive(indexed)) return true;
5066
+ if (indexed.worker.completionBlocker) return true;
5067
+ if (TERMINAL_RUN_STATUSES.has(indexed.run.status)) return false;
5068
+ if (isRunStaleActive(indexed)) return false;
5069
+ if (!isFinishedWorkerStatus(indexed.status)) return true;
5070
+ return deriveTerminalRunStatus(indexed.run) === null;
5071
+ }
5050
5072
 
5051
5073
  // src/cleanup-guards.ts
5052
- var ACTIVE_RUN_STATUSES2 = /* @__PURE__ */ new Set(["running", "dispatching", "pending", "queued", "needs_attention"]);
5053
- var TERMINAL_RUN_STATUSES = /* @__PURE__ */ new Set(["completed", "failed", "cancelled"]);
5054
5074
  function prUrlFromFinalResult(finalResult) {
5055
5075
  if (typeof finalResult === "string") {
5056
5076
  const match = finalResult.match(/https:\/\/github\.com\/[^\s]+\/pull\/\d+/i);
@@ -5072,18 +5092,29 @@ function isPrOrUnmergedWork(status) {
5072
5092
  if (status.changedFiles.length > 0 && status.finalResult) return true;
5073
5093
  return false;
5074
5094
  }
5095
+ function materialWorktreeChanges(changedFiles) {
5096
+ return changedFiles.filter((line) => {
5097
+ const trimmed = line.trim();
5098
+ const pathPart = trimmed.startsWith("??") ? trimmed.slice(2).trim() : trimmed.length > 3 ? trimmed.slice(3).trim() : trimmed;
5099
+ return pathPart !== "node_modules" && !pathPart.startsWith("node_modules/");
5100
+ });
5101
+ }
5102
+ function hasUnrestorableWorktreeChanges(status) {
5103
+ if (materialWorktreeChanges(status.changedFiles).length > 0) return true;
5104
+ if (status.gitAncestry?.relation === "diverged") return true;
5105
+ return false;
5106
+ }
5075
5107
  function skipWorktreeRemoval(input) {
5076
5108
  const { indexed, includeOrphans, worktreesAgeMs, ageMs } = input;
5077
5109
  if (worktreesAgeMs <= 0) return "worktrees_disabled";
5078
5110
  if (ageMs < worktreesAgeMs) return "below_age_threshold";
5079
5111
  if (!indexed) return includeOrphans ? null : "orphan_without_flag";
5080
- if (ACTIVE_RUN_STATUSES2.has(indexed.run.status)) return "run_still_active";
5081
- if (!TERMINAL_RUN_STATUSES.has(indexed.run.status)) return "run_still_active";
5082
- if (indexed.status.alive) return "active_worker";
5083
- if (indexed.worker.status === "running") return "active_worker";
5112
+ if (isWorkerProcessLive(indexed)) return "active_worker";
5084
5113
  if (indexed.worker.completionBlocker) return "completion_blocked";
5114
+ if (runBlocksWorktreeRemoval(indexed)) return "run_still_active";
5115
+ if (!isFinishedWorkerStatus(indexed.status)) return "run_still_active";
5085
5116
  if (isPrOrUnmergedWork(indexed.status)) return "pr_or_unmerged_commits";
5086
- if (indexed.status.changedFiles.length > 0) return "dirty_worktree";
5117
+ if (materialWorktreeChanges(indexed.status.changedFiles).length > 0) return "dirty_worktree";
5087
5118
  const landing = assessWorkerLanding({
5088
5119
  finalResult: indexed.status.finalResult,
5089
5120
  changedFiles: indexed.status.changedFiles,
@@ -5097,18 +5128,19 @@ function skipNodeModulesRemoval(input) {
5097
5128
  const { indexed, includeOrphans, nodeModulesAgeMs, ageMs } = input;
5098
5129
  if (ageMs < nodeModulesAgeMs) return "below_age_threshold";
5099
5130
  if (!indexed) return includeOrphans ? null : "orphan_without_flag";
5100
- if (indexed.status.alive) return "active_worker";
5101
- if (indexed.worker.status === "running") return "active_worker";
5131
+ if (isWorkerProcessLive(indexed)) return "active_worker";
5102
5132
  if (indexed.worker.completionBlocker) return "completion_blocked";
5103
- if (isPrOrUnmergedWork(indexed.status)) return "pr_or_unmerged_commits";
5104
- if (indexed.status.changedFiles.length > 0) return "dirty_worktree";
5133
+ if (!isFinishedWorkerStatus(indexed.status)) return "run_still_active";
5134
+ if (hasUnrestorableWorktreeChanges(indexed.status)) return "dirty_worktree";
5105
5135
  const landing = assessWorkerLanding({
5106
5136
  finalResult: indexed.status.finalResult,
5107
5137
  changedFiles: indexed.status.changedFiles,
5108
5138
  gitAncestry: indexed.status.gitAncestry,
5109
5139
  prUrl: prUrlFromFinalResult(indexed.status.finalResult)
5110
5140
  });
5111
- if (landing.blocked) return "landing_blocked";
5141
+ if (landing.blocked && materialWorktreeChanges(indexed.status.changedFiles).length > 0) {
5142
+ return "landing_blocked";
5143
+ }
5112
5144
  return null;
5113
5145
  }
5114
5146
 
@@ -5335,48 +5367,98 @@ function buildWorktreeIndex() {
5335
5367
  return index;
5336
5368
  }
5337
5369
 
5338
- // src/cleanup.ts
5339
- function resolveOptions(options = {}) {
5340
- const harnessRoot = options.harnessRoot ? resolveUserPath(options.harnessRoot) : resolveHarnessRoot();
5341
- const { worktreesDir } = options.harnessRoot ? { worktreesDir: path30.join(harnessRoot, "worktrees") } : getHarnessPaths();
5342
- const execute = options.execute === true;
5343
- const nodeModulesAgeMs = options.nodeModulesAgeMs ?? DEFAULT_NODE_MODULES_AGE_MS;
5344
- const worktreesAgeMs = options.worktreesAgeMs ?? 0;
5345
- const includeOrphans = options.includeOrphans === true;
5346
- const runIdFilter = options.runIdFilter ? String(options.runIdFilter) : void 0;
5347
- const now = options.now ?? Date.now();
5370
+ // src/cleanup-types.ts
5371
+ var DEFAULT_NODE_MODULES_AGE_MS = 6 * 60 * 60 * 1e3;
5372
+ var DEFAULT_WORKTREES_AGE_MS = 7 * 24 * 60 * 60 * 1e3;
5373
+
5374
+ // src/cleanup-retention-config.ts
5375
+ function envFlag(name) {
5376
+ const v = process.env[name];
5377
+ return v === "1" || v === "true" || v === "yes";
5378
+ }
5379
+ function envMs(name, fallback) {
5380
+ const raw = process.env[name];
5381
+ if (!raw) return fallback;
5382
+ const n = Number(raw);
5383
+ return Number.isFinite(n) && n >= 0 ? n : fallback;
5384
+ }
5385
+ function resolveHarnessRetention(options = {}) {
5386
+ const execute = options.execute === true || options.execute !== false && envFlag("KYNVER_CLEANUP_EXECUTE");
5387
+ const finalizeStaleRuns2 = options.finalizeStaleRuns !== false && !envFlag("KYNVER_CLEANUP_SKIP_FINALIZE");
5388
+ const nodeModulesAgeMs = options.nodeModulesAgeMs ?? envMs("KYNVER_CLEANUP_NODE_MODULES_AGE_MS", DEFAULT_NODE_MODULES_AGE_MS);
5389
+ const worktreesAgeMs = options.worktreesAgeMs ?? envMs("KYNVER_CLEANUP_WORKTREES_AGE_MS", 0);
5390
+ const includeOrphans = options.includeOrphans === true || envFlag("KYNVER_CLEANUP_INCLUDE_ORPHANS");
5391
+ const scopeAll = envFlag("KYNVER_CLEANUP_SCOPE_ALL") || process.env.KYNVER_CLEANUP_SCOPE === "all";
5392
+ const runIdFilter = scopeAll ? options.runIdFilter : options.runIdFilter ?? (process.env.KYNVER_CLEANUP_RUN_ID || void 0);
5393
+ const accountBytes = options.accountBytes !== false && !envFlag("KYNVER_CLEANUP_SKIP_BYTE_ACCOUNTING");
5348
5394
  return {
5349
- harnessRoot,
5350
- worktreesDir,
5351
5395
  execute,
5352
- dryRun: !execute,
5396
+ finalizeStaleRuns: finalizeStaleRuns2,
5353
5397
  nodeModulesAgeMs,
5354
- worktreesAgeMs,
5398
+ worktreesAgeMs: worktreesAgeMs > 0 ? worktreesAgeMs : 0,
5355
5399
  includeOrphans,
5356
- runIdFilter,
5357
- now
5400
+ runIdFilter: runIdFilter ? String(runIdFilter) : void 0,
5401
+ accountBytes
5358
5402
  };
5359
5403
  }
5404
+ function resolvePipelineHarnessRetention(runId) {
5405
+ const scopeAll = process.env.KYNVER_CLEANUP_SCOPE === "all";
5406
+ const envWorktrees = Number(process.env.KYNVER_CLEANUP_WORKTREES_AGE_MS);
5407
+ const worktreesAgeMs = scopeAll ? Number.isFinite(envWorktrees) && envWorktrees > 0 ? envWorktrees : DEFAULT_WORKTREES_AGE_MS : Number.isFinite(envWorktrees) && envWorktrees > 0 ? envWorktrees : 0;
5408
+ return resolveHarnessRetention({
5409
+ runIdFilter: scopeAll ? void 0 : runId,
5410
+ worktreesAgeMs,
5411
+ finalizeStaleRuns: true,
5412
+ accountBytes: true
5413
+ });
5414
+ }
5415
+
5416
+ // src/cleanup.ts
5417
+ function resolvePaths(options = {}) {
5418
+ const harnessRoot = options.harnessRoot ? resolveUserPath(options.harnessRoot) : resolveHarnessRoot();
5419
+ const { worktreesDir } = options.harnessRoot ? { worktreesDir: path30.join(harnessRoot, "worktrees") } : getHarnessPaths();
5420
+ const now = options.now ?? Date.now();
5421
+ return { harnessRoot, worktreesDir, now };
5422
+ }
5360
5423
  function recordSkip(skips, pathValue, reason, detail) {
5361
5424
  skips.push({ path: pathValue, reason, ...detail ? { detail } : {} });
5362
5425
  }
5426
+ function attachCandidateBytes(candidate, accountBytes) {
5427
+ if (!accountBytes || candidate.bytes != null) return candidate;
5428
+ return { ...candidate, bytes: directorySizeBytes(candidate.path) };
5429
+ }
5430
+ function tallySkipReasons(actions, skips) {
5431
+ const counts = {};
5432
+ for (const skip of skips) {
5433
+ counts[skip.reason] = (counts[skip.reason] ?? 0) + 1;
5434
+ }
5435
+ for (const action of actions) {
5436
+ if (action.skipReason) {
5437
+ counts[action.skipReason] = (counts[action.skipReason] ?? 0) + 1;
5438
+ }
5439
+ }
5440
+ return counts;
5441
+ }
5363
5442
  function runHarnessCleanup(options = {}) {
5364
- const resolved = resolveOptions(options);
5443
+ const retention = resolveHarnessRetention(options);
5444
+ const paths = resolvePaths(options);
5445
+ const finalizedRuns = retention.finalizeStaleRuns ? finalizeStaleRuns().map((f) => ({ runId: f.runId, from: f.from, to: f.to })) : [];
5365
5446
  const index = buildWorktreeIndex();
5366
5447
  const scanOpts = {
5367
- harnessRoot: resolved.harnessRoot,
5368
- worktreesDir: resolved.worktreesDir,
5369
- nodeModulesAgeMs: resolved.nodeModulesAgeMs,
5370
- worktreesAgeMs: resolved.worktreesAgeMs,
5371
- includeOrphans: resolved.includeOrphans,
5372
- runIdFilter: resolved.runIdFilter,
5448
+ harnessRoot: paths.harnessRoot,
5449
+ worktreesDir: paths.worktreesDir,
5450
+ nodeModulesAgeMs: retention.nodeModulesAgeMs,
5451
+ worktreesAgeMs: retention.worktreesAgeMs,
5452
+ includeOrphans: retention.includeOrphans,
5453
+ runIdFilter: retention.runIdFilter,
5373
5454
  index,
5374
- now: resolved.now
5455
+ now: paths.now
5375
5456
  };
5376
5457
  const skips = [];
5377
5458
  const actions = [];
5378
- for (const candidate of scanNodeModulesCandidates(scanOpts)) {
5379
- const pathSkip = isHarnessNodeModulesPath(candidate.path, resolved.harnessRoot, resolved.worktreesDir);
5459
+ for (const raw of scanNodeModulesCandidates(scanOpts)) {
5460
+ const candidate = attachCandidateBytes(raw, retention.accountBytes);
5461
+ const pathSkip = isHarnessNodeModulesPath(candidate.path, paths.harnessRoot, paths.worktreesDir);
5380
5462
  if (pathSkip) {
5381
5463
  recordSkip(skips, candidate.path, pathSkip);
5382
5464
  actions.push({ ...candidate, executed: false, skipped: true, skipReason: pathSkip });
@@ -5386,8 +5468,8 @@ function runHarnessCleanup(options = {}) {
5386
5468
  const indexed = index.get(worktreePath) ?? null;
5387
5469
  const guardReason = skipNodeModulesRemoval({
5388
5470
  indexed,
5389
- includeOrphans: resolved.includeOrphans,
5390
- nodeModulesAgeMs: resolved.nodeModulesAgeMs,
5471
+ includeOrphans: retention.includeOrphans,
5472
+ nodeModulesAgeMs: retention.nodeModulesAgeMs,
5391
5473
  ageMs: candidate.ageMs
5392
5474
  });
5393
5475
  if (guardReason) {
@@ -5395,14 +5477,15 @@ function runHarnessCleanup(options = {}) {
5395
5477
  actions.push({ ...candidate, executed: false, skipped: true, skipReason: guardReason });
5396
5478
  continue;
5397
5479
  }
5398
- actions.push(removeNodeModules(candidate, resolved.execute));
5480
+ actions.push(removeNodeModules(candidate, retention.execute));
5399
5481
  }
5400
- for (const candidate of scanWorktreeCandidates(scanOpts)) {
5482
+ for (const raw of scanWorktreeCandidates(scanOpts)) {
5483
+ const candidate = attachCandidateBytes(raw, retention.accountBytes);
5401
5484
  const indexed = index.get(path30.resolve(candidate.path)) ?? null;
5402
5485
  const guardReason = skipWorktreeRemoval({
5403
5486
  indexed,
5404
- includeOrphans: resolved.includeOrphans,
5405
- worktreesAgeMs: resolved.worktreesAgeMs,
5487
+ includeOrphans: retention.includeOrphans,
5488
+ worktreesAgeMs: retention.worktreesAgeMs,
5406
5489
  ageMs: candidate.ageMs
5407
5490
  });
5408
5491
  if (guardReason) {
@@ -5410,51 +5493,55 @@ function runHarnessCleanup(options = {}) {
5410
5493
  actions.push({ ...candidate, executed: false, skipped: true, skipReason: guardReason });
5411
5494
  continue;
5412
5495
  }
5413
- actions.push(removeWorktree(candidate, resolved.execute));
5496
+ actions.push(removeWorktree(candidate, retention.execute));
5414
5497
  }
5415
5498
  let candidateBytes = 0;
5499
+ let reclaimableBytes = 0;
5416
5500
  let removedBytes = 0;
5417
5501
  let removedPaths = 0;
5418
5502
  let skippedPaths = 0;
5419
5503
  for (const action of actions) {
5420
5504
  if (action.bytes) candidateBytes += action.bytes;
5505
+ if (!action.skipped && !action.executed && action.bytes) reclaimableBytes += action.bytes;
5421
5506
  if (action.executed) {
5422
5507
  removedPaths += 1;
5423
5508
  removedBytes += action.bytes ?? 0;
5424
5509
  } else if (action.skipped) {
5425
5510
  skippedPaths += 1;
5511
+ if (action.skipReason === "dry_run" && action.bytes) reclaimableBytes += action.bytes;
5426
5512
  }
5427
5513
  }
5428
5514
  return {
5429
- harnessRoot: resolved.harnessRoot,
5430
- dryRun: resolved.dryRun,
5431
- execute: resolved.execute,
5432
- nodeModulesAgeMs: resolved.nodeModulesAgeMs,
5433
- worktreesAgeMs: resolved.worktreesAgeMs,
5434
- includeOrphans: resolved.includeOrphans,
5435
- scannedAt: new Date(resolved.now).toISOString(),
5515
+ harnessRoot: paths.harnessRoot,
5516
+ dryRun: !retention.execute,
5517
+ execute: retention.execute,
5518
+ nodeModulesAgeMs: retention.nodeModulesAgeMs,
5519
+ worktreesAgeMs: retention.worktreesAgeMs,
5520
+ includeOrphans: retention.includeOrphans,
5521
+ scannedAt: new Date(paths.now).toISOString(),
5522
+ finalizedRuns,
5436
5523
  actions,
5437
5524
  skips,
5438
5525
  totals: {
5439
5526
  candidateBytes,
5527
+ reclaimableBytes,
5440
5528
  removedBytes,
5441
5529
  removedPaths,
5442
- skippedPaths
5530
+ skippedPaths,
5531
+ skipReasons: tallySkipReasons(actions, skips)
5443
5532
  }
5444
5533
  };
5445
5534
  }
5446
5535
  function runPipelineHarnessCleanup(runId) {
5447
- const nodeModulesAgeMs = Number(process.env.KYNVER_CLEANUP_NODE_MODULES_AGE_MS) || DEFAULT_NODE_MODULES_AGE_MS;
5448
- const worktreesAgeMs = Number(process.env.KYNVER_CLEANUP_WORKTREES_AGE_MS) || 0;
5449
- const execute = process.env.KYNVER_CLEANUP_EXECUTE === "1";
5450
- const includeOrphans = process.env.KYNVER_CLEANUP_INCLUDE_ORPHANS === "1";
5451
- const scopeAll = process.env.KYNVER_CLEANUP_SCOPE === "all";
5536
+ const retention = resolvePipelineHarnessRetention(runId);
5452
5537
  return runHarnessCleanup({
5453
- execute,
5454
- nodeModulesAgeMs,
5455
- worktreesAgeMs,
5456
- includeOrphans,
5457
- runIdFilter: scopeAll ? void 0 : runId
5538
+ execute: retention.execute,
5539
+ finalizeStaleRuns: retention.finalizeStaleRuns,
5540
+ accountBytes: retention.accountBytes,
5541
+ nodeModulesAgeMs: retention.nodeModulesAgeMs,
5542
+ worktreesAgeMs: retention.worktreesAgeMs,
5543
+ includeOrphans: retention.includeOrphans,
5544
+ runIdFilter: retention.runIdFilter
5458
5545
  });
5459
5546
  }
5460
5547
  function isPipelineCleanupEnabled() {
@@ -6121,12 +6208,14 @@ async function runPlanOutboxDrain(args) {
6121
6208
  // src/cleanup-cli.ts
6122
6209
  function runCleanupCli(args) {
6123
6210
  const execute = args.execute === true || args.execute === "true";
6211
+ const skipFinalize = args.skipFinalize === true || args.skipFinalize === "true";
6124
6212
  const nodeModulesAgeMs = args.nodeModulesAgeMs ? Number(args.nodeModulesAgeMs) : DEFAULT_NODE_MODULES_AGE_MS;
6125
6213
  const worktreesAgeMs = args.worktreesAgeMs ? Number(args.worktreesAgeMs) : 0;
6126
6214
  const includeOrphans = args.includeOrphans === true || args.includeOrphans === "true";
6127
6215
  const harnessRoot = args.harnessRoot ? String(args.harnessRoot) : void 0;
6128
6216
  const summary = runHarnessCleanup({
6129
6217
  execute,
6218
+ finalizeStaleRuns: !skipFinalize,
6130
6219
  nodeModulesAgeMs: Number.isFinite(nodeModulesAgeMs) ? nodeModulesAgeMs : DEFAULT_NODE_MODULES_AGE_MS,
6131
6220
  worktreesAgeMs: Number.isFinite(worktreesAgeMs) ? worktreesAgeMs : 0,
6132
6221
  includeOrphans,
@@ -7281,7 +7370,7 @@ function usage(code = 0) {
7281
7370
  " kynver plan persist --operation create|add_version|update_metadata --title TITLE (--body-file PATH | --body TEXT) [--slug SLUG] [--plan PLAN_ID] [--summary TEXT] [--failure-kind approval_guard|auth|network|server|tool_interruption]",
7282
7371
  " kynver plan outbox list",
7283
7372
  " kynver plan outbox drain [--max N] [--id OUTBOX_ID]",
7284
- " kynver cleanup [--execute] [--node-modules-age-ms MS] [--worktrees-age-ms MS] [--harness-root PATH] [--include-orphans]",
7373
+ " kynver cleanup [--execute] [--node-modules-age-ms MS] [--worktrees-age-ms MS] [--harness-root PATH] [--include-orphans] [--skip-finalize]",
7285
7374
  " kynver monitor start --run RUN_ID [--name worker] [--agent-os-id AOS_ID] [--poll-ms MS]",
7286
7375
  " kynver monitor status [--run RUN_ID] [--name worker] [--tick]",
7287
7376
  " kynver monitor stop --run RUN_ID [--name worker]",