@kynver-app/runtime 0.1.103 → 0.1.105

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.
Files changed (45) hide show
  1. package/dist/cleanup-completion-blocker.d.ts +10 -0
  2. package/dist/cleanup-guards.d.ts +1 -6
  3. package/dist/cleanup-worktree-salvage.d.ts +7 -0
  4. package/dist/cli.js +166 -59
  5. package/dist/cli.js.map +4 -4
  6. package/dist/index.js +166 -59
  7. package/dist/index.js.map +4 -4
  8. package/dist/server/cleanup.d.ts +3 -0
  9. package/dist/server/cleanup.js +3511 -0
  10. package/dist/server/cleanup.js.map +7 -0
  11. package/dist/server/default-repo.d.ts +1 -0
  12. package/dist/server/default-repo.js +228 -0
  13. package/dist/server/default-repo.js.map +7 -0
  14. package/dist/server/harness-notice.d.ts +2 -0
  15. package/dist/server/harness-notice.js +287 -0
  16. package/dist/server/harness-notice.js.map +7 -0
  17. package/dist/server/heavy-verification.d.ts +2 -0
  18. package/dist/server/heavy-verification.js +223 -0
  19. package/dist/server/heavy-verification.js.map +7 -0
  20. package/dist/server/landing.d.ts +1 -0
  21. package/dist/server/landing.js +44 -0
  22. package/dist/server/landing.js.map +7 -0
  23. package/dist/server/memory-cost-enforce.d.ts +1 -0
  24. package/dist/server/memory-cost-enforce.js +470 -0
  25. package/dist/server/memory-cost-enforce.js.map +7 -0
  26. package/dist/server/memory-cost.d.ts +1 -0
  27. package/dist/server/memory-cost.js +184 -0
  28. package/dist/server/memory-cost.js.map +7 -0
  29. package/dist/server/monitor.d.ts +3 -0
  30. package/dist/server/monitor.js +1577 -0
  31. package/dist/server/monitor.js.map +7 -0
  32. package/dist/server/orchestration.d.ts +10 -0
  33. package/dist/server/orchestration.js +444 -0
  34. package/dist/server/orchestration.js.map +7 -0
  35. package/dist/server/pr-evidence.d.ts +2 -0
  36. package/dist/server/pr-evidence.js +163 -0
  37. package/dist/server/pr-evidence.js.map +7 -0
  38. package/dist/server/repo-search.d.ts +1 -0
  39. package/dist/server/repo-search.js +224 -0
  40. package/dist/server/repo-search.js.map +7 -0
  41. package/dist/server/worker-policy.d.ts +2 -0
  42. package/dist/server/worker-policy.js +177 -0
  43. package/dist/server/worker-policy.js.map +7 -0
  44. package/dist/status.d.ts +4 -0
  45. package/package.json +63 -3
@@ -0,0 +1,10 @@
1
+ import type { IndexedWorktree } from "./cleanup-worktree-index.js";
2
+ import type { RawHarnessWorkerStatus } from "./status.js";
3
+ /**
4
+ * Whether a persisted `completionBlocker` should still block whole-worktree removal.
5
+ *
6
+ * Dead workers with landed/clean work may keep replay metadata on disk after the
7
+ * board advanced externally — those blockers are stale for GC and must not pin
8
+ * worktrees indefinitely.
9
+ */
10
+ export declare function completionBlockerBlocksWorktreeRemoval(indexed: IndexedWorktree, status?: RawHarnessWorkerStatus): boolean;
@@ -1,13 +1,8 @@
1
1
  import type { CleanupSkipReason, WorktreeRemovalGuardHook } from "./cleanup-types.js";
2
2
  import type { IndexedWorktree } from "./cleanup-worktree-index.js";
3
3
  import type { CleanupRunLivenessContext } from "./cleanup-run-liveness.js";
4
- import type { GitAncestry } from "./git.js";
5
- import type { RawHarnessWorkerStatus } from "./status.js";
6
4
  export { materialWorktreeChanges } from "./cleanup-guards-helpers.js";
7
- /** True when git ancestry shows the worker branch is fully landed on the run base. */
8
- export declare function isLandedGitAncestry(ancestry: GitAncestry | null | undefined): boolean;
9
- /** Blocks whole-worktree removal when commits are not landed or tree is dirty. */
10
- export declare function isPrOrUnmergedWork(status: RawHarnessWorkerStatus): boolean;
5
+ export { isLandedGitAncestry, isPrOrUnmergedWork } from "./cleanup-worktree-salvage.js";
11
6
  export interface WorktreeGuardInput {
12
7
  indexed: IndexedWorktree | null;
13
8
  /** Resolved worktree directory (required for overlay guards on orphans). */
@@ -0,0 +1,7 @@
1
+ import type { GitAncestry } from "./git.js";
2
+ import type { RawHarnessWorkerStatus } from "./status.js";
3
+ export declare function prUrlFromFinalResult(finalResult: unknown): string | null;
4
+ /** True when git ancestry shows the worker branch is fully landed on the run base. */
5
+ export declare function isLandedGitAncestry(ancestry: GitAncestry | null | undefined): boolean;
6
+ /** Blocks whole-worktree removal when commits are not landed or tree is dirty. */
7
+ export declare function isPrOrUnmergedWork(status: RawHarnessWorkerStatus): boolean;
package/dist/cli.js CHANGED
@@ -1783,7 +1783,7 @@ var NO_START_MS = 18e4;
1783
1783
  var STALE_MS = 6e5;
1784
1784
  function computeAttention(input) {
1785
1785
  const now = Date.now();
1786
- if (input.completionBlocker) {
1786
+ if (input.completionBlocker && !isSkippedTerminalCompletionBlocker(input.completionBlocker)) {
1787
1787
  return { state: "blocked", reason: input.completionBlocker };
1788
1788
  }
1789
1789
  if (input.finalResult) {
@@ -1821,6 +1821,9 @@ function computeAttention(input) {
1821
1821
  return { state: "done", reason: "final result recorded" };
1822
1822
  }
1823
1823
  if (!input.alive) {
1824
+ if (isAbandonedEmptyWorker(input)) {
1825
+ return { state: "done", reason: "empty abandoned worker record" };
1826
+ }
1824
1827
  const classified = classifyExitFailure(input.error);
1825
1828
  if (classified) return { state: "blocked", reason: classified.reason };
1826
1829
  const salvage = assessExitedWorkerSalvage({
@@ -1855,6 +1858,19 @@ function computeAttention(input) {
1855
1858
  }
1856
1859
  return { state: "ok", reason: "recent activity" };
1857
1860
  }
1861
+ function isSkippedTerminalCompletionBlocker(reason) {
1862
+ const text = reason?.trim();
1863
+ if (!text) return false;
1864
+ return /completion acknowledged but board not advanced/i.test(text) && /task already terminal/i.test(text);
1865
+ }
1866
+ function isAbandonedEmptyWorker(input) {
1867
+ if (input.finalResult) return false;
1868
+ if (input.taskId || input.agentOsId) return false;
1869
+ if (input.stdoutBytes > 0 || (input.stderrBytes ?? 0) > 0 || input.heartbeatBytes > 0) return false;
1870
+ if (input.error?.trim()) return false;
1871
+ if ((input.changedFiles ?? []).some((line) => line.trim())) return false;
1872
+ return /empty worker dir|marked abandoned/i.test(input.reconcileReason ?? "");
1873
+ }
1858
1874
  function hasMergedTargetPrReconciliation(value) {
1859
1875
  let record = null;
1860
1876
  if (typeof value === "string") record = extractEmbeddedWorkerFinalResultRecord(value);
@@ -1899,6 +1915,7 @@ function computeWorkerStatus(worker, options = {}) {
1899
1915
  ]);
1900
1916
  const error = parsed.error || (!alive && !finalResult ? tailFile(worker.stderrPath, 10).trim() || void 0 : void 0);
1901
1917
  const completionBlocker = typeof worker.completionBlocker === "string" && worker.completionBlocker.trim() ? worker.completionBlocker.trim() : null;
1918
+ const effectiveCompletionBlocker = isSkippedTerminalCompletionBlocker(completionBlocker) ? null : completionBlocker;
1902
1919
  const landingContract = worker.repairTargetPrUrl ? {
1903
1920
  landingOnly: false,
1904
1921
  targetPrUrls: [worker.repairTargetPrUrl],
@@ -1910,6 +1927,7 @@ function computeWorkerStatus(worker, options = {}) {
1910
1927
  finalResult,
1911
1928
  firstEventAt: parsed.firstEventAt,
1912
1929
  stdoutBytes,
1930
+ stderrBytes,
1913
1931
  heartbeatBytes,
1914
1932
  lastActivityAt,
1915
1933
  heartbeatBlocker: heartbeat.heartbeatBlocker,
@@ -1917,12 +1935,15 @@ function computeWorkerStatus(worker, options = {}) {
1917
1935
  error,
1918
1936
  changedFiles,
1919
1937
  gitAncestry,
1920
- completionBlocker,
1938
+ completionBlocker: effectiveCompletionBlocker,
1921
1939
  landingContract,
1922
1940
  prUrl: worker.repairTargetPrUrl ?? worker.taskPrUrl ?? null,
1923
- localOnly: worker.localOnly === true
1941
+ localOnly: worker.localOnly === true,
1942
+ taskId: worker.taskId ?? null,
1943
+ agentOsId: worker.agentOsId ?? null,
1944
+ reconcileReason: worker.reconcileReason ?? null
1924
1945
  });
1925
- const workerStatusLabel = completionBlocker || attention.state === "blocked" ? "blocked" : completionAcknowledged || attention.state === "done" ? "done" : finalResult ? "exited" : alive ? "running" : "exited";
1946
+ const workerStatusLabel = effectiveCompletionBlocker || attention.state === "blocked" ? "blocked" : completionAcknowledged || attention.state === "done" ? "done" : finalResult ? "exited" : alive ? "running" : "exited";
1926
1947
  return {
1927
1948
  runId: worker.runId,
1928
1949
  worker: worker.name,
@@ -2839,6 +2860,9 @@ function enforceCursorWorkerProvider(input) {
2839
2860
  if (taskAllowsClaudeWorker(task)) {
2840
2861
  return routing;
2841
2862
  }
2863
+ if (routing.rule === "explicit:cli" && isClaudeFamilyProvider(routing.provider)) {
2864
+ return routing;
2865
+ }
2842
2866
  if (!isClaudeFamilyProvider(routing.provider)) {
2843
2867
  return routing;
2844
2868
  }
@@ -3872,7 +3896,7 @@ function resolveWorkerLaunch(input) {
3872
3896
  if (!input.task || Object.keys(input.task).length === 0) {
3873
3897
  return afterCursorPolicy;
3874
3898
  }
3875
- if (afterCursorPolicy.rule === "explicit:model_provider_alias" || afterCursorPolicy.rule === "explicit:model_provider_alias_overrode_provider") {
3899
+ if (afterCursorPolicy.rule === "explicit:model_provider_alias" || afterCursorPolicy.rule === "explicit:model_provider_alias_overrode_provider" || afterCursorPolicy.rule === "explicit:cli") {
3876
3900
  return afterCursorPolicy;
3877
3901
  }
3878
3902
  if (isClaudeFamilyProvider(afterCursorPolicy.provider) && (input.explicitProviderIsOperatorOverride || taskAllowsClaudeWorker(input.task))) {
@@ -4486,6 +4510,7 @@ function asString(value) {
4486
4510
  var ADVANCED_OUTCOMES = /* @__PURE__ */ new Set([
4487
4511
  "review_scheduled",
4488
4512
  "review_already_scheduled",
4513
+ "skipped_terminal_task",
4489
4514
  "closed",
4490
4515
  "dispatched",
4491
4516
  "dispatch_already_done"
@@ -4833,6 +4858,36 @@ function defaultPrBody(taskId, workerName, runId) {
4833
4858
  "Opened by orchestrator completion enforcement so production review receives a reviewable artifact."
4834
4859
  ].filter(Boolean).join("\n");
4835
4860
  }
4861
+ function commitDirtyToExistingPr(input) {
4862
+ if (input.snapshot.changedFiles.length === 0) {
4863
+ return {
4864
+ ok: true,
4865
+ prUrl: input.prUrl,
4866
+ headCommit: input.snapshot.headCommit ?? resolveHeadCommit(input.snapshot.worktreePath, input.exec) ?? void 0
4867
+ };
4868
+ }
4869
+ const pushResult = commitAndPushBranch({
4870
+ worktreePath: input.snapshot.worktreePath,
4871
+ branch: input.snapshot.branch,
4872
+ commitMessage: input.commitMessage,
4873
+ hasDirtyFiles: true,
4874
+ exec: input.exec
4875
+ });
4876
+ if (!pushResult.ok) {
4877
+ return {
4878
+ ok: false,
4879
+ reason: `PR-ready handoff blocked: ${pushResult.detail ?? "git commit/push failed"}`,
4880
+ nextAction: "Commit and push the dirty worker changes to the existing PR branch, then rerun `kynver worker complete`."
4881
+ };
4882
+ }
4883
+ return {
4884
+ ok: true,
4885
+ prUrl: input.prUrl,
4886
+ headCommit: pushResult.headCommit ?? input.snapshot.headCommit ?? void 0,
4887
+ committed: pushResult.committed,
4888
+ pushed: pushResult.pushed
4889
+ };
4890
+ }
4836
4891
  function ensurePrReadyHandoff(input, exec = defaultPrHandoffExec) {
4837
4892
  const prUrlHint = input.prUrlHint ?? extractPrUrlFromText(input.status.finalResult) ?? null;
4838
4893
  const snapshot = buildPrHandoffSnapshotFromStatus(input.status, {
@@ -4856,10 +4911,23 @@ function ensurePrReadyHandoff(input, exec = defaultPrHandoffExec) {
4856
4911
  snapshot
4857
4912
  });
4858
4913
  if (!requirement.required) {
4859
- return { ok: true, prUrl: prUrlHint ?? void 0 };
4914
+ if (prUrlHint) {
4915
+ return commitDirtyToExistingPr({
4916
+ snapshot,
4917
+ prUrl: prUrlHint,
4918
+ commitMessage: `chore(harness): update PR handoff for ${input.worker.name}`,
4919
+ exec
4920
+ });
4921
+ }
4922
+ return { ok: true };
4860
4923
  }
4861
4924
  if (prUrlHint) {
4862
- return { ok: true, prUrl: prUrlHint };
4925
+ return commitDirtyToExistingPr({
4926
+ snapshot,
4927
+ prUrl: prUrlHint,
4928
+ commitMessage: `chore(harness): update PR handoff for ${input.worker.name}`,
4929
+ exec
4930
+ });
4863
4931
  }
4864
4932
  if (!ghAvailable(exec)) {
4865
4933
  const dirty = snapshot.changedFiles.length;
@@ -4922,11 +4990,12 @@ function ensurePrReadyHandoff(input, exec = defaultPrHandoffExec) {
4922
4990
  }
4923
4991
  const existing = findOpenPrUrl(snapshot.worktreePath, repo, snapshot.branch, exec);
4924
4992
  if (existing) {
4925
- return {
4926
- ok: true,
4993
+ return commitDirtyToExistingPr({
4994
+ snapshot,
4927
4995
  prUrl: existing,
4928
- headCommit: snapshot.headCommit ?? resolveHeadCommit(snapshot.worktreePath, exec) ?? void 0
4929
- };
4996
+ commitMessage: `chore(harness): update PR handoff for ${input.worker.name}`,
4997
+ exec
4998
+ });
4930
4999
  }
4931
5000
  const hasDirty = snapshot.changedFiles.length > 0;
4932
5001
  let committed = false;
@@ -5322,6 +5391,11 @@ function deriveNextAction(input) {
5322
5391
  }
5323
5392
  return null;
5324
5393
  }
5394
+ function isSkippedTerminalCompletionBlocker2(reason) {
5395
+ const text = reason?.trim();
5396
+ if (!text) return false;
5397
+ return /completion acknowledged but board not advanced/i.test(text) && /task already terminal/i.test(text);
5398
+ }
5325
5399
  function deriveHandoffState(input) {
5326
5400
  if (input.prUrl) return "pr_handoff";
5327
5401
  if (input.headCommit) return "commit_handoff";
@@ -5378,6 +5452,23 @@ async function tryCompleteWorker(args) {
5378
5452
  if (!forceReplay && shouldReplayHarnessCompletion(worker)) {
5379
5453
  clearCompletionBlockerForReplay(worker);
5380
5454
  }
5455
+ const skipPrHandoff = args.skipPrHandoff === true || args.skipPrHandoff === "true";
5456
+ if (!skipPrHandoff && worker.dispatched && taskId) {
5457
+ const handoff = ensurePrReadyHandoff({ worker, run, status });
5458
+ if (!handoff.ok) {
5459
+ persistCompletionBlocker(worker, handoff.reason);
5460
+ return {
5461
+ ok: false,
5462
+ reason: handoff.reason,
5463
+ nextAction: handoff.nextAction,
5464
+ completionBlocked: true
5465
+ };
5466
+ }
5467
+ if (handoff.prUrl || handoff.headCommit) {
5468
+ status = computeWorkerStatus(worker, workerStatusOptions(run));
5469
+ status = applyPrHandoffToStatus(status, handoff);
5470
+ }
5471
+ }
5381
5472
  const headCommit = status.gitAncestry.headIsAncestorOfBase === false && status.gitAncestry.head ? status.gitAncestry.head : status.headCommit;
5382
5473
  if (worker.dispatched) {
5383
5474
  const handoff = assessWorktreeCompletionHandoff({
@@ -5398,22 +5489,6 @@ async function tryCompleteWorker(args) {
5398
5489
  };
5399
5490
  }
5400
5491
  }
5401
- const skipPrHandoff = args.skipPrHandoff === true || args.skipPrHandoff === "true";
5402
- if (!skipPrHandoff && worker.dispatched && taskId) {
5403
- const handoff = ensurePrReadyHandoff({ worker, run, status });
5404
- if (!handoff.ok) {
5405
- persistCompletionBlocker(worker, handoff.reason);
5406
- return {
5407
- ok: false,
5408
- reason: handoff.reason,
5409
- nextAction: handoff.nextAction,
5410
- completionBlocked: true
5411
- };
5412
- }
5413
- if (handoff.prUrl || handoff.headCommit) {
5414
- status = applyPrHandoffToStatus(status, handoff);
5415
- }
5416
- }
5417
5492
  const base = resolveBaseUrl(args.baseUrl ? String(args.baseUrl) : void 0);
5418
5493
  const explicitSecret = args.secret ? String(args.secret) : void 0;
5419
5494
  let secret = await resolveCallbackSecretWithMint(explicitSecret, agentOsId, { baseUrl: base });
@@ -5619,7 +5694,8 @@ function buildWorkerBoardEntry(input) {
5619
5694
  headCommit
5620
5695
  });
5621
5696
  const rawBlocker = worker.completionBlocker;
5622
- const completionBlocker = typeof rawBlocker === "string" && rawBlocker ? rawBlocker : void 0;
5697
+ const rawCompletionBlocker = typeof rawBlocker === "string" && rawBlocker ? rawBlocker : void 0;
5698
+ const completionBlocker = isSkippedTerminalCompletionBlocker2(rawCompletionBlocker) ? void 0 : rawCompletionBlocker;
5623
5699
  const boardStatus = completionBlocker ? "blocked" : status.status;
5624
5700
  const boardAttention = completionBlocker ? "blocked" : status.attention.state;
5625
5701
  const completionResponse = asRecord3(worker.completionResponse);
@@ -5704,7 +5780,8 @@ function buildWorkerBoardEntry(input) {
5704
5780
  }
5705
5781
  function isMetadataTerminalDone(worker) {
5706
5782
  const status = typeof worker.status === "string" ? worker.status : "";
5707
- const completionBlocker = typeof worker.completionBlocker === "string" && worker.completionBlocker.trim().length > 0;
5783
+ const rawCompletionBlocker = typeof worker.completionBlocker === "string" && worker.completionBlocker.trim().length > 0 ? worker.completionBlocker : void 0;
5784
+ const completionBlocker = rawCompletionBlocker && !isSkippedTerminalCompletionBlocker2(rawCompletionBlocker);
5708
5785
  if (completionBlocker) return false;
5709
5786
  if (typeof worker.completionReportedAt === "string" && worker.completionReportedAt.trim()) return true;
5710
5787
  if (worker.completionOutcome === "acknowledged") return true;
@@ -9364,30 +9441,7 @@ function indexedWorktreeHasMaterialChanges(entry) {
9364
9441
  return materialWorktreeChanges2(gitStatusShort(entry.worktreePath)).length > 0;
9365
9442
  }
9366
9443
 
9367
- // src/cleanup-run-liveness.ts
9368
- function deriveRunTerminal(indexed, ctx) {
9369
- if (ctx) return ctx.runTerminalCache.derive(indexed.run);
9370
- return deriveTerminalRunStatus(indexed.run);
9371
- }
9372
- function isWorkerProcessLive(indexed) {
9373
- if (isPidAlive(indexed.worker.pid)) return true;
9374
- if (!indexed.worker.pid) return indexedWorktreeStatus(indexed).alive;
9375
- return false;
9376
- }
9377
- function isRunStaleActive(indexed, ctx) {
9378
- if (TERMINAL_RUN_STATUSES.has(indexed.run.status)) return false;
9379
- return deriveRunTerminal(indexed, ctx) !== null;
9380
- }
9381
- function runBlocksWorktreeRemoval(indexed, ctx) {
9382
- if (isWorkerProcessLive(indexed)) return true;
9383
- if (indexed.worker.completionBlocker) return true;
9384
- if (TERMINAL_RUN_STATUSES.has(indexed.run.status)) return false;
9385
- if (isRunStaleActive(indexed, ctx)) return false;
9386
- if (!isFinishedWorkerStatus(indexedWorktreeStatus(indexed))) return true;
9387
- return deriveRunTerminal(indexed, ctx) === null;
9388
- }
9389
-
9390
- // src/cleanup-guards.ts
9444
+ // src/cleanup-worktree-salvage.ts
9391
9445
  function prUrlFromFinalResult(finalResult) {
9392
9446
  if (typeof finalResult === "string") {
9393
9447
  const match = finalResult.match(/https:\/\/github\.com\/[^\s]+\/pull\/\d+/i);
@@ -9412,12 +9466,63 @@ function isPrOrUnmergedWork(status) {
9412
9466
  if (status.changedFiles.length > 0 && status.finalResult) return true;
9413
9467
  return false;
9414
9468
  }
9469
+
9470
+ // src/cleanup-completion-blocker.ts
9471
+ function completionBlockerBlocksWorktreeRemoval(indexed, status) {
9472
+ const blocker = typeof indexed.worker.completionBlocker === "string" ? indexed.worker.completionBlocker.trim() : "";
9473
+ if (!blocker) return false;
9474
+ if (isWorkerProcessLive(indexed)) return true;
9475
+ const resolved = status ?? indexedWorktreeStatus(indexed);
9476
+ if (!isFinishedWorkerStatus(resolved)) return true;
9477
+ if (isPrOrUnmergedWork(resolved)) return true;
9478
+ if (materialWorktreeChanges2(resolved.changedFiles).length > 0) return true;
9479
+ const landing = assessWorkerLanding({
9480
+ finalResult: resolved.finalResult,
9481
+ changedFiles: resolved.changedFiles,
9482
+ gitAncestry: resolved.gitAncestry,
9483
+ prUrl: prUrlFromFinalResult(resolved.finalResult)
9484
+ });
9485
+ if (landing.blocked) return true;
9486
+ return false;
9487
+ }
9488
+
9489
+ // src/cleanup-run-liveness.ts
9490
+ function deriveRunTerminal(indexed, ctx) {
9491
+ if (ctx) return ctx.runTerminalCache.derive(indexed.run);
9492
+ return deriveTerminalRunStatus(indexed.run);
9493
+ }
9494
+ function isWorkerProcessLive(indexed) {
9495
+ if (isPidAlive(indexed.worker.pid)) return true;
9496
+ if (!indexed.worker.pid) return indexedWorktreeStatus(indexed).alive;
9497
+ return false;
9498
+ }
9499
+ function isRunStaleActive(indexed, ctx) {
9500
+ if (TERMINAL_RUN_STATUSES.has(indexed.run.status)) return false;
9501
+ return deriveRunTerminal(indexed, ctx) !== null;
9502
+ }
9503
+ function runBlocksWorktreeRemoval(indexed, ctx) {
9504
+ if (isWorkerProcessLive(indexed)) return true;
9505
+ const status = indexedWorktreeStatus(indexed);
9506
+ if (completionBlockerBlocksWorktreeRemoval(indexed, status)) return true;
9507
+ if (isFinishedWorkerStatus(status)) return false;
9508
+ if (TERMINAL_RUN_STATUSES.has(indexed.run.status)) return false;
9509
+ if (isRunStaleActive(indexed, ctx)) return false;
9510
+ return deriveRunTerminal(indexed, ctx) === null;
9511
+ }
9512
+
9513
+ // src/cleanup-guards.ts
9415
9514
  function effectiveWorktreeAgeMs(input) {
9416
9515
  const { indexed, includeOrphans, worktreesAgeMs, terminalWorktreesAgeMs } = input;
9417
9516
  if (!indexed) return includeOrphans ? terminalWorktreesAgeMs : worktreesAgeMs;
9418
9517
  if (TERMINAL_RUN_STATUSES.has(indexed.run.status)) {
9419
9518
  return terminalWorktreesAgeMs;
9420
9519
  }
9520
+ if (input.liveness && isRunStaleActive(indexed, input.liveness)) {
9521
+ return terminalWorktreesAgeMs;
9522
+ }
9523
+ if (input.liveness && isFinishedWorkerStatus(indexedWorktreeStatus(indexed)) && !isWorkerProcessLive(indexed)) {
9524
+ return terminalWorktreesAgeMs;
9525
+ }
9421
9526
  return worktreesAgeMs;
9422
9527
  }
9423
9528
  function skipWorktreeRemoval(input) {
@@ -9431,7 +9536,7 @@ function skipWorktreeRemoval(input) {
9431
9536
  if (ageThresholdMs > 0 && ageMs < ageThresholdMs) return "below_age_threshold";
9432
9537
  const status = indexedWorktreeStatus(indexed);
9433
9538
  if (isWorkerProcessLive(indexed)) return "active_worker";
9434
- if (indexed.worker.completionBlocker) return "completion_blocked";
9539
+ if (completionBlockerBlocksWorktreeRemoval(indexed, status)) return "completion_blocked";
9435
9540
  if (runBlocksWorktreeRemoval(indexed, input.liveness)) return "run_still_active";
9436
9541
  if (!isFinishedWorkerStatus(status)) return "run_still_active";
9437
9542
  if (isPrOrUnmergedWork(status)) return "pr_or_unmerged_commits";
@@ -9587,7 +9692,9 @@ function skipRunDirectoryRemoval(input) {
9587
9692
  }
9588
9693
  if (!runDirectoryIsEmpty(runPath)) return "run_still_active";
9589
9694
  const run = loadRunStatus(harnessRoot, runId);
9590
- if (run && !TERMINAL_RUN_STATUSES.has(run.status)) return "run_still_active";
9695
+ if (run && !TERMINAL_RUN_STATUSES.has(run.status)) {
9696
+ if (!deriveTerminalRunStatus(run)) return "run_still_active";
9697
+ }
9591
9698
  if (runDirectoriesAgeMs > 0 && ageMs < runDirectoriesAgeMs) return "below_age_threshold";
9592
9699
  return null;
9593
9700
  }
@@ -10677,14 +10784,14 @@ function runHarnessCleanup(options = {}) {
10677
10784
  const paths = resolvePaths(options);
10678
10785
  emitCleanupProgress("scan", `${paths.scanRoots.length} harness root(s)`);
10679
10786
  const activeGuards = collectActiveWorktreeGuards(paths.scanRoots, paths.now);
10680
- emitCleanupProgress("index", "building worktree index");
10681
- const index = mergeWorktreeIndexes(paths.scanRoots);
10682
- emitCleanupProgress("index", `${index.size} indexed worktree(s)`);
10683
- const liveness = { runTerminalCache: new CleanupRunTerminalCache() };
10684
10787
  const finalizedRuns = retention.finalizeStaleRuns ? finalizeStaleRuns().map((f) => ({ runId: f.runId, from: f.from, to: f.to })) : [];
10685
10788
  if (finalizedRuns.length > 0) {
10686
10789
  emitCleanupProgress("finalize", `${finalizedRuns.length} stale run(s) marked terminal`);
10687
10790
  }
10791
+ emitCleanupProgress("index", "building worktree index");
10792
+ const index = mergeWorktreeIndexes(paths.scanRoots);
10793
+ emitCleanupProgress("index", `${index.size} indexed worktree(s)`);
10794
+ const liveness = { runTerminalCache: new CleanupRunTerminalCache() };
10688
10795
  const skips = [];
10689
10796
  const actions = [];
10690
10797
  const processedPaths = /* @__PURE__ */ new Set();