@kynver-app/runtime 0.1.103 → 0.1.106

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 (47) 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 +171 -62
  5. package/dist/cli.js.map +4 -4
  6. package/dist/index.js +172 -63
  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/dist/worker-persona-catalog.js +2 -2
  46. package/dist/worker-persona-catalog.js.map +2 -2
  47. 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
@@ -311,7 +311,9 @@ function redactHomePath(value) {
311
311
  if (resolved.startsWith(`${home}${path2.sep}`)) {
312
312
  return `~/${path2.relative(home, resolved).split(path2.sep).join("/")}`;
313
313
  }
314
- return resolved.replace(/^\/home\/[^/]+(?=\/|$)/, "~").replace(/^\/Users\/[^/]+(?=\/|$)/, "~");
314
+ const posix = resolved.replace(/\\/g, "/");
315
+ const redacted = posix.replace(/^\/home\/[^/]+(?=\/|$)/, "~").replace(/^\/Users\/[^/]+(?=\/|$)/, "~").replace(/^[A-Za-z]:\/home\/[^/]+(?=\/|$)/i, "~").replace(/^[A-Za-z]:\/Users\/[^/]+(?=\/|$)/i, "~");
316
+ return redacted;
315
317
  }
316
318
  function displayUserPath(value) {
317
319
  return redactHomePath(value);
@@ -1783,7 +1785,7 @@ var NO_START_MS = 18e4;
1783
1785
  var STALE_MS = 6e5;
1784
1786
  function computeAttention(input) {
1785
1787
  const now = Date.now();
1786
- if (input.completionBlocker) {
1788
+ if (input.completionBlocker && !isSkippedTerminalCompletionBlocker(input.completionBlocker)) {
1787
1789
  return { state: "blocked", reason: input.completionBlocker };
1788
1790
  }
1789
1791
  if (input.finalResult) {
@@ -1821,6 +1823,9 @@ function computeAttention(input) {
1821
1823
  return { state: "done", reason: "final result recorded" };
1822
1824
  }
1823
1825
  if (!input.alive) {
1826
+ if (isAbandonedEmptyWorker(input)) {
1827
+ return { state: "done", reason: "empty abandoned worker record" };
1828
+ }
1824
1829
  const classified = classifyExitFailure(input.error);
1825
1830
  if (classified) return { state: "blocked", reason: classified.reason };
1826
1831
  const salvage = assessExitedWorkerSalvage({
@@ -1855,6 +1860,19 @@ function computeAttention(input) {
1855
1860
  }
1856
1861
  return { state: "ok", reason: "recent activity" };
1857
1862
  }
1863
+ function isSkippedTerminalCompletionBlocker(reason) {
1864
+ const text = reason?.trim();
1865
+ if (!text) return false;
1866
+ return /completion acknowledged but board not advanced/i.test(text) && /task already terminal/i.test(text);
1867
+ }
1868
+ function isAbandonedEmptyWorker(input) {
1869
+ if (input.finalResult) return false;
1870
+ if (input.taskId || input.agentOsId) return false;
1871
+ if (input.stdoutBytes > 0 || (input.stderrBytes ?? 0) > 0 || input.heartbeatBytes > 0) return false;
1872
+ if (input.error?.trim()) return false;
1873
+ if ((input.changedFiles ?? []).some((line) => line.trim())) return false;
1874
+ return /empty worker dir|marked abandoned/i.test(input.reconcileReason ?? "");
1875
+ }
1858
1876
  function hasMergedTargetPrReconciliation(value) {
1859
1877
  let record = null;
1860
1878
  if (typeof value === "string") record = extractEmbeddedWorkerFinalResultRecord(value);
@@ -1899,6 +1917,7 @@ function computeWorkerStatus(worker, options = {}) {
1899
1917
  ]);
1900
1918
  const error = parsed.error || (!alive && !finalResult ? tailFile(worker.stderrPath, 10).trim() || void 0 : void 0);
1901
1919
  const completionBlocker = typeof worker.completionBlocker === "string" && worker.completionBlocker.trim() ? worker.completionBlocker.trim() : null;
1920
+ const effectiveCompletionBlocker = isSkippedTerminalCompletionBlocker(completionBlocker) ? null : completionBlocker;
1902
1921
  const landingContract = worker.repairTargetPrUrl ? {
1903
1922
  landingOnly: false,
1904
1923
  targetPrUrls: [worker.repairTargetPrUrl],
@@ -1910,6 +1929,7 @@ function computeWorkerStatus(worker, options = {}) {
1910
1929
  finalResult,
1911
1930
  firstEventAt: parsed.firstEventAt,
1912
1931
  stdoutBytes,
1932
+ stderrBytes,
1913
1933
  heartbeatBytes,
1914
1934
  lastActivityAt,
1915
1935
  heartbeatBlocker: heartbeat.heartbeatBlocker,
@@ -1917,12 +1937,15 @@ function computeWorkerStatus(worker, options = {}) {
1917
1937
  error,
1918
1938
  changedFiles,
1919
1939
  gitAncestry,
1920
- completionBlocker,
1940
+ completionBlocker: effectiveCompletionBlocker,
1921
1941
  landingContract,
1922
1942
  prUrl: worker.repairTargetPrUrl ?? worker.taskPrUrl ?? null,
1923
- localOnly: worker.localOnly === true
1943
+ localOnly: worker.localOnly === true,
1944
+ taskId: worker.taskId ?? null,
1945
+ agentOsId: worker.agentOsId ?? null,
1946
+ reconcileReason: worker.reconcileReason ?? null
1924
1947
  });
1925
- const workerStatusLabel = completionBlocker || attention.state === "blocked" ? "blocked" : completionAcknowledged || attention.state === "done" ? "done" : finalResult ? "exited" : alive ? "running" : "exited";
1948
+ const workerStatusLabel = effectiveCompletionBlocker || attention.state === "blocked" ? "blocked" : completionAcknowledged || attention.state === "done" ? "done" : finalResult ? "exited" : alive ? "running" : "exited";
1926
1949
  return {
1927
1950
  runId: worker.runId,
1928
1951
  worker: worker.name,
@@ -2675,14 +2698,14 @@ var WORKER_PERSONA_CATALOG = [
2675
2698
  {
2676
2699
  slug: "lorentz",
2677
2700
  displayName: "Lorentz",
2678
- description: "Review / testing \u2014 pre-landing and post-landing verification, report + deep review.",
2701
+ description: "Deep/adversarial review lane expert for risk, correctness, and safety gates. Run adversarial review and validation gating.",
2679
2702
  dispatchLane: "review",
2680
2703
  defaultRoleLane: "report_reviewer"
2681
2704
  },
2682
2705
  {
2683
2706
  slug: "dalton",
2684
2707
  displayName: "Dalton",
2685
- description: "Landing / merge execution \u2014 merge-ready PR landing and merge evidence only (no implementation).",
2708
+ description: "Landing-only \u2014 merge-ready handoff and final verification evidence; no implementation ownership.",
2686
2709
  dispatchLane: "landing",
2687
2710
  defaultRoleLane: "implementer"
2688
2711
  }
@@ -2839,6 +2862,9 @@ function enforceCursorWorkerProvider(input) {
2839
2862
  if (taskAllowsClaudeWorker(task)) {
2840
2863
  return routing;
2841
2864
  }
2865
+ if (routing.rule === "explicit:cli" && isClaudeFamilyProvider(routing.provider)) {
2866
+ return routing;
2867
+ }
2842
2868
  if (!isClaudeFamilyProvider(routing.provider)) {
2843
2869
  return routing;
2844
2870
  }
@@ -3872,7 +3898,7 @@ function resolveWorkerLaunch(input) {
3872
3898
  if (!input.task || Object.keys(input.task).length === 0) {
3873
3899
  return afterCursorPolicy;
3874
3900
  }
3875
- if (afterCursorPolicy.rule === "explicit:model_provider_alias" || afterCursorPolicy.rule === "explicit:model_provider_alias_overrode_provider") {
3901
+ if (afterCursorPolicy.rule === "explicit:model_provider_alias" || afterCursorPolicy.rule === "explicit:model_provider_alias_overrode_provider" || afterCursorPolicy.rule === "explicit:cli") {
3876
3902
  return afterCursorPolicy;
3877
3903
  }
3878
3904
  if (isClaudeFamilyProvider(afterCursorPolicy.provider) && (input.explicitProviderIsOperatorOverride || taskAllowsClaudeWorker(input.task))) {
@@ -4486,6 +4512,7 @@ function asString(value) {
4486
4512
  var ADVANCED_OUTCOMES = /* @__PURE__ */ new Set([
4487
4513
  "review_scheduled",
4488
4514
  "review_already_scheduled",
4515
+ "skipped_terminal_task",
4489
4516
  "closed",
4490
4517
  "dispatched",
4491
4518
  "dispatch_already_done"
@@ -4833,6 +4860,36 @@ function defaultPrBody(taskId, workerName, runId) {
4833
4860
  "Opened by orchestrator completion enforcement so production review receives a reviewable artifact."
4834
4861
  ].filter(Boolean).join("\n");
4835
4862
  }
4863
+ function commitDirtyToExistingPr(input) {
4864
+ if (input.snapshot.changedFiles.length === 0) {
4865
+ return {
4866
+ ok: true,
4867
+ prUrl: input.prUrl,
4868
+ headCommit: input.snapshot.headCommit ?? resolveHeadCommit(input.snapshot.worktreePath, input.exec) ?? void 0
4869
+ };
4870
+ }
4871
+ const pushResult = commitAndPushBranch({
4872
+ worktreePath: input.snapshot.worktreePath,
4873
+ branch: input.snapshot.branch,
4874
+ commitMessage: input.commitMessage,
4875
+ hasDirtyFiles: true,
4876
+ exec: input.exec
4877
+ });
4878
+ if (!pushResult.ok) {
4879
+ return {
4880
+ ok: false,
4881
+ reason: `PR-ready handoff blocked: ${pushResult.detail ?? "git commit/push failed"}`,
4882
+ nextAction: "Commit and push the dirty worker changes to the existing PR branch, then rerun `kynver worker complete`."
4883
+ };
4884
+ }
4885
+ return {
4886
+ ok: true,
4887
+ prUrl: input.prUrl,
4888
+ headCommit: pushResult.headCommit ?? input.snapshot.headCommit ?? void 0,
4889
+ committed: pushResult.committed,
4890
+ pushed: pushResult.pushed
4891
+ };
4892
+ }
4836
4893
  function ensurePrReadyHandoff(input, exec = defaultPrHandoffExec) {
4837
4894
  const prUrlHint = input.prUrlHint ?? extractPrUrlFromText(input.status.finalResult) ?? null;
4838
4895
  const snapshot = buildPrHandoffSnapshotFromStatus(input.status, {
@@ -4856,10 +4913,23 @@ function ensurePrReadyHandoff(input, exec = defaultPrHandoffExec) {
4856
4913
  snapshot
4857
4914
  });
4858
4915
  if (!requirement.required) {
4859
- return { ok: true, prUrl: prUrlHint ?? void 0 };
4916
+ if (prUrlHint) {
4917
+ return commitDirtyToExistingPr({
4918
+ snapshot,
4919
+ prUrl: prUrlHint,
4920
+ commitMessage: `chore(harness): update PR handoff for ${input.worker.name}`,
4921
+ exec
4922
+ });
4923
+ }
4924
+ return { ok: true };
4860
4925
  }
4861
4926
  if (prUrlHint) {
4862
- return { ok: true, prUrl: prUrlHint };
4927
+ return commitDirtyToExistingPr({
4928
+ snapshot,
4929
+ prUrl: prUrlHint,
4930
+ commitMessage: `chore(harness): update PR handoff for ${input.worker.name}`,
4931
+ exec
4932
+ });
4863
4933
  }
4864
4934
  if (!ghAvailable(exec)) {
4865
4935
  const dirty = snapshot.changedFiles.length;
@@ -4922,11 +4992,12 @@ function ensurePrReadyHandoff(input, exec = defaultPrHandoffExec) {
4922
4992
  }
4923
4993
  const existing = findOpenPrUrl(snapshot.worktreePath, repo, snapshot.branch, exec);
4924
4994
  if (existing) {
4925
- return {
4926
- ok: true,
4995
+ return commitDirtyToExistingPr({
4996
+ snapshot,
4927
4997
  prUrl: existing,
4928
- headCommit: snapshot.headCommit ?? resolveHeadCommit(snapshot.worktreePath, exec) ?? void 0
4929
- };
4998
+ commitMessage: `chore(harness): update PR handoff for ${input.worker.name}`,
4999
+ exec
5000
+ });
4930
5001
  }
4931
5002
  const hasDirty = snapshot.changedFiles.length > 0;
4932
5003
  let committed = false;
@@ -5322,6 +5393,11 @@ function deriveNextAction(input) {
5322
5393
  }
5323
5394
  return null;
5324
5395
  }
5396
+ function isSkippedTerminalCompletionBlocker2(reason) {
5397
+ const text = reason?.trim();
5398
+ if (!text) return false;
5399
+ return /completion acknowledged but board not advanced/i.test(text) && /task already terminal/i.test(text);
5400
+ }
5325
5401
  function deriveHandoffState(input) {
5326
5402
  if (input.prUrl) return "pr_handoff";
5327
5403
  if (input.headCommit) return "commit_handoff";
@@ -5378,6 +5454,23 @@ async function tryCompleteWorker(args) {
5378
5454
  if (!forceReplay && shouldReplayHarnessCompletion(worker)) {
5379
5455
  clearCompletionBlockerForReplay(worker);
5380
5456
  }
5457
+ const skipPrHandoff = args.skipPrHandoff === true || args.skipPrHandoff === "true";
5458
+ if (!skipPrHandoff && worker.dispatched && taskId) {
5459
+ const handoff = ensurePrReadyHandoff({ worker, run, status });
5460
+ if (!handoff.ok) {
5461
+ persistCompletionBlocker(worker, handoff.reason);
5462
+ return {
5463
+ ok: false,
5464
+ reason: handoff.reason,
5465
+ nextAction: handoff.nextAction,
5466
+ completionBlocked: true
5467
+ };
5468
+ }
5469
+ if (handoff.prUrl || handoff.headCommit) {
5470
+ status = computeWorkerStatus(worker, workerStatusOptions(run));
5471
+ status = applyPrHandoffToStatus(status, handoff);
5472
+ }
5473
+ }
5381
5474
  const headCommit = status.gitAncestry.headIsAncestorOfBase === false && status.gitAncestry.head ? status.gitAncestry.head : status.headCommit;
5382
5475
  if (worker.dispatched) {
5383
5476
  const handoff = assessWorktreeCompletionHandoff({
@@ -5398,22 +5491,6 @@ async function tryCompleteWorker(args) {
5398
5491
  };
5399
5492
  }
5400
5493
  }
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
5494
  const base = resolveBaseUrl(args.baseUrl ? String(args.baseUrl) : void 0);
5418
5495
  const explicitSecret = args.secret ? String(args.secret) : void 0;
5419
5496
  let secret = await resolveCallbackSecretWithMint(explicitSecret, agentOsId, { baseUrl: base });
@@ -5619,7 +5696,8 @@ function buildWorkerBoardEntry(input) {
5619
5696
  headCommit
5620
5697
  });
5621
5698
  const rawBlocker = worker.completionBlocker;
5622
- const completionBlocker = typeof rawBlocker === "string" && rawBlocker ? rawBlocker : void 0;
5699
+ const rawCompletionBlocker = typeof rawBlocker === "string" && rawBlocker ? rawBlocker : void 0;
5700
+ const completionBlocker = isSkippedTerminalCompletionBlocker2(rawCompletionBlocker) ? void 0 : rawCompletionBlocker;
5623
5701
  const boardStatus = completionBlocker ? "blocked" : status.status;
5624
5702
  const boardAttention = completionBlocker ? "blocked" : status.attention.state;
5625
5703
  const completionResponse = asRecord3(worker.completionResponse);
@@ -5704,7 +5782,8 @@ function buildWorkerBoardEntry(input) {
5704
5782
  }
5705
5783
  function isMetadataTerminalDone(worker) {
5706
5784
  const status = typeof worker.status === "string" ? worker.status : "";
5707
- const completionBlocker = typeof worker.completionBlocker === "string" && worker.completionBlocker.trim().length > 0;
5785
+ const rawCompletionBlocker = typeof worker.completionBlocker === "string" && worker.completionBlocker.trim().length > 0 ? worker.completionBlocker : void 0;
5786
+ const completionBlocker = rawCompletionBlocker && !isSkippedTerminalCompletionBlocker2(rawCompletionBlocker);
5708
5787
  if (completionBlocker) return false;
5709
5788
  if (typeof worker.completionReportedAt === "string" && worker.completionReportedAt.trim()) return true;
5710
5789
  if (worker.completionOutcome === "acknowledged") return true;
@@ -9364,30 +9443,7 @@ function indexedWorktreeHasMaterialChanges(entry) {
9364
9443
  return materialWorktreeChanges2(gitStatusShort(entry.worktreePath)).length > 0;
9365
9444
  }
9366
9445
 
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
9446
+ // src/cleanup-worktree-salvage.ts
9391
9447
  function prUrlFromFinalResult(finalResult) {
9392
9448
  if (typeof finalResult === "string") {
9393
9449
  const match = finalResult.match(/https:\/\/github\.com\/[^\s]+\/pull\/\d+/i);
@@ -9412,12 +9468,63 @@ function isPrOrUnmergedWork(status) {
9412
9468
  if (status.changedFiles.length > 0 && status.finalResult) return true;
9413
9469
  return false;
9414
9470
  }
9471
+
9472
+ // src/cleanup-completion-blocker.ts
9473
+ function completionBlockerBlocksWorktreeRemoval(indexed, status) {
9474
+ const blocker = typeof indexed.worker.completionBlocker === "string" ? indexed.worker.completionBlocker.trim() : "";
9475
+ if (!blocker) return false;
9476
+ if (isWorkerProcessLive(indexed)) return true;
9477
+ const resolved = status ?? indexedWorktreeStatus(indexed);
9478
+ if (!isFinishedWorkerStatus(resolved)) return true;
9479
+ if (isPrOrUnmergedWork(resolved)) return true;
9480
+ if (materialWorktreeChanges2(resolved.changedFiles).length > 0) return true;
9481
+ const landing = assessWorkerLanding({
9482
+ finalResult: resolved.finalResult,
9483
+ changedFiles: resolved.changedFiles,
9484
+ gitAncestry: resolved.gitAncestry,
9485
+ prUrl: prUrlFromFinalResult(resolved.finalResult)
9486
+ });
9487
+ if (landing.blocked) return true;
9488
+ return false;
9489
+ }
9490
+
9491
+ // src/cleanup-run-liveness.ts
9492
+ function deriveRunTerminal(indexed, ctx) {
9493
+ if (ctx) return ctx.runTerminalCache.derive(indexed.run);
9494
+ return deriveTerminalRunStatus(indexed.run);
9495
+ }
9496
+ function isWorkerProcessLive(indexed) {
9497
+ if (isPidAlive(indexed.worker.pid)) return true;
9498
+ if (!indexed.worker.pid) return indexedWorktreeStatus(indexed).alive;
9499
+ return false;
9500
+ }
9501
+ function isRunStaleActive(indexed, ctx) {
9502
+ if (TERMINAL_RUN_STATUSES.has(indexed.run.status)) return false;
9503
+ return deriveRunTerminal(indexed, ctx) !== null;
9504
+ }
9505
+ function runBlocksWorktreeRemoval(indexed, ctx) {
9506
+ if (isWorkerProcessLive(indexed)) return true;
9507
+ const status = indexedWorktreeStatus(indexed);
9508
+ if (completionBlockerBlocksWorktreeRemoval(indexed, status)) return true;
9509
+ if (isFinishedWorkerStatus(status)) return false;
9510
+ if (TERMINAL_RUN_STATUSES.has(indexed.run.status)) return false;
9511
+ if (isRunStaleActive(indexed, ctx)) return false;
9512
+ return deriveRunTerminal(indexed, ctx) === null;
9513
+ }
9514
+
9515
+ // src/cleanup-guards.ts
9415
9516
  function effectiveWorktreeAgeMs(input) {
9416
9517
  const { indexed, includeOrphans, worktreesAgeMs, terminalWorktreesAgeMs } = input;
9417
9518
  if (!indexed) return includeOrphans ? terminalWorktreesAgeMs : worktreesAgeMs;
9418
9519
  if (TERMINAL_RUN_STATUSES.has(indexed.run.status)) {
9419
9520
  return terminalWorktreesAgeMs;
9420
9521
  }
9522
+ if (input.liveness && isRunStaleActive(indexed, input.liveness)) {
9523
+ return terminalWorktreesAgeMs;
9524
+ }
9525
+ if (input.liveness && isFinishedWorkerStatus(indexedWorktreeStatus(indexed)) && !isWorkerProcessLive(indexed)) {
9526
+ return terminalWorktreesAgeMs;
9527
+ }
9421
9528
  return worktreesAgeMs;
9422
9529
  }
9423
9530
  function skipWorktreeRemoval(input) {
@@ -9431,7 +9538,7 @@ function skipWorktreeRemoval(input) {
9431
9538
  if (ageThresholdMs > 0 && ageMs < ageThresholdMs) return "below_age_threshold";
9432
9539
  const status = indexedWorktreeStatus(indexed);
9433
9540
  if (isWorkerProcessLive(indexed)) return "active_worker";
9434
- if (indexed.worker.completionBlocker) return "completion_blocked";
9541
+ if (completionBlockerBlocksWorktreeRemoval(indexed, status)) return "completion_blocked";
9435
9542
  if (runBlocksWorktreeRemoval(indexed, input.liveness)) return "run_still_active";
9436
9543
  if (!isFinishedWorkerStatus(status)) return "run_still_active";
9437
9544
  if (isPrOrUnmergedWork(status)) return "pr_or_unmerged_commits";
@@ -9587,7 +9694,9 @@ function skipRunDirectoryRemoval(input) {
9587
9694
  }
9588
9695
  if (!runDirectoryIsEmpty(runPath)) return "run_still_active";
9589
9696
  const run = loadRunStatus(harnessRoot, runId);
9590
- if (run && !TERMINAL_RUN_STATUSES.has(run.status)) return "run_still_active";
9697
+ if (run && !TERMINAL_RUN_STATUSES.has(run.status)) {
9698
+ if (!deriveTerminalRunStatus(run)) return "run_still_active";
9699
+ }
9591
9700
  if (runDirectoriesAgeMs > 0 && ageMs < runDirectoriesAgeMs) return "below_age_threshold";
9592
9701
  return null;
9593
9702
  }
@@ -10677,14 +10786,14 @@ function runHarnessCleanup(options = {}) {
10677
10786
  const paths = resolvePaths(options);
10678
10787
  emitCleanupProgress("scan", `${paths.scanRoots.length} harness root(s)`);
10679
10788
  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
10789
  const finalizedRuns = retention.finalizeStaleRuns ? finalizeStaleRuns().map((f) => ({ runId: f.runId, from: f.from, to: f.to })) : [];
10685
10790
  if (finalizedRuns.length > 0) {
10686
10791
  emitCleanupProgress("finalize", `${finalizedRuns.length} stale run(s) marked terminal`);
10687
10792
  }
10793
+ emitCleanupProgress("index", "building worktree index");
10794
+ const index = mergeWorktreeIndexes(paths.scanRoots);
10795
+ emitCleanupProgress("index", `${index.size} indexed worktree(s)`);
10796
+ const liveness = { runTerminalCache: new CleanupRunTerminalCache() };
10688
10797
  const skips = [];
10689
10798
  const actions = [];
10690
10799
  const processedPaths = /* @__PURE__ */ new Set();