claude-code-session-manager 0.87.0 → 0.88.0

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 (290) hide show
  1. package/README.md +26 -53
  2. package/dist/assets/{AgentLibrary-DyLWzZDf.js → AgentLibrary-BZ6IY2g1.js} +1 -1
  3. package/dist/assets/{DataModel--mISIJ6h.js → DataModel-dAoGAODX.js} +1 -1
  4. package/dist/assets/{History-C2ahUXTg.js → History-CF157HyX.js} +1 -1
  5. package/dist/assets/{Hooks-BiC6oyR2.js → Hooks-CCPPipBk.js} +1 -1
  6. package/dist/assets/{HostBilko-BPleEOld.js → HostBilko-Dlmz2m06.js} +1 -1
  7. package/dist/assets/{Library-Dc8Qst1R.js → Library-BaXcboWX.js} +1 -1
  8. package/dist/assets/{ListDetail-DIXh-OLX.js → ListDetail-CoUCzG5S.js} +1 -1
  9. package/dist/assets/{MarkdownEditor-C90bkLXK.js → MarkdownEditor-DTg3JlOT.js} +1 -1
  10. package/dist/assets/{McpServers-DqcbLOLZ.js → McpServers-DdjY1sA3.js} +1 -1
  11. package/dist/assets/{Memory-CW62MXlh.js → Memory-BJ3Jlw0C.js} +1 -1
  12. package/dist/assets/{Panel-Bw1FhRuF.js → Panel-ygRSgc41.js} +1 -1
  13. package/dist/assets/{Permissions-BcUC-5y8.js → Permissions-m0KAnccF.js} +1 -1
  14. package/dist/assets/{Plugins-BnKx9flD.js → Plugins-CJRl6B3Q.js} +2 -2
  15. package/dist/assets/{ProvenanceBadge-Bw5vNVPT.js → ProvenanceBadge-w8cnlVvW.js} +1 -1
  16. package/dist/assets/{SaveBar-CWr0O_w-.js → SaveBar-BpyPcUIk.js} +1 -1
  17. package/dist/assets/{Scheduler-DYdLuUqq.js → Scheduler-D2ppxSfy.js} +7 -7
  18. package/dist/assets/{ScopeSwitcher-CrBLbg8s.js → ScopeSwitcher-EyuIwt-j.js} +1 -1
  19. package/dist/assets/{Settings-DluB-vN1.js → Settings-BHSGGFEc.js} +1 -1
  20. package/dist/assets/{SkillReferenceGraph-CHLSseay.js → SkillReferenceGraph-BNaZ2JCF.js} +1 -1
  21. package/dist/assets/{Skills-gNdo_HNK.js → Skills-Dl3q75QV.js} +1 -1
  22. package/dist/assets/{SystemPrompt-Cru05-Ia.js → SystemPrompt-Bg6F-Cyp.js} +1 -1
  23. package/dist/assets/{TagLibrary-DNHY0xou.js → TagLibrary-BSutflCy.js} +1 -1
  24. package/dist/assets/{TiptapBody-I4lmbCgP.js → TiptapBody-CXih4xrc.js} +1 -1
  25. package/dist/assets/{Toggle-bWMHjmRh.js → Toggle-2qtUHhA9.js} +1 -1
  26. package/dist/assets/{index-fc_JjdxL.js → index-B3emy2oI.js} +356 -356
  27. package/dist/assets/{index-DV3PorRY.css → index-BHTX4OTc.css} +1 -1
  28. package/dist/assets/{settingsSchema-BfhtZnGD.js → settingsSchema-CdzxaqwM.js} +1 -1
  29. package/dist/assets/{whisperWorker-Dbia1OpC.js → whisperWorker-C7ZGQwKg.js} +7 -7
  30. package/dist/index.html +2 -2
  31. package/dist/vad/ort-wasm-simd-threaded.asyncify.mjs +106 -110
  32. package/dist/vad/ort-wasm-simd-threaded.asyncify.wasm +0 -0
  33. package/dist/vad/ort-wasm-simd-threaded.jsep.mjs +98 -98
  34. package/dist/vad/ort-wasm-simd-threaded.jsep.wasm +0 -0
  35. package/dist/vad/ort-wasm-simd-threaded.jspi.mjs +99 -102
  36. package/dist/vad/ort-wasm-simd-threaded.jspi.wasm +0 -0
  37. package/dist/vad/ort-wasm-simd-threaded.mjs +46 -46
  38. package/dist/vad/ort-wasm-simd-threaded.wasm +0 -0
  39. package/package.json +10 -13
  40. package/scripts/README.md +59 -0
  41. package/scripts/audit-ops-hygiene.cjs +350 -0
  42. package/scripts/hooks/guard-destructive-git.cjs +10 -34
  43. package/scripts/hooks/guard-inline-implementation.cjs +14 -8
  44. package/scripts/hooks/guard-prd-writes.cjs +7 -36
  45. package/scripts/hooks/guard-self-schedule.cjs +175 -0
  46. package/scripts/ops-sweep.cjs +355 -0
  47. package/scripts/scheduler-mcp-server.cjs +28 -71
  48. package/src/main/__tests__/bilkoHost-integration.test.cjs +3 -3
  49. package/src/main/__tests__/chat-cancel-terminal.test.cjs +6 -9
  50. package/src/main/__tests__/chat-exit-close-race.test.cjs +3 -3
  51. package/src/main/__tests__/chat-mcp-consent-notice.test.cjs +4 -5
  52. package/src/main/__tests__/chat-queue.test.cjs +2 -2
  53. package/src/main/__tests__/chat-stop-signal.test.cjs +2 -2
  54. package/src/main/__tests__/dep-orphan-archive-health.test.cjs +77 -0
  55. package/src/main/__tests__/dod-batchkey.test.cjs +2 -2
  56. package/src/main/__tests__/dod-drain-hook.test.cjs +2 -2
  57. package/src/main/__tests__/dod-report.test.cjs +2 -2
  58. package/src/main/__tests__/dod-reverify.test.cjs +2 -2
  59. package/src/main/__tests__/epicMint.test.cjs +2 -2
  60. package/src/main/__tests__/exchanges.test.cjs +2 -2
  61. package/src/main/__tests__/extractJson.test.cjs +2 -2
  62. package/src/main/__tests__/files-reject-credentials.test.cjs +1 -1
  63. package/src/main/__tests__/fixtures/1218-fo-01-move-scripts-lib-into-src-main-lib.log +556 -0
  64. package/src/main/__tests__/health-build-freshness.test.cjs +39 -0
  65. package/src/main/__tests__/health-delegation-chain.test.cjs +15 -1
  66. package/src/main/__tests__/health-queue-dispatch.test.cjs +58 -7
  67. package/src/main/__tests__/health-tick-liveness.test.cjs +11 -3
  68. package/src/main/__tests__/health-usage-poller.test.cjs +70 -23
  69. package/src/main/__tests__/historyRollup.test.cjs +2 -2
  70. package/src/main/__tests__/kg-augment.test.cjs +2 -2
  71. package/src/main/__tests__/mcpStatus.test.cjs +2 -2
  72. package/src/main/__tests__/memoryAggregate.test.cjs +1 -1
  73. package/src/main/__tests__/memoryStale.test.cjs +1 -1
  74. package/src/main/__tests__/opsErrorLogTelemetryTap.test.cjs +25 -1
  75. package/src/main/__tests__/pollLoop-dispatch-on-failure.test.cjs +33 -3
  76. package/src/main/__tests__/prd-group-allocator.test.cjs +2 -2
  77. package/src/main/__tests__/prdAdminRouteParity.test.cjs +2 -0
  78. package/src/main/__tests__/prdAdminRoutes.test.cjs +14 -2
  79. package/src/main/__tests__/prdAuthoringSeed.test.cjs +39 -0
  80. package/src/main/__tests__/prdLocationsArchived.test.cjs +20 -13
  81. package/src/main/__tests__/proc-role-env.test.cjs +125 -0
  82. package/src/main/__tests__/procname-claude-spawn-sites.test.cjs +304 -0
  83. package/src/main/__tests__/procname-sm-processes.test.cjs +127 -0
  84. package/src/main/__tests__/projectHomeAdminRoutes.test.cjs +81 -401
  85. package/src/main/__tests__/projectPages.test.cjs +63 -149
  86. package/src/main/__tests__/queue-health-verdict.test.cjs +9 -9
  87. package/src/main/__tests__/queue-starvation-dispatch-driver.test.cjs +117 -20
  88. package/src/main/__tests__/queueHistory.test.cjs +2 -2
  89. package/src/main/__tests__/rateLimitPollerStreak.test.cjs +38 -3
  90. package/src/main/__tests__/runVerify-landed-commit-outranks.test.cjs +181 -0
  91. package/src/main/__tests__/runVerify.test.cjs +5 -5
  92. package/src/main/__tests__/scheduleJobStatusDrift.test.cjs +3 -3
  93. package/src/main/__tests__/scheduleJobTransitions.test.cjs +2 -2
  94. package/src/main/__tests__/scheduler-adopted-run-supervision.test.cjs +143 -0
  95. package/src/main/__tests__/scheduler-autofix-select.test.cjs +2 -2
  96. package/src/main/__tests__/scheduler-autopromote.test.cjs +2 -2
  97. package/src/main/__tests__/scheduler-bash-timeout-env.test.cjs +3 -5
  98. package/src/main/__tests__/scheduler-boot-orphans.test.cjs +78 -97
  99. package/src/main/__tests__/scheduler-default-eligible-heal.test.cjs +161 -0
  100. package/src/main/__tests__/scheduler-dispatch-loop.test.cjs +58 -0
  101. package/src/main/__tests__/scheduler-epic-digest.test.cjs +3 -5
  102. package/src/main/__tests__/scheduler-force-tick-outcome.test.cjs +2 -2
  103. package/src/main/__tests__/scheduler-gate-shadow.test.cjs +119 -0
  104. package/src/main/__tests__/scheduler-guard-verdict-autoresolve.test.cjs +46 -0
  105. package/src/main/__tests__/scheduler-heartbeat-payload.test.cjs +80 -0
  106. package/src/main/__tests__/scheduler-inplace-salvage.test.cjs +25 -18
  107. package/src/main/__tests__/scheduler-investigation-prompt.test.cjs +4 -4
  108. package/src/main/__tests__/scheduler-launch-failure.test.cjs +3 -5
  109. package/src/main/__tests__/scheduler-looks-done.test.cjs +26 -7
  110. package/src/main/__tests__/scheduler-manual-pause.test.cjs +118 -0
  111. package/src/main/__tests__/scheduler-meta-code-sha.test.cjs +26 -3
  112. package/src/main/__tests__/scheduler-prd-missing-skip.test.cjs +17 -2
  113. package/src/main/__tests__/scheduler-prd-persona-spawn.test.cjs +3 -5
  114. package/src/main/__tests__/scheduler-quiet-machine-lease.test.cjs +41 -6
  115. package/src/main/__tests__/scheduler-rate-limit-pause.test.cjs +62 -6
  116. package/src/main/__tests__/scheduler-rate-limit-spin-guard.test.cjs +3 -5
  117. package/src/main/__tests__/scheduler-reap-dead-running-jobs.test.cjs +23 -4
  118. package/src/main/__tests__/scheduler-reconcile-cwd-preserve.test.cjs +100 -0
  119. package/src/main/__tests__/scheduler-reconcile-invalid-repair.test.cjs +3 -3
  120. package/src/main/__tests__/scheduler-reconcile-quarantine.test.cjs +4 -4
  121. package/src/main/__tests__/scheduler-shard-quarantine.test.cjs +110 -0
  122. package/src/main/__tests__/scheduler-shared-tree-guard.test.cjs +81 -1
  123. package/src/main/__tests__/scheduler-starve-escalation.test.cjs +4 -4
  124. package/src/main/__tests__/scheduler-stranded-autofix-park.test.cjs +245 -0
  125. package/src/main/__tests__/scheduler-supervisor-record.test.cjs +81 -0
  126. package/src/main/__tests__/scheduler-tick-cancel-token.test.cjs +2 -2
  127. package/src/main/__tests__/scheduler-tick-wedge.test.cjs +172 -0
  128. package/src/main/__tests__/scheduler-transient-failure.test.cjs +2 -2
  129. package/src/main/__tests__/scheduler-worktree-cap-defer.test.cjs +37 -11
  130. package/src/main/__tests__/scheduler-worktree-exec-cwd.test.cjs +3 -5
  131. package/src/main/__tests__/usageSingleFlight.test.cjs +157 -0
  132. package/src/main/__tests__/workTypeLibrary.test.cjs +1 -1
  133. package/src/main/bilkoHost.cjs +16 -7
  134. package/src/main/build-info.json +8 -0
  135. package/src/main/chatRunner.cjs +4 -2
  136. package/src/main/config.cjs +2 -3
  137. package/src/main/docEdit.cjs +4 -2
  138. package/src/main/health.cjs +267 -51
  139. package/src/main/heapSnapshot.cjs +2 -2
  140. package/src/main/historyAggregator.cjs +1 -1
  141. package/src/main/index.cjs +61 -10
  142. package/src/main/ipcSchemas.cjs +6 -17
  143. package/src/main/lib/__tests__/auditLog.test.cjs +38 -0
  144. package/src/main/lib/__tests__/buildIdentity.test.cjs +121 -0
  145. package/src/main/lib/__tests__/cwdClassify.test.cjs +111 -0
  146. package/src/main/lib/__tests__/definitionOfDoneSequence.test.cjs +95 -0
  147. package/src/main/lib/__tests__/delegationReadiness.test.cjs +1 -1
  148. package/src/main/lib/__tests__/dispatchLoop.test.cjs +63 -0
  149. package/src/main/lib/__tests__/gateFixtures.json +20 -0
  150. package/src/main/lib/__tests__/gitWorktree.test.cjs +8 -1
  151. package/src/main/lib/__tests__/instanceLock.test.cjs +93 -7
  152. package/src/main/lib/__tests__/jobSupervisorRecord.test.cjs +78 -0
  153. package/src/main/lib/__tests__/jobWorktreeBootLive.test.cjs +11 -0
  154. package/src/main/lib/__tests__/localAdminHttp.test.cjs +1 -1
  155. package/src/main/lib/__tests__/mcpToolCatalog.test.cjs +6 -1
  156. package/src/main/lib/__tests__/procIdentity.test.cjs +119 -0
  157. package/src/main/lib/__tests__/procName.test.cjs +92 -0
  158. package/src/main/lib/__tests__/queueStoreMachineStateRecovery.test.cjs +67 -0
  159. package/src/main/lib/__tests__/schedulerMcpServerHelp.test.cjs +6 -7
  160. package/src/main/lib/__tests__/schedulerMcpServerProjectHome.test.cjs +37 -204
  161. package/src/main/lib/__tests__/schedulerPaths.test.cjs +226 -0
  162. package/src/main/lib/__tests__/schedulerPathsWorktree.test.cjs +94 -0
  163. package/src/main/lib/__tests__/schedulerRuntimeState.test.cjs +56 -0
  164. package/src/main/lib/__tests__/sessionSlots.test.cjs +45 -0
  165. package/src/main/lib/__tests__/telemetryBacklog.test.cjs +1 -1
  166. package/src/main/lib/__tests__/upgradeDrain.test.cjs +130 -0
  167. package/src/main/lib/__tests__/usageCircuit.test.cjs +61 -0
  168. package/src/main/lib/__tests__/watchdog-helpers.test.cjs +63 -0
  169. package/src/main/lib/__tests__/watchdog-relaunch.test.cjs +73 -0
  170. package/src/main/lib/activeIndexRebuild.cjs +1 -3
  171. package/src/main/lib/activeSessions.cjs +20 -88
  172. package/src/main/lib/adoptedRunSupervisor.cjs +136 -0
  173. package/src/main/lib/agentModelResolve.cjs +33 -1
  174. package/src/main/lib/agentPersonaSchema.cjs +2 -2
  175. package/src/main/lib/auditLog.cjs +30 -5
  176. package/src/main/lib/buildIdentity.cjs +113 -0
  177. package/src/main/lib/classifyPromptTicket.cjs +3 -2
  178. package/src/main/lib/claudeBin.cjs +37 -2
  179. package/src/main/lib/cleanEnv.cjs +27 -1
  180. package/src/main/lib/credentials.cjs +4 -2
  181. package/src/main/lib/cwdClassify.cjs +185 -0
  182. package/src/main/lib/definitionOfDone.cjs +296 -52
  183. package/src/main/lib/dispatchLoop.cjs +40 -0
  184. package/src/main/lib/effectiveModelInfo.cjs +9 -10
  185. package/src/main/lib/ephemeralCwd.cjs +7 -28
  186. package/src/main/lib/gitWorktree.cjs +95 -8
  187. package/src/main/lib/guardShims.cjs +3 -3
  188. package/src/main/lib/historyRollup.cjs +6 -7
  189. package/src/main/lib/instanceLock.cjs +32 -5
  190. package/src/main/lib/jobSupervisorRecord.cjs +147 -0
  191. package/src/main/lib/jobWorktreeBootLive.cjs +9 -5
  192. package/src/main/lib/localAdminHttp.cjs +9 -27
  193. package/src/main/lib/mcpToolCatalog.cjs +29 -77
  194. package/src/main/lib/opsOwnership.cjs +27 -19
  195. package/src/main/lib/prdAuthoringSeed.cjs +36 -0
  196. package/src/main/lib/prdLocations.cjs +48 -1
  197. package/src/main/lib/procIdentity.cjs +126 -0
  198. package/src/main/lib/procName.cjs +98 -0
  199. package/src/main/lib/projectHomeAdminRoutes.cjs +56 -327
  200. package/src/main/lib/queueHistory.cjs +8 -7
  201. package/src/main/lib/queueStore.cjs +57 -37
  202. package/src/main/lib/quietMachineLease.cjs +19 -2
  203. package/src/main/lib/reservationExpiry.cjs +30 -0
  204. package/src/main/lib/runClaudeP.cjs +4 -2
  205. package/src/main/lib/runLogRetention.cjs +3 -2
  206. package/src/main/lib/scheduleJobSchema.cjs +2 -2
  207. package/src/main/lib/scheduleJobTransitions.cjs +29 -2
  208. package/src/main/lib/schedulerBatch.cjs +27 -2
  209. package/src/main/lib/schedulerPaths.cjs +175 -0
  210. package/src/main/lib/schedulerRuntimeState.cjs +59 -0
  211. package/src/main/lib/sessionSlots.cjs +47 -9
  212. package/src/main/lib/smProcNames.cjs +51 -0
  213. package/src/main/lib/upgradeDrain.cjs +188 -0
  214. package/src/main/lib/usageCircuit.cjs +53 -6
  215. package/src/main/lib/watchdogHelpers.cjs +70 -37
  216. package/src/main/lib/withTimeout.cjs +33 -0
  217. package/src/main/mcpStatus.cjs +4 -2
  218. package/src/main/pluginInstall.cjs +6 -2
  219. package/src/main/projectPages.cjs +41 -145
  220. package/src/main/pty.cjs +8 -1
  221. package/src/main/queueOps.cjs +10 -10
  222. package/src/main/runVerify.cjs +69 -3
  223. package/src/main/scheduler.cjs +1598 -381
  224. package/src/main/seedAgentPersonas.cjs +1 -1
  225. package/src/main/seedDevPlugin.cjs +1 -1
  226. package/src/main/seedSchedulerMcp.cjs +7 -6
  227. package/src/main/seedStatus.cjs +1 -1
  228. package/src/main/supervisor.cjs +5 -3
  229. package/src/main/usage.cjs +126 -62
  230. package/src/preload/api.d.ts +18 -64
  231. package/src/preload/index.cjs +2 -4
  232. package/src/seed/agents/project-home-builder.md +31 -48
  233. package/screenshots/.gitkeep +0 -0
  234. package/screenshots/README-screenshots.md +0 -13
  235. package/src/main/lib/projectPageSummarySchema.cjs +0 -181
  236. package/src/main/teams.cjs +0 -95
  237. package/src/main/templates/project-pages-catalog.json +0 -741
  238. package/src/main/templates/project-pages-default-home.html +0 -123
  239. package/src/main/templates/project-pages-pipeline.md +0 -417
  240. package/src/seed/prompts/code-review/ac-coverage-check.md +0 -8
  241. package/src/seed/prompts/code-review/correctness-only.md +0 -8
  242. package/src/seed/prompts/code-review/full-spectrum-high.md +0 -8
  243. package/src/seed/prompts/code-review/hallucination-check.md +0 -8
  244. package/src/seed/prompts/code-review/public-api-compat.md +0 -8
  245. package/src/seed/prompts/code-review/readability-naming.md +0 -8
  246. package/src/seed/prompts/debugging/bug-as-failing-test.md +0 -8
  247. package/src/seed/prompts/debugging/git-bisect-regression.md +0 -8
  248. package/src/seed/prompts/debugging/instrument-intermittent-bug.md +0 -8
  249. package/src/seed/prompts/debugging/localize-pipeline-failure.md +0 -8
  250. package/src/seed/prompts/debugging/reproduce-then-diagnose.md +0 -8
  251. package/src/seed/prompts/documentation/adr-from-change.md +0 -8
  252. package/src/seed/prompts/documentation/module-readme.md +0 -8
  253. package/src/seed/prompts/documentation/onboarding-plan.md +0 -8
  254. package/src/seed/prompts/documentation/refresh-claude-md.md +0 -8
  255. package/src/seed/prompts/documentation/tsdoc-public-exports.md +0 -8
  256. package/src/seed/prompts/git-pr/conventional-commit.md +0 -8
  257. package/src/seed/prompts/git-pr/draft-pr-title-body.md +0 -8
  258. package/src/seed/prompts/git-pr/pre-commit-safety-sweep.md +0 -8
  259. package/src/seed/prompts/git-pr/release-notes-block.md +0 -8
  260. package/src/seed/prompts/git-pr/split-large-pr.md +0 -8
  261. package/src/seed/prompts/performance/bundle-startup-audit.md +0 -8
  262. package/src/seed/prompts/performance/complexity-audit.md +0 -8
  263. package/src/seed/prompts/performance/cpu-profile-hot-path.md +0 -8
  264. package/src/seed/prompts/performance/db-query-plan-review.md +0 -8
  265. package/src/seed/prompts/performance/memory-leak-hunt.md +0 -8
  266. package/src/seed/prompts/qa/api-contract-tests.md +0 -8
  267. package/src/seed/prompts/qa/e2e-critical-path.md +0 -8
  268. package/src/seed/prompts/qa/failing-test-for-bug.md +0 -8
  269. package/src/seed/prompts/qa/find-missing-test-coverage.md +0 -8
  270. package/src/seed/prompts/qa/stabilize-flaky-test.md +0 -8
  271. package/src/seed/prompts/qa/tdd-red-first.md +0 -8
  272. package/src/seed/prompts/qa/visual-regression-review.md +0 -8
  273. package/src/seed/prompts/qa/wcag-axe-scan.md +0 -8
  274. package/src/seed/prompts/refactoring/dead-code-sweep.md +0 -8
  275. package/src/seed/prompts/refactoring/extract-duplicated-pattern.md +0 -8
  276. package/src/seed/prompts/refactoring/modernize-legacy-file.md +0 -8
  277. package/src/seed/prompts/refactoring/reduce-cyclomatic-complexity.md +0 -8
  278. package/src/seed/prompts/refactoring/tighten-module-boundaries.md +0 -8
  279. package/src/seed/prompts/security/authz-audit.md +0 -8
  280. package/src/seed/prompts/security/crypto-correctness.md +0 -8
  281. package/src/seed/prompts/security/cwe-top-25-hunt.md +0 -8
  282. package/src/seed/prompts/security/dependency-audit.md +0 -8
  283. package/src/seed/prompts/security/ipc-boundary-hardening.md +0 -8
  284. package/src/seed/prompts/security/owasp-top-10-staged-diff.md +0 -8
  285. package/src/seed/prompts/security/secret-credential-scan.md +0 -8
  286. package/web/README.md +0 -41
  287. package/web/project-pages/logic/dist/logic.cjs +0 -4709
  288. package/web/project-pages/render.cjs +0 -70
  289. package/web/project-pages/renderer/dist/renderer.cjs +0 -18900
  290. package/web/project-pages/validate-summary.cjs +0 -62
@@ -11,14 +11,18 @@ const os = require('node:os');
11
11
  const { execFileSync } = require('node:child_process');
12
12
  const { POLL_INTERVAL_MS, loadGateThreshold } = require('./lib/schedulerConfig.cjs');
13
13
  const { opsPath } = require('./lib/opsOwnership.cjs');
14
+ const { effectivePaused } = require('./lib/upgradeDrain.cjs');
14
15
  const { checkPersonaImports } = require('./lib/personaImportHealth.cjs');
15
16
  const { checkDelegationReadiness } = require('./lib/delegationReadiness.cjs');
16
17
  const { resolvePrdsDirs } = require('./lib/prdLocations.cjs');
17
18
  const { migratePrds } = require('./lib/prdMigration.cjs');
18
19
  const queueStore = require('./lib/queueStore.cjs');
19
- const { computeStallSummary, SCHEDULER_STATE_PATH, FAILURE_STREAK_WARN_THRESHOLD, classifyQueueStarvation, STARVE_ESCALATION_MS } = require('./scheduler.cjs');
20
- const { findStarvedProjects } = require('./lib/schedulerBatch.cjs');
21
- const { AUDIT_LOG_PATH } = require('./lib/auditLog.cjs');
20
+ const schedulerPaths = require('./lib/schedulerPaths.cjs');
21
+ const { evaluateDispatchLiveness } = require('./lib/watchdogHelpers.cjs');
22
+ const { computeStallSummary, computeDepHistorySatisfaction, FAILURE_STREAK_ESCALATION_MS, classifyQueueStarvation, launchBlockedSlugs, STARVE_ESCALATION_MS, REVERIFY_INTERVAL_MS } = require('./scheduler.cjs');
23
+ const { findStarvedProjects, findUnresolvableDepRoots, DEFAULT_PROJECT_CWD, DEP_HISTORY_FAIL_OPEN } = require('./lib/schedulerBatch.cjs');
24
+ const { auditLogPath, readTail } = require('./lib/auditLog.cjs');
25
+ const { resolveBuildIdentity } = require('./lib/buildIdentity.cjs');
22
26
  const { DEFAULT_RUNS_DIR, computeReport, isRetentionEnabled, liveKeysFromJobs } = require('./lib/runLogRetention.cjs');
23
27
  const { allProjectCwds } = require('./lib/activeSessions.cjs');
24
28
 
@@ -40,6 +44,7 @@ const TICK_STALL_THRESHOLD_MS = TICK_STALL_MULTIPLIER * POLL_INTERVAL_MS;
40
44
  // scheduler-heartbeat.log is only appended to while Electron is running, so
41
45
  // a stale line here is silent-on-purpose, not evidence of anything.
42
46
  const HEARTBEAT_STALE_MS = 5 * 60_000;
47
+ const AUDIT_TAIL_BYTES = 1024 * 1024;
43
48
 
44
49
  function runCheck(cmd, cwd = PROJECT_ROOT) {
45
50
  try {
@@ -72,6 +77,7 @@ function readFreshHeartbeat(heartbeatPath) {
72
77
  } catch {
73
78
  return null;
74
79
  }
80
+ if (entry.degraded === true) return null; // a subsystem threw — utilization was not read
75
81
  if (typeof entry.ts !== 'number' || Date.now() - entry.ts > HEARTBEAT_STALE_MS) return null;
76
82
  return entry;
77
83
  }
@@ -92,7 +98,7 @@ function evaluateTickLiveness(queueState, heartbeat, now, runningCount) {
92
98
  ?? (() => { try { return require('./lib/sessionSlots.cjs').totalSlots(); } catch { return Infinity; } })();
93
99
 
94
100
  if (pending.length === 0) return { stalled: false, reason: 'no-pending-jobs' };
95
- if (queueState.paused) return { stalled: false, reason: 'paused' };
101
+ if (effectivePaused(queueState)) return { stalled: false, reason: queueState.paused ? 'paused' : 'draining' };
96
102
  if (config.enabled === false) return { stalled: false, reason: 'disabled' };
97
103
  if (running >= concurrencyCap) return { stalled: false, reason: 'at-capacity' };
98
104
  // 'manual' means the operator fires batches by hand — the scheduler is not
@@ -375,27 +381,50 @@ function evaluateEpicIndexHealth(cwds) {
375
381
  // not a failure, matches evaluatePrdMigrationHealth's fail-open-on-absence
376
382
  // spirit. Kept separate from the fs-touching check() call site, same pattern
377
383
  // as every other evaluate* helper in this file.
378
- function evaluateUsagePollerHealth(state, threshold = FAILURE_STREAK_WARN_THRESHOLD) {
384
+ //
385
+ // Ladder is driven by the shared usageCircuit's OWN open/closed state and
386
+ // how long it's been open (usageCircuitState/usageCircuitOpenedAt, persisted
387
+ // by scheduler.cjs's persistSchedulerState — this process never holds the
388
+ // live in-memory circuit itself) — not a binary consecutiveFailures<5 check,
389
+ // which used to read GREEN right up until the exact failure that also fired
390
+ // the one-time WARN, then stayed flatly non-GREEN forever after with no way
391
+ // to tell "just tripped" from "down for hours" apart:
392
+ // GREEN — circuit closed (or half_open probing after a fresh backoff).
393
+ // YELLOW — circuit open, less than FAILURE_STREAK_ESCALATION_MS (30 min).
394
+ // Still `ok: true` — the circuit's OWN open threshold (3
395
+ // consecutive failures, usageCircuit.cjs) is lower than the old
396
+ // 5-failure WARN, so treating YELLOW as `ok: false` here would
397
+ // silently tighten the overall `npm run health` rollup's failure
398
+ // bar from 5 down to 3. Graceful degradation (the whole point of
399
+ // the breaker) should not itself read as a critical failure.
400
+ // RED — circuit open for FAILURE_STREAK_ESCALATION_MS or longer —
401
+ // `ok: false`, matching the same 30-minute threshold the
402
+ // escalation ladder (warnFailureStreakIfNeeded) re-alerts on.
403
+ function evaluateUsagePollerHealth(state, redThresholdMs = FAILURE_STREAK_ESCALATION_MS) {
379
404
  if (!state || typeof state !== 'object') return { ok: true, applicable: false };
380
405
  const consecutiveFailures = typeof state.consecutiveFailures === 'number' ? state.consecutiveFailures : 0;
381
406
  const backoffMs = typeof state.backoffMs === 'number' ? state.backoffMs : null;
382
407
  const lastPollAt = typeof state.lastPollAt === 'number' ? state.lastPollAt : null;
383
- // Trips at the SAME count as scheduler.cjs's shouldWarnFailureStreak
384
- // (`consecutiveFailures >= threshold`) — the WARN and this non-GREEN must
385
- // fire together, not one failure apart, or an operator sees the log line
386
- // while the health dashboard still reads green.
387
- const ok = consecutiveFailures < threshold;
408
+ const circuitState = typeof state.usageCircuitState === 'string' ? state.usageCircuitState : 'closed';
409
+ const openedAt = typeof state.usageCircuitOpenedAt === 'number' ? state.usageCircuitOpenedAt : null;
410
+
411
+ if (circuitState !== 'open' && circuitState !== 'half_open') {
412
+ return { ok: true, applicable: true, color: 'GREEN', consecutiveFailures, backoffMs, lastPollAt };
413
+ }
414
+
415
+ const openMs = openedAt ? Math.max(0, Date.now() - openedAt) : 0;
416
+ const color = openMs >= redThresholdMs ? 'RED' : 'YELLOW';
388
417
  return {
389
- ok,
418
+ ok: color !== 'RED',
390
419
  applicable: true,
420
+ color,
391
421
  consecutiveFailures,
392
422
  backoffMs,
393
423
  lastPollAt,
394
- threshold,
395
- ...(ok ? {} : {
396
- message: `usage/rate-limit poller has ${consecutiveFailures} consecutive failures `
397
- + `(threshold ${threshold}), backoffMs=${backoffMs} — see logs scope=scheduler`,
398
- }),
424
+ openedAt,
425
+ openMs,
426
+ message: `usage/rate-limit poller circuit is ${circuitState}, open for ${Math.round(openMs / 60_000)}m (${color}) — `
427
+ + `${consecutiveFailures} consecutive failures, backoffMs=${backoffMs} — see logs scope=scheduler`,
399
428
  };
400
429
  }
401
430
 
@@ -416,6 +445,10 @@ function evaluateUsagePollerHealth(state, threshold = FAILURE_STREAK_WARN_THRESH
416
445
  // OWN forcing threshold; this is the "even the watchdog isn't helping
417
446
  // anymore" signal a human needs to see.
418
447
  const DISPATCH_STALL_THRESHOLD_MS = 2 * 60 * 60_000;
448
+ // Early-warning tier off the SAME launch-keyed clock (lastRunAt — a batch
449
+ // actually launched): still ok:true, but flagged so a slow leak surfaces
450
+ // well before the 2-hour cold-report threshold.
451
+ const DISPATCH_WARN_THRESHOLD_MS = 15 * 60_000;
419
452
 
420
453
  /**
421
454
  * evaluateQueueDispatchHealth(queueState, runningCount, now, thresholdMs) →
@@ -430,14 +463,18 @@ const DISPATCH_STALL_THRESHOLD_MS = 2 * 60 * 60_000;
430
463
  * `blocked` verdict therefore stays `ok: true` (never trips the health gate)
431
464
  * but is still reported by name so it isn't silently invisible either.
432
465
  */
433
- function evaluateQueueDispatchHealth(queueState, runningCount, now, thresholdMs = DISPATCH_STALL_THRESHOLD_MS) {
466
+ function evaluateQueueDispatchHealth(queueState, runningCount, now, thresholdMs = DISPATCH_STALL_THRESHOLD_MS, warnMs = DISPATCH_WARN_THRESHOLD_MS) {
467
+ // Clock = lastRunAt (launches), never lastDispatchAttemptAt (loop-alive
468
+ // heartbeat, refreshed by ticks that launch nothing). Cold process: no
469
+ // in-memory pause/boot stamps to fold in. Breaker-held rows count blocked.
434
470
  const verdict = classifyQueueStarvation({
435
471
  jobs: queueState?.jobs,
436
- paused: queueState?.paused,
472
+ paused: effectivePaused(queueState),
437
473
  runningCount,
438
- lastRunAtMs: Date.parse(queueState?.lastDispatchAttemptAt ?? ''),
474
+ lastRunAtMs: Date.parse(queueState?.lastRunAt ?? ''),
475
+ heldSlugs: launchBlockedSlugs(queueState?.jobs, queueState?.launchBlocks),
439
476
  now,
440
- thresholdMs,
477
+ thresholdMs: Math.min(warnMs, thresholdMs),
441
478
  });
442
479
  if (!verdict) return { ok: true };
443
480
  if (verdict.kind === 'blocked') {
@@ -445,18 +482,79 @@ function evaluateQueueDispatchHealth(queueState, runningCount, now, thresholdMs
445
482
  ok: true,
446
483
  blocked: true,
447
484
  pending: verdict.pending,
448
- message: `${verdict.pending} pending job(s) behind a blocked dependsOn chain — dispatch cannot help, needs a human`,
485
+ message: `${verdict.pending} pending job(s) behind a blocked dependsOn chain or open launch breaker — dispatch cannot help, needs a human`,
449
486
  };
450
487
  }
451
488
  const ageMin = Math.round(verdict.idleMs / 60_000);
489
+ const critical = verdict.idleMs >= thresholdMs;
452
490
  return {
453
- ok: false,
454
- starved: true,
491
+ ok: !critical,
492
+ ...(critical ? { starved: true } : { warn: true }),
455
493
  pending: verdict.pending,
456
494
  dispatchable: verdict.dispatchable,
457
495
  idleMs: verdict.idleMs,
458
- message: `${verdict.dispatchable} dispatchable pending job(s), 0 running, no dispatch attempt in ~${ageMin}m `
459
- + `(threshold ${Math.round(thresholdMs / 60_000)}m) — dispatch appears stuck`,
496
+ message: `${verdict.dispatchable} dispatchable pending job(s), 0 running, no launch in ~${ageMin}m `
497
+ + (critical
498
+ ? `(threshold ${Math.round(thresholdMs / 60_000)}m) — dispatch appears stuck`
499
+ : `(warn at ${Math.round(warnMs / 60_000)}m, fail at ${Math.round(thresholdMs / 60_000)}m) — dispatch may be stalling`),
500
+ };
501
+ }
502
+
503
+ /**
504
+ * evaluateUnresolvableDepHealth(jobs, satisfiedSlugsByCwd) →
505
+ * { ok, projects?: [{ cwd, roots, pending }], message? }
506
+ *
507
+ * Pure. A project whose EVERY pending row is held (directly or transitively)
508
+ * behind a dependsOn slug that names no live row and no history/archive
509
+ * record is fully blocked with a healthy engine — the 2026-09-18 fo-01 stall
510
+ * (21 pending rows, 0 running, one archived root, every other component
511
+ * GREEN). classifyQueueStarvation's 'blocked' kind cannot see this shape (it
512
+ * only walks failed/skipped ROWS), and evaluateQueueDispatchHealth keeps
513
+ * 'blocked' ok:true by design, so this is the component that fails it, naming
514
+ * the root slug(s). Reuses the picker's own resolver (findUnresolvableDepRoots)
515
+ * so health can never disagree with what dispatch actually holds. A project
516
+ * with anything running, or any dispatchable pending row, is not fully blocked.
517
+ * O(jobs + deps) per project; a fail-open project (history unreadable) is skipped.
518
+ */
519
+ function evaluateUnresolvableDepHealth(jobs, satisfiedSlugsByCwd) {
520
+ const byCwd = new Map();
521
+ for (const j of Array.isArray(jobs) ? jobs : []) {
522
+ if (!j) continue;
523
+ const cwd = j.cwd || DEFAULT_PROJECT_CWD;
524
+ if (!byCwd.has(cwd)) byCwd.set(cwd, []);
525
+ byCwd.get(cwd).push(j);
526
+ }
527
+ const projects = [];
528
+ for (const [cwd, rows] of byCwd) {
529
+ const satisfied = satisfiedSlugsByCwd?.get?.(cwd) ?? new Set();
530
+ if (satisfied === DEP_HISTORY_FAIL_OPEN) continue;
531
+ const pending = rows.filter((j) => j.status === 'pending');
532
+ if (pending.length === 0 || rows.some((j) => j.status === 'running')) continue;
533
+ const roots = findUnresolvableDepRoots(rows, satisfied);
534
+ if (roots.length === 0) continue;
535
+ const rootSet = new Set(roots);
536
+ const bare = (x) => String(x ?? '').replace(/^\d+-/, '');
537
+ const held = new Set();
538
+ let grew = true;
539
+ while (grew) {
540
+ grew = false;
541
+ for (const j of pending) {
542
+ if (held.has(j.slug)) continue;
543
+ const isHeld = (j.dependsOn ?? []).some((d) => rootSet.has(d)
544
+ || pending.some((p) => held.has(p.slug) && (p.slug === d || bare(p.slug) === bare(d))));
545
+ if (isHeld) { held.add(j.slug); grew = true; }
546
+ }
547
+ }
548
+ if (held.size === pending.length) projects.push({ cwd, roots, pending: pending.length });
549
+ }
550
+ if (projects.length === 0) return { ok: true };
551
+ return {
552
+ ok: false,
553
+ projects,
554
+ message: projects
555
+ .map((p) => `${p.cwd}: all ${p.pending} pending job(s) held behind unresolvable dependsOn root ${p.roots.join(', ')} `
556
+ + '(no live row, no history/archive record) — restore its completion record or fix the dependsOn')
557
+ .join('; '),
460
558
  };
461
559
  }
462
560
 
@@ -472,12 +570,9 @@ function evaluateQueueDispatchHealth(queueState, runningCount, now, thresholdMs
472
570
  * Missing/unreadable log → {} (no reasons known yet), never a throw.
473
571
  */
474
572
  function latestStarveEscalationReasons(auditLogPath) {
475
- let lines;
476
- try {
477
- lines = fs.readFileSync(auditLogPath, 'utf8').split('\n').filter(Boolean);
478
- } catch {
479
- return {};
480
- }
573
+ // Bounded reverse tail — the log is never rotated (28 MB+), and the latest
574
+ // escalation per cwd is by definition recent.
575
+ const lines = readTail(AUDIT_TAIL_BYTES, auditLogPath);
481
576
  const byCwd = {};
482
577
  for (const line of lines) {
483
578
  let rec;
@@ -491,6 +586,24 @@ function latestStarveEscalationReasons(auditLogPath) {
491
586
  return byCwd;
492
587
  }
493
588
 
589
+ /**
590
+ * evaluateBuildFreshness({running, installedOnDisk, repoHead}) → component
591
+ *
592
+ * Pure; each input is a short git sha or null. restartNeeded: the on-disk
593
+ * install differs from what the live heartbeat says is running. publishNeeded:
594
+ * repo HEAD differs from the installed build. Both are reported, neither is
595
+ * critical (never flips status.ok).
596
+ */
597
+ function evaluateBuildFreshness({ running = null, installedOnDisk = null, repoHead = null } = {}) {
598
+ const differs = (a, b) => !!a && !!b && !(a.startsWith(b) || b.startsWith(a));
599
+ const restartNeeded = differs(running, installedOnDisk);
600
+ const publishNeeded = differs(repoHead, installedOnDisk);
601
+ const notes = [];
602
+ if (restartNeeded) notes.push(`restart needed: running ${running}, installed ${installedOnDisk}`);
603
+ if (publishNeeded) notes.push(`publish needed: repo HEAD ${repoHead}, installed ${installedOnDisk}`);
604
+ return { ok: true, running, installedOnDisk, repoHead, restartNeeded, publishNeeded, ...(notes.length ? { note: notes.join('; ') } : {}) };
605
+ }
606
+
494
607
  /**
495
608
  * evaluateStarveEscalationHealth(jobs, now, thresholdMs, escalationReasons)
496
609
  * → { ok, projects?, message? }
@@ -532,6 +645,69 @@ function loadUsagePollerState(statePath) {
532
645
  }
533
646
  }
534
647
 
648
+ /**
649
+ * evaluateBlockingParkHealth(jobs, now, thresholdMs = REVERIFY_INTERVAL_MS) →
650
+ * { ok, parks?: [{ cwd, slug, verdict, ageMs, dependents }], message? }
651
+ *
652
+ * Pure. A `needs_review` row parked longer than one full periodic-reverify
653
+ * interval that STILL holds >= 1 pending dependent (transitively, through
654
+ * pending rows, via dependsOn — exact or bare slug, same matching as the
655
+ * picker) is a silently-blocking park: the self-heal ladder had a full pass
656
+ * and did not release it. 2026-09-18: 1229-fo-03 held 19 of 20 pending rows
657
+ * while every other component read GREEN. Parked-since is the row's last
658
+ * `to: needs_review` statusHistory entry (fallback finishedAt); a row with no
659
+ * recoverable timestamp is skipped rather than guessed at. O(jobs + deps).
660
+ */
661
+ function evaluateBlockingParkHealth(jobs, now, thresholdMs = REVERIFY_INTERVAL_MS) {
662
+ const rows = (Array.isArray(jobs) ? jobs : []).filter(Boolean);
663
+ const bare = (x) => String(x ?? '').replace(/^\d+-/, '');
664
+ const byCwd = new Map();
665
+ for (const j of rows) {
666
+ const cwd = j.cwd || DEFAULT_PROJECT_CWD;
667
+ if (!byCwd.has(cwd)) byCwd.set(cwd, []);
668
+ byCwd.get(cwd).push(j);
669
+ }
670
+ const parks = [];
671
+ for (const [cwd, group] of byCwd) {
672
+ const pending = group.filter((j) => j.status === 'pending');
673
+ if (pending.length === 0) continue;
674
+ for (const park of group) {
675
+ if (park.status !== 'needs_review') continue;
676
+ const history = Array.isArray(park.statusHistory) ? park.statusHistory : [];
677
+ let at = null;
678
+ for (let i = history.length - 1; i >= 0; i--) {
679
+ if (history[i]?.to === 'needs_review') { at = history[i].at; break; }
680
+ }
681
+ const since = Date.parse(at ?? park.finishedAt ?? '');
682
+ if (Number.isNaN(since) || now - since <= thresholdMs) continue;
683
+ const held = new Set();
684
+ const heldBy = (d, root) => root.slug === d || bare(root.slug) === bare(d);
685
+ let grew = true;
686
+ while (grew) {
687
+ grew = false;
688
+ for (const j of pending) {
689
+ if (held.has(j.slug)) continue;
690
+ if ((j.dependsOn ?? []).some((d) => heldBy(d, park) || pending.some((p) => held.has(p.slug) && heldBy(d, p)))) {
691
+ held.add(j.slug);
692
+ grew = true;
693
+ }
694
+ }
695
+ }
696
+ if (held.size > 0) parks.push({ cwd, slug: park.slug, verdict: park.verifierVerdict ?? null, ageMs: now - since, dependents: held.size });
697
+ }
698
+ }
699
+ if (parks.length === 0) return { ok: true };
700
+ return {
701
+ ok: false,
702
+ parks,
703
+ message: parks
704
+ .map((p) => `${p.slug} (${p.cwd}) parked needs_review${p.verdict ? `/${p.verdict}` : ''} for ${Math.round(p.ageMs / 60_000)}m `
705
+ + `(> one ${Math.round(REVERIFY_INTERVAL_MS / 60_000)}m reverify interval) is blocking ${p.dependents} pending dependent(s) — `
706
+ + 'the self-heal ladder did not release it; resolve it or scheduler_reset_job')
707
+ .join('; '),
708
+ };
709
+ }
710
+
535
711
  async function check() {
536
712
  const start = Date.now();
537
713
  const status = {
@@ -577,7 +753,7 @@ async function check() {
577
753
  };
578
754
 
579
755
  // 2. Check config directory exists and is writable.
580
- const configDir = path.join(os.homedir(), '.claude');
756
+ const configDir = schedulerPaths.claudeHome();
581
757
  try {
582
758
  await fsp.access(configDir, fs.constants.R_OK | fs.constants.W_OK);
583
759
  const stat = await fsp.stat(configDir);
@@ -599,11 +775,14 @@ async function check() {
599
775
  // 3. Check scheduler state (federated, 2026-07-31): machine runtime file +
600
776
  // per-project job shards merged via queueStore — the retired global
601
777
  // queue.json is no longer consulted.
602
- const queuePath = queueStore.MACHINE_STATE_PATH;
778
+ const queuePath = schedulerPaths.machineStatePath();
603
779
  let queueState = null;
604
780
  try {
605
781
  queueState = queueStore.readMergedSync();
606
782
  if (queueState.unreadable) throw new Error(queueState.unreadable);
783
+ for (const u of queueState.unreadableCwds ?? []) {
784
+ status.issues.push(`Scheduler queue shard quarantined for ${u.cwd}: ${u.error} — that project is skipped until it reads clean`);
785
+ }
607
786
  // A torn scheduler-machine.json is now recovered rather than halting
608
787
  // dispatch (queueStore.cjs's findLongestValidJsonPrefix) — but recovery
609
788
  // happening at all means the file failed to parse moments ago, which is
@@ -626,11 +805,7 @@ async function check() {
626
805
  const quarantinedCount = Object.values(queueState.jobs || {}).filter(
627
806
  (j) => j.status === 'quarantined'
628
807
  ).length;
629
- const heartbeatPath = path.join(
630
- os.homedir(),
631
- '.claude/session-manager/scheduler-heartbeat.log'
632
- );
633
- const heartbeat = readFreshHeartbeat(heartbeatPath);
808
+ const heartbeat = readFreshHeartbeat(schedulerPaths.heartbeatPath());
634
809
  const liveness = evaluateTickLiveness(queueState, heartbeat, Date.now(), runningCount);
635
810
 
636
811
  // Per-project rollup (PRD: monitoring must not collapse per-project
@@ -647,10 +822,13 @@ async function check() {
647
822
  .map(([cwd]) => cwd);
648
823
 
649
824
  status.components.scheduler_queue = {
650
- ok: !liveness.stalled && projectsPastThreshold.length === 0 && !queueState.machineStateRecovered,
825
+ ok: !liveness.stalled && projectsPastThreshold.length === 0 && !queueState.machineStateRecovered
826
+ && (queueState.unreadableCwds ?? []).length === 0,
651
827
  path: queuePath,
652
828
  jobs: Object.keys(queueState.jobs || {}).length,
653
829
  running: runningCount,
830
+ // Deliberate restart drain (lib/upgradeDrain.cjs) — reported so it is never read as a stall.
831
+ drain: queueState.drain?.active ? { since: queueState.drain.since ?? null, requestedAt: queueState.drain.requestedAt ?? null } : null,
654
832
  failed: failedCount,
655
833
  needsReview: needsReviewCount,
656
834
  quarantined: quarantinedCount,
@@ -658,6 +836,7 @@ async function check() {
658
836
  perProjectStall,
659
837
  tickLiveness: liveness.reason,
660
838
  machineStateRecovered: queueState.machineStateRecovered ?? false,
839
+ quarantinedCwds: (queueState.unreadableCwds ?? []).map((u) => u.cwd),
661
840
  machineStateRecoveryMode: queueState.machineStateRecoveryMode ?? null,
662
841
  // Informational only (PRD 1085): current 1-min loadavg per core vs the
663
842
  // launch-gate threshold, so a "nothing is launching" report can be
@@ -694,10 +873,31 @@ async function check() {
694
873
  );
695
874
  }
696
875
  }
876
+ // Same classification the external watchdog logs (observability only).
877
+ const dispatchLiveness = evaluateDispatchLiveness(heartbeat, now);
878
+ status.components.scheduler_queue.dispatchLiveness = dispatchLiveness;
879
+ if (dispatchLiveness.dead) {
880
+ status.issues.push('Scheduler dispatch appears dead: heartbeat is fresh but nothing has launched in 30m+ with dispatchable work pending and nothing running');
881
+ }
697
882
  status.components.queue_dispatch = evaluateQueueDispatchHealth(queueState, runningCount, now);
698
883
  if (!status.components.queue_dispatch.ok || status.components.queue_dispatch.blocked) {
699
884
  status.issues.push(`Queue dispatch: ${status.components.queue_dispatch.message}`);
700
885
  }
886
+ try {
887
+ status.components.queue_dep_roots = evaluateUnresolvableDepHealth(
888
+ queueState.jobs, await computeDepHistorySatisfaction(queueState),
889
+ );
890
+ } catch (e) {
891
+ status.components.queue_dep_roots = { ok: true, error: `dep-root check failed: ${e?.message}` };
892
+ }
893
+ if (!status.components.queue_dep_roots.ok) {
894
+ status.issues.push(`Queue dependsOn: ${status.components.queue_dep_roots.message}`);
895
+ }
896
+
897
+ status.components.blocking_parks = evaluateBlockingParkHealth(queueState.jobs, now);
898
+ if (!status.components.blocking_parks.ok) {
899
+ status.issues.push(`Blocking park: ${status.components.blocking_parks.message}`);
900
+ }
701
901
 
702
902
  // Per-project starve escalation (bounded consequence for project_starved
703
903
  // — see runStarveEscalationSweep in scheduler.cjs). Distinct from
@@ -705,7 +905,7 @@ async function check() {
705
905
  // "has any ONE project been starved past the LATER escalation threshold",
706
906
  // the exact condition the 2026-09-12 19h Bilko starve went unreported by.
707
907
  status.components.project_starve_escalation = evaluateStarveEscalationHealth(
708
- queueState.jobs, now, STARVE_ESCALATION_MS, latestStarveEscalationReasons(AUDIT_LOG_PATH),
908
+ queueState.jobs, now, STARVE_ESCALATION_MS, latestStarveEscalationReasons(auditLogPath()),
709
909
  );
710
910
  if (!status.components.project_starve_escalation.ok) {
711
911
  status.issues.push(status.components.project_starve_escalation.message);
@@ -746,7 +946,7 @@ async function check() {
746
946
  // failure. A CORRUPT file (the torn-write class fixed for the sibling
747
947
  // scheduler-machine.json in f56bdc0) is NOT the same as missing — surfaced
748
948
  // as non-GREEN rather than fail-open, via loadUsagePollerState below.
749
- const loaded = loadUsagePollerState(SCHEDULER_STATE_PATH);
949
+ const loaded = loadUsagePollerState(schedulerPaths.schedulerStatePath());
750
950
  if (loaded.missing) {
751
951
  status.components.usage_poller = { ok: true, applicable: false };
752
952
  } else if (loaded.errorMessage) {
@@ -790,10 +990,7 @@ async function check() {
790
990
  // of a stale installed build vs. the git repo — it fires regardless of
791
991
  // *why* the build is stale (the 2026-07-31 burrow-project ENOENT + the
792
992
  // 223-file/189-resurrected-job incident both trace back to this).
793
- const legacyPrdsDir = path.join(
794
- os.homedir(),
795
- '.claude/session-manager/scheduled-plans/prds'
796
- );
993
+ const legacyPrdsDir = schedulerPaths.prdsRoot();
797
994
  try {
798
995
  const migrationResult = await migratePrds(legacyPrdsDir);
799
996
  status.components.prd_migration = evaluatePrdMigrationHealth(migrationResult, legacyPrdsDir);
@@ -809,7 +1006,7 @@ async function check() {
809
1006
  }
810
1007
 
811
1008
  // 5. Check transcripts directory (where live session logs are tailed).
812
- const projectsDir = path.join(os.homedir(), '.claude/projects');
1009
+ const projectsDir = schedulerPaths.claudeProjectsDir();
813
1010
  try {
814
1011
  await fsp.access(projectsDir, fs.constants.R_OK);
815
1012
  status.components.transcripts_dir = {
@@ -826,10 +1023,7 @@ async function check() {
826
1023
  }
827
1024
 
828
1025
  // 6. Check session-manager's own logs (informational, not blocking).
829
- const smLogsDir = path.join(
830
- os.homedir(),
831
- '.claude/session-manager/logs'
832
- );
1026
+ const smLogsDir = schedulerPaths.watchdogLogsDir();
833
1027
  let logAge = null;
834
1028
  try {
835
1029
  const files = await fsp.readdir(smLogsDir);
@@ -944,11 +1138,29 @@ async function check() {
944
1138
  status.issues.push(`Epic index health check failed: ${e.message}`);
945
1139
  }
946
1140
 
1141
+ // 6.7. Build freshness (informational): running heartbeat vs installed
1142
+ // build-info vs repo HEAD.
1143
+ try {
1144
+ let repoHead = null;
1145
+ try {
1146
+ repoHead = execFileSync('git', ['-C', PROJECT_ROOT, 'rev-parse', '--short', 'HEAD'], {
1147
+ timeout: 5000, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'],
1148
+ }).trim() || null;
1149
+ } catch { /* not a git checkout */ }
1150
+ status.components.build = evaluateBuildFreshness({
1151
+ running: readFreshHeartbeat(schedulerPaths.heartbeatPath())?.build?.codeSha ?? null,
1152
+ installedOnDisk: resolveBuildIdentity().codeSha ?? null,
1153
+ repoHead,
1154
+ });
1155
+ } catch (e) {
1156
+ status.components.build = { ok: true, error: e.message };
1157
+ }
1158
+
947
1159
  // 7. Summary scoring: ok if all critical components pass.
948
1160
  // Critical: nodejs, config dir, typescript, build artifact, test infrastructure.
949
1161
  // Non-fatal: scheduler/transcripts dirs may not exist on fresh install.
950
1162
  // Informational: app log age (shows if app is running, but not blocking).
951
- const criticalComponents = ['nodejs', 'config_dir', 'typescript', 'build_artifact', 'test_infrastructure', 'scheduler_queue', 'queue_dispatch', 'project_starve_escalation', 'usage_poller', 'prd_migration', 'claude_md_budget', 'delegation_chain'];
1163
+ const criticalComponents = ['nodejs', 'config_dir', 'typescript', 'build_artifact', 'test_infrastructure', 'scheduler_queue', 'queue_dispatch', 'queue_dep_roots', 'blocking_parks', 'project_starve_escalation', 'usage_poller', 'prd_migration', 'claude_md_budget', 'delegation_chain'];
952
1164
  status.ok = criticalComponents.every((c) => status.components[c]?.ok !== false);
953
1165
 
954
1166
  status.elapsedMs = Date.now() - start;
@@ -980,9 +1192,13 @@ module.exports = {
980
1192
  evaluateUsagePollerHealth,
981
1193
  loadUsagePollerState,
982
1194
  evaluateQueueDispatchHealth,
1195
+ evaluateUnresolvableDepHealth,
1196
+ evaluateBlockingParkHealth,
983
1197
  evaluateStarveEscalationHealth,
1198
+ evaluateBuildFreshness,
984
1199
  latestStarveEscalationReasons,
985
1200
  DISPATCH_STALL_THRESHOLD_MS,
1201
+ DISPATCH_WARN_THRESHOLD_MS,
986
1202
  TICK_STALL_THRESHOLD_MS,
987
1203
  HEARTBEAT_STALE_MS,
988
1204
  };
@@ -23,8 +23,8 @@
23
23
  'use strict';
24
24
 
25
25
  const path = require('node:path');
26
- const os = require('node:os');
27
26
  const fsp = require('node:fs/promises');
27
+ const { heapSnapshotDir } = require('./lib/schedulerPaths.cjs');
28
28
 
29
29
  const CHANNEL = 'diagnostics:heap-snapshot';
30
30
  const DEFAULT_TIMEOUT_MS = 3 * 60 * 1000; // 3 min — snapshots can be multi-hundred-MB
@@ -34,7 +34,7 @@ function isEnabled() {
34
34
  }
35
35
 
36
36
  function snapshotDir() {
37
- return path.join(os.homedir(), '.claude', 'session-manager');
37
+ return heapSnapshotDir();
38
38
  }
39
39
 
40
40
  function timestampedFilename(now = new Date()) {
@@ -643,7 +643,7 @@ function resetIntradayRegistryForTests() {
643
643
  /**
644
644
  * Pure walk-and-parse step (no rollup write) — split out from
645
645
  * refreshIntradayToday() so it can be benchmarked read-only against the real
646
- * ~/.claude/projects (see scripts/bench-intraday-walk.cjs) without touching
646
+ * ~/.claude/projects without touching
647
647
  * the rollup file on disk.
648
648
  */
649
649
  async function computeIntradayBuckets() {