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
@@ -0,0 +1,39 @@
1
+ /**
2
+ * health-build-freshness.test.cjs — evaluateBuildFreshness separates
3
+ * restart-needed (installed differs from running) from publish-needed
4
+ * (repo HEAD differs from installed); both reported, never critical.
5
+ *
6
+ * Run: timeout 120 npx vitest run src/main/__tests__/health-build-freshness.test.cjs
7
+ */
8
+ 'use strict';
9
+
10
+ import { test, expect } from 'vitest';
11
+ const { evaluateBuildFreshness } = require('../health.cjs');
12
+
13
+ test('all shas equal → nothing needed', () => {
14
+ const r = evaluateBuildFreshness({ running: 'abc1234', installedOnDisk: 'abc1234', repoHead: 'abc1234' });
15
+ expect(r).toMatchObject({ ok: true, restartNeeded: false, publishNeeded: false });
16
+ });
17
+
18
+ test('installed differs from running → restart needed only', () => {
19
+ const r = evaluateBuildFreshness({ running: 'aaa1111', installedOnDisk: 'bbb2222', repoHead: 'bbb2222' });
20
+ expect(r).toMatchObject({ ok: true, restartNeeded: true, publishNeeded: false });
21
+ });
22
+
23
+ test('repo HEAD differs from installed → publish needed only', () => {
24
+ const r = evaluateBuildFreshness({ running: 'bbb2222', installedOnDisk: 'bbb2222', repoHead: 'ccc3333' });
25
+ expect(r).toMatchObject({ ok: true, restartNeeded: false, publishNeeded: true });
26
+ });
27
+
28
+ test('both differ → both reported, still ok (non-critical)', () => {
29
+ const r = evaluateBuildFreshness({ running: 'aaa1111', installedOnDisk: 'bbb2222', repoHead: 'ccc3333' });
30
+ expect(r).toMatchObject({ ok: true, restartNeeded: true, publishNeeded: true });
31
+ expect(r.note).toMatch(/restart needed/);
32
+ expect(r.note).toMatch(/publish needed/);
33
+ });
34
+
35
+ test('unknown inputs never claim a difference; short/long sha prefixes match', () => {
36
+ expect(evaluateBuildFreshness({})).toMatchObject({ restartNeeded: false, publishNeeded: false });
37
+ expect(evaluateBuildFreshness({ running: null, installedOnDisk: 'abc1234', repoHead: 'abc1234567890' }))
38
+ .toMatchObject({ restartNeeded: false, publishNeeded: false });
39
+ });
@@ -84,7 +84,21 @@ test('failing check with no fix omits the "(fix: ...)" suffix', () => {
84
84
  });
85
85
 
86
86
  test('end-to-end: check() reports delegation_chain for this repo, expected to pass here', async () => {
87
- const status = await check();
87
+ // Probes THIS machine's real user-scope config (~/.claude.json MCP registration, guard
88
+ // shims), so it reads the real home — not the per-run sandbox HOME. Read-only: scheduler
89
+ // state stays redirected to the sandbox (SM_SCHEDULER_HOME re-pinned after the HOME swap).
90
+ const sandboxHome = process.env.HOME;
91
+ const sandboxSchedulerHome = process.env.SM_SCHEDULER_HOME;
92
+ process.env.HOME = require('node:os').userInfo().homedir;
93
+ if (sandboxSchedulerHome !== undefined) process.env.SM_SCHEDULER_HOME = sandboxSchedulerHome;
94
+ process.env.SM_ALLOW_LIVE_ROOT_READS = '1';
95
+ let status;
96
+ try {
97
+ status = await check();
98
+ } finally {
99
+ delete process.env.SM_ALLOW_LIVE_ROOT_READS;
100
+ process.env.HOME = sandboxHome;
101
+ }
88
102
  expect(status.components.delegation_chain).toBeDefined();
89
103
  expect(status.components.delegation_chain.checks).toHaveLength(9);
90
104
  const ids = status.components.delegation_chain.checks.map((c) => c.id);
@@ -2,7 +2,7 @@
2
2
  * health-queue-dispatch.test.cjs — `npm run health` must report non-GREEN
3
3
  * when there is at least one DISPATCHABLE pending job (reusing
4
4
  * classifyQueueStarvation's own 'starved' verdict, not merely "pending"),
5
- * nothing running, and no dispatch attempt in a long time — the exact shape
5
+ * nothing running, and no launch in a long time — the exact shape
6
6
  * of the 2026-09-11 incident (27 pending across two projects, 0 running, for
7
7
  * days, every other health surface reporting green). A dependsOn-blocked
8
8
  * queue must NOT trip this the same way: ticking can't fix a blocked chain,
@@ -18,7 +18,7 @@
18
18
  'use strict';
19
19
 
20
20
  import { test, expect } from 'vitest';
21
- const { evaluateQueueDispatchHealth, DISPATCH_STALL_THRESHOLD_MS } = require('../health.cjs');
21
+ const { evaluateQueueDispatchHealth, DISPATCH_STALL_THRESHOLD_MS, DISPATCH_WARN_THRESHOLD_MS } = require('../health.cjs');
22
22
 
23
23
  const CWD = '/home/bilko/Projects/starry-night-ships';
24
24
  const NOW = Date.parse('2026-09-11T12:00:00.000Z');
@@ -28,7 +28,7 @@ test('a dispatchable pending job, 0 running, stale past the threshold — non-GR
28
28
  const state = {
29
29
  jobs: [{ slug: 'a', cwd: CWD, status: 'pending', dependsOn: [] }],
30
30
  paused: null,
31
- lastDispatchAttemptAt: idleFor(DISPATCH_STALL_THRESHOLD_MS + 60_000),
31
+ lastRunAt: idleFor(DISPATCH_STALL_THRESHOLD_MS + 60_000),
32
32
  };
33
33
  const result = evaluateQueueDispatchHealth(state, 0, NOW);
34
34
  expect(result.ok).toBe(false);
@@ -44,7 +44,7 @@ test('every pending row behind a blocked dependsOn chain stays GREEN, reported a
44
44
  { slug: 'blocked-child', cwd: CWD, status: 'pending', dependsOn: ['dead'] },
45
45
  ],
46
46
  paused: null,
47
- lastDispatchAttemptAt: idleFor(DISPATCH_STALL_THRESHOLD_MS + 60_000),
47
+ lastRunAt: idleFor(DISPATCH_STALL_THRESHOLD_MS + 60_000),
48
48
  };
49
49
  const result = evaluateQueueDispatchHealth(state, 0, NOW);
50
50
  expect(result.ok).toBe(true);
@@ -57,7 +57,7 @@ test('healthy queue (recent dispatch attempt) is GREEN with no verdict', () => {
57
57
  const state = {
58
58
  jobs: [{ slug: 'a', cwd: CWD, status: 'pending', dependsOn: [] }],
59
59
  paused: null,
60
- lastDispatchAttemptAt: idleFor(60_000),
60
+ lastRunAt: idleFor(60_000),
61
61
  };
62
62
  const result = evaluateQueueDispatchHealth(state, 0, NOW);
63
63
  expect(result).toEqual({ ok: true });
@@ -67,7 +67,7 @@ test('nothing pending is GREEN regardless of how stale the dispatch timestamp is
67
67
  const state = {
68
68
  jobs: [{ slug: 'a', cwd: CWD, status: 'completed' }],
69
69
  paused: null,
70
- lastDispatchAttemptAt: idleFor(DISPATCH_STALL_THRESHOLD_MS + 60_000),
70
+ lastRunAt: idleFor(DISPATCH_STALL_THRESHOLD_MS + 60_000),
71
71
  };
72
72
  const result = evaluateQueueDispatchHealth(state, 0, NOW);
73
73
  expect(result).toEqual({ ok: true });
@@ -77,8 +77,59 @@ test('work already running is GREEN — the queue is flowing', () => {
77
77
  const state = {
78
78
  jobs: [{ slug: 'a', cwd: CWD, status: 'pending', dependsOn: [] }],
79
79
  paused: null,
80
- lastDispatchAttemptAt: idleFor(DISPATCH_STALL_THRESHOLD_MS + 60_000),
80
+ lastRunAt: idleFor(DISPATCH_STALL_THRESHOLD_MS + 60_000),
81
81
  };
82
82
  const result = evaluateQueueDispatchHealth(state, 1, NOW);
83
83
  expect(result).toEqual({ ok: true });
84
84
  });
85
+
86
+ test('the two tiers are exported: 15-minute warn, 2-hour cold report', () => {
87
+ expect(DISPATCH_WARN_THRESHOLD_MS).toBe(15 * 60_000);
88
+ expect(DISPATCH_STALL_THRESHOLD_MS).toBe(2 * 60 * 60_000);
89
+ });
90
+
91
+ test('starved between the warn and cold thresholds — ok:true but flagged warn', () => {
92
+ const state = {
93
+ jobs: [{ slug: 'a', cwd: CWD, status: 'pending', dependsOn: [] }],
94
+ paused: null,
95
+ lastRunAt: idleFor(DISPATCH_WARN_THRESHOLD_MS + 60_000),
96
+ };
97
+ const result = evaluateQueueDispatchHealth(state, 0, NOW);
98
+ expect(result.ok).toBe(true);
99
+ expect(result.warn).toBe(true);
100
+ expect(result.starved).toBeUndefined();
101
+ expect(result.dispatchable).toBe(1);
102
+ });
103
+
104
+ test('below the warn threshold is plain GREEN', () => {
105
+ const state = {
106
+ jobs: [{ slug: 'a', cwd: CWD, status: 'pending', dependsOn: [] }],
107
+ paused: null,
108
+ lastRunAt: idleFor(DISPATCH_WARN_THRESHOLD_MS - 60_000),
109
+ };
110
+ expect(evaluateQueueDispatchHealth(state, 0, NOW)).toEqual({ ok: true });
111
+ });
112
+
113
+ test('ticks that launch nothing (fresh lastDispatchAttemptAt, stale lastRunAt) still go non-GREEN', () => {
114
+ const state = {
115
+ jobs: [{ slug: 'a', cwd: CWD, status: 'pending', dependsOn: [] }],
116
+ paused: null,
117
+ lastDispatchAttemptAt: idleFor(5_000),
118
+ lastRunAt: idleFor(DISPATCH_STALL_THRESHOLD_MS + 60_000),
119
+ };
120
+ const result = evaluateQueueDispatchHealth(state, 0, NOW);
121
+ expect(result.ok).toBe(false);
122
+ expect(result.starved).toBe(true);
123
+ });
124
+
125
+ test('rows held by an open launch breaker read blocked, not starved', () => {
126
+ const state = {
127
+ jobs: [{ slug: 'a', cwd: CWD, status: 'pending', dependsOn: [] }],
128
+ paused: null,
129
+ lastRunAt: idleFor(DISPATCH_STALL_THRESHOLD_MS + 60_000),
130
+ launchBlocks: { default: { kind: 'auth', at: NOW - 1000 } },
131
+ };
132
+ const result = evaluateQueueDispatchHealth(state, 0, NOW);
133
+ expect(result.ok).toBe(true);
134
+ expect(result.blocked).toBe(true);
135
+ });
@@ -6,12 +6,12 @@
6
6
  * since they're pure/fs-isolated — no need to mock os.homedir() through the
7
7
  * full check() to cover the liveness logic.
8
8
  *
9
- * Run: timeout 120 node --test src/main/__tests__/health-tick-liveness.test.cjs
9
+ * Run: timeout 300 npx vitest run src/main/__tests__/health-tick-liveness.test.cjs
10
10
  */
11
11
 
12
12
  'use strict';
13
13
 
14
- const { test } = require('node:test');
14
+ import { test } from 'vitest';
15
15
  const assert = require('node:assert/strict');
16
16
  const fs = require('node:fs');
17
17
  const os = require('node:os');
@@ -72,6 +72,7 @@ test('no false positive: running is already at concurrencyCap', () => {
72
72
  { slug: '544-stuck-job', status: 'pending' },
73
73
  ],
74
74
  });
75
+ q.slotCap = 3; // machine-wide sessionSlots pool is the only cap now
75
76
  const result = evaluateTickLiveness(q, null, NOW);
76
77
  assert.strictEqual(result.stalled, false);
77
78
  assert.strictEqual(result.reason, 'at-capacity');
@@ -105,9 +106,16 @@ test('degrades honestly: heartbeat missing/stale means utilization cannot be rul
105
106
  assert.strictEqual(result.reason, 'cannot-verify-utilization');
106
107
  });
107
108
 
108
- test('does not caveat when firePolicy is not when-available (utilization is irrelevant, so no heartbeat needed)', () => {
109
+ test('manual firePolicy: pending work with free capacity is configured behaviour, not a stall', () => {
109
110
  const q = baseQueue({ config: { ...baseQueue().config, firePolicy: 'manual' } });
110
111
  const result = evaluateTickLiveness(q, null, NOW);
112
+ assert.strictEqual(result.stalled, false);
113
+ assert.strictEqual(result.reason, 'manual-fire-policy');
114
+ });
115
+
116
+ test('does not caveat when firePolicy is neither when-available nor manual (no heartbeat needed)', () => {
117
+ const q = baseQueue({ config: { ...baseQueue().config, firePolicy: 'always' } });
118
+ const result = evaluateTickLiveness(q, null, NOW);
111
119
  assert.strictEqual(result.stalled, true);
112
120
  });
113
121
 
@@ -1,12 +1,15 @@
1
1
  /**
2
- * health-usage-poller.test.cjs — `npm run health` must report non-GREEN when
3
- * the usage/rate-limit poller's (scheduler.cjs pollLoop) consecutiveFailures
4
- * streak, persisted to ~/.claude/session-manager/scheduler-state.json,
5
- * reaches FAILURE_STREAK_WARN_THRESHOLD — the SAME count that gates the
6
- * one-time opsErrorLog WARN (rateLimitPollerStreak.test.cjs shouldWarnFailureStreak
7
- * fires at `>= threshold`), so the log line and the health-GREEN flip happen
8
- * on the exact same failure, not one apart. Exercises evaluateUsagePollerHealth()
9
- * and loadUsagePollerState() directly since both are pure/file-scoped, matching
2
+ * health-usage-poller.test.cjs — `npm run health` must report GREEN / YELLOW
3
+ * / RED for the usage/rate-limit poller (scheduler.cjs pollLoop), driven by
4
+ * the shared usageCircuit's OWN open/closed state and how long it's been
5
+ * open (usageCircuitState/usageCircuitOpenedAt, persisted to
6
+ * ~/.claude/session-manager/scheduler-state.json by persistSchedulerState —
7
+ * this is a separate `npm run health` process, so it reads the PERSISTED
8
+ * circuit fields rather than holding the live in-memory breaker) — not the
9
+ * old binary consecutiveFailures<5 check, which read flatly non-GREEN
10
+ * forever after the fifth failure with no way to distinguish "just tripped"
11
+ * from "down for hours." Exercises evaluateUsagePollerHealth() and
12
+ * loadUsagePollerState() directly since both are pure/file-scoped, matching
10
13
  * every other evaluate* helper's test pattern in this file — deliberately NOT
11
14
  * driving the full (slow, machine-state-coupled) check() for this.
12
15
  *
@@ -20,44 +23,88 @@ import { mkdtempSync, writeFileSync, rmSync } from 'node:fs';
20
23
  import { tmpdir } from 'node:os';
21
24
  import { join } from 'node:path';
22
25
  const { evaluateUsagePollerHealth, loadUsagePollerState } = require('../health.cjs');
23
- const { FAILURE_STREAK_WARN_THRESHOLD } = require('../scheduler.cjs');
26
+ const { FAILURE_STREAK_ESCALATION_MS } = require('../scheduler.cjs');
24
27
 
25
28
  test('missing state (fresh install / poller never ran) is ok, not applicable', () => {
26
29
  expect(evaluateUsagePollerHealth(null)).toEqual({ ok: true, applicable: false });
27
30
  expect(evaluateUsagePollerHealth(undefined)).toEqual({ ok: true, applicable: false });
28
31
  });
29
32
 
30
- test('consecutiveFailures below the threshold is GREEN', () => {
31
- const result = evaluateUsagePollerHealth({ consecutiveFailures: FAILURE_STREAK_WARN_THRESHOLD - 1, backoffMs: 240_000, lastPollAt: Date.now() });
33
+ test('circuit closed is GREEN, regardless of a nonzero consecutiveFailures count', () => {
34
+ const result = evaluateUsagePollerHealth({ usageCircuitState: 'closed', consecutiveFailures: 2, backoffMs: 60_000, lastPollAt: Date.now() });
32
35
  expect(result.ok).toBe(true);
33
36
  expect(result.applicable).toBe(true);
37
+ expect(result.color).toBe('GREEN');
34
38
  expect(result.message).toBeUndefined();
35
39
  });
36
40
 
37
- test('consecutiveFailures AT the threshold is already non-GREEN — same count the WARN fires at', () => {
38
- // Must match shouldWarnFailureStreak's `>= threshold` exactly (scheduler.cjs)
39
- // so the opsErrorLog WARN and this health flip happen on the same failure.
40
- const result = evaluateUsagePollerHealth({ consecutiveFailures: FAILURE_STREAK_WARN_THRESHOLD, backoffMs: 480_000, lastPollAt: Date.now() });
41
- expect(result.ok).toBe(false);
42
- expect(result.message).toMatch(new RegExp(`${FAILURE_STREAK_WARN_THRESHOLD} consecutive failures`));
41
+ test('no usageCircuitState field at all (pre-upgrade sidecar) defaults to closed -> GREEN', () => {
42
+ const result = evaluateUsagePollerHealth({ consecutiveFailures: 0, backoffMs: null, lastPollAt: Date.now() });
43
+ expect(result.ok).toBe(true);
44
+ expect(result.color).toBe('GREEN');
43
45
  });
44
46
 
45
- test('consecutiveFailures well past the threshold is non-GREEN with a diagnostic message', () => {
46
- const result = evaluateUsagePollerHealth({ consecutiveFailures: 57, backoffMs: 480_000, lastPollAt: Date.now() });
47
+ test('circuit open for under FAILURE_STREAK_ESCALATION_MS (30 min) is YELLOW, not GREEN — but still ok:true, not yet critical', () => {
48
+ const now = Date.now();
49
+ const result = evaluateUsagePollerHealth({
50
+ usageCircuitState: 'open',
51
+ usageCircuitOpenedAt: now - 5 * 60_000, // 5 minutes ago
52
+ consecutiveFailures: 6,
53
+ backoffMs: 480_000,
54
+ lastPollAt: now,
55
+ });
56
+ // ok:true is deliberate — the circuit's own open threshold (3 consecutive
57
+ // failures) is LOWER than the old 5-failure WARN, so an ok:false YELLOW
58
+ // would silently tighten the overall `npm run health` rollup's failure bar
59
+ // from 5 down to 3. Graceful degradation must not itself read as critical.
60
+ expect(result.ok).toBe(true);
61
+ expect(result.color).toBe('YELLOW');
62
+ expect(result.message).toMatch(/YELLOW/);
63
+ });
64
+
65
+ test('circuit open for AT LEAST FAILURE_STREAK_ESCALATION_MS (30 min) is RED — ok:false, this is the critical state', () => {
66
+ const now = Date.now();
67
+ const result = evaluateUsagePollerHealth({
68
+ usageCircuitState: 'open',
69
+ usageCircuitOpenedAt: now - FAILURE_STREAK_ESCALATION_MS,
70
+ consecutiveFailures: 57,
71
+ backoffMs: 480_000,
72
+ lastPollAt: now,
73
+ });
47
74
  expect(result.ok).toBe(false);
75
+ expect(result.color).toBe('RED');
48
76
  expect(result.consecutiveFailures).toBe(57);
49
77
  expect(result.backoffMs).toBe(480_000);
50
- expect(result.message).toMatch(/57 consecutive failures/);
78
+ expect(result.message).toMatch(/RED/);
79
+ });
80
+
81
+ test('half_open (mid-probe) is treated the same as open — not GREEN just because a probe is in flight', () => {
82
+ const now = Date.now();
83
+ const result = evaluateUsagePollerHealth({
84
+ usageCircuitState: 'half_open',
85
+ usageCircuitOpenedAt: now - 60_000,
86
+ consecutiveFailures: 5,
87
+ lastPollAt: now,
88
+ });
89
+ expect(result.ok).toBe(true); // still YELLOW-shaped (under the red threshold), so still ok:true
90
+ expect(result.color).toBe('YELLOW');
51
91
  });
52
92
 
53
- test('respects a custom threshold argument', () => {
54
- expect(evaluateUsagePollerHealth({ consecutiveFailures: 3 }, 3).ok).toBe(false);
55
- expect(evaluateUsagePollerHealth({ consecutiveFailures: 2 }, 3).ok).toBe(true);
93
+ test('respects a custom redThresholdMs argument', () => {
94
+ const now = Date.now();
95
+ const openedAt = now - 10_000;
96
+ const red = evaluateUsagePollerHealth({ usageCircuitState: 'open', usageCircuitOpenedAt: openedAt, consecutiveFailures: 5 }, 5_000);
97
+ expect(red.color).toBe('RED');
98
+ expect(red.ok).toBe(false);
99
+ const yellow = evaluateUsagePollerHealth({ usageCircuitState: 'open', usageCircuitOpenedAt: openedAt, consecutiveFailures: 5 }, 60_000);
100
+ expect(yellow.color).toBe('YELLOW');
101
+ expect(yellow.ok).toBe(true);
56
102
  });
57
103
 
58
104
  test('tolerates a malformed/partial state object without throwing', () => {
59
105
  const result = evaluateUsagePollerHealth({});
60
106
  expect(result.ok).toBe(true);
107
+ expect(result.color).toBe('GREEN');
61
108
  expect(result.consecutiveFailures).toBe(0);
62
109
  });
63
110
 
@@ -135,7 +135,7 @@ test('appendRollupDays: compacts (dedupes) once file exceeds threshold', async (
135
135
  { date: '2026-07-01', projectDir: 'p', modelId: 'sonnet', inputTokens: 2 },
136
136
  ]);
137
137
  await historyRollup.compact();
138
- const raw = fs.readFileSync(historyRollup.ROLLUP_PATH, 'utf8');
138
+ const raw = fs.readFileSync(historyRollup.historyRollupPath(), 'utf8');
139
139
  const nonEmptyLines = raw.split('\n').filter((l) => l.trim());
140
140
  expect(nonEmptyLines.length).toBe(1);
141
141
  expect(JSON.parse(nonEmptyLines[0]).inputTokens).toBe(2);
@@ -305,7 +305,7 @@ test('refreshIntradayToday: upserts a provisional (non-finalized) line for today
305
305
  // duplicates — today's line for the same key once the file is compacted.
306
306
  await historyAggregator.refreshIntradayToday();
307
307
  await historyRollup.compact();
308
- const raw = fs.readFileSync(historyRollup.ROLLUP_PATH, 'utf8');
308
+ const raw = fs.readFileSync(historyRollup.historyRollupPath(), 'utf8');
309
309
  const matchingLines = raw.split('\n').filter((l) => {
310
310
  if (!l.trim()) return false;
311
311
  const obj = JSON.parse(l);
@@ -14,10 +14,10 @@
14
14
  * differences between log sources don't break the match.
15
15
  *
16
16
  * Does NOT spawn claude -p. Run:
17
- * timeout 120 node --test src/main/__tests__/kg-augment.test.cjs
17
+ * timeout 300 npx vitest run src/main/__tests__/kg-augment.test.cjs
18
18
  */
19
19
 
20
- const { test, describe, before, after } = require('node:test');
20
+ import { test, describe, beforeAll as before, afterAll as after } from 'vitest';
21
21
  const assert = require('node:assert/strict');
22
22
  const fs = require('node:fs');
23
23
  const fsp = require('node:fs/promises');
@@ -1,12 +1,12 @@
1
1
  /**
2
2
  * mcpStatus.test.cjs — unit tests for the `claude mcp list` output parser.
3
3
  *
4
- * Run: timeout 120 node --test src/main/__tests__/mcpStatus.test.cjs
4
+ * Run: timeout 300 npx vitest run src/main/__tests__/mcpStatus.test.cjs
5
5
  */
6
6
 
7
7
  'use strict';
8
8
 
9
- const { test } = require('node:test');
9
+ import { test } from 'vitest';
10
10
  const assert = require('node:assert/strict');
11
11
  const { parseMcpList } = require('../mcpStatus.cjs');
12
12
 
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- const test = require('node:test');
3
+ import { test } from 'vitest';
4
4
  const assert = require('node:assert/strict');
5
5
  const { parseClusters } = require('../memoryAggregate.cjs');
6
6
 
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- const test = require('node:test');
3
+ import { test } from 'vitest';
4
4
  const assert = require('node:assert/strict');
5
5
  const { scoreMemories } = require('../lib/memoryStale.cjs');
6
6
 
@@ -18,10 +18,15 @@ const opsErrorLog = require('../lib/opsErrorLog.cjs');
18
18
  const tmpDirs = [];
19
19
  let originalHome;
20
20
  let originalSmTelemetrySpool;
21
+ let originalWorktreeRoot;
21
22
 
22
23
  afterEach(async () => {
23
24
  if (originalHome !== undefined) process.env.HOME = originalHome;
24
25
  if (originalSmTelemetrySpool === undefined) delete process.env.SM_TELEMETRY_SPOOL; else process.env.SM_TELEMETRY_SPOOL = originalSmTelemetrySpool;
26
+ if (originalWorktreeRoot !== undefined) {
27
+ if (originalWorktreeRoot === null) delete process.env.SM_WORKTREE_ROOT; else process.env.SM_WORKTREE_ROOT = originalWorktreeRoot;
28
+ originalWorktreeRoot = undefined;
29
+ }
25
30
  const telemetryPath = require.resolve('../lib/telemetryClient.cjs');
26
31
  delete require.cache[telemetryPath];
27
32
  const activeSessionsPath = require.resolve('../lib/activeSessions.cjs');
@@ -85,6 +90,17 @@ test('level "warn" routes to telemetryClient.logLine, default "error" routes to
85
90
  // ─── ephemeral cwd: local refused, telemetry still fires ─────────────────
86
91
 
87
92
  test('an ephemeral cwd yields zero local lines but one telemetry record, with a normalized projectHash', () => {
93
+ // Why this test depends on the process tmpdir: ephemeral classification
94
+ // (cwdClassify.classifyCwd, via ephemeralCwd.isEphemeralCwd) is an exact match
95
+ // on os.tmpdir() plus a prefix match on the managed job/epic worktree roots,
96
+ // and those roots are <SM_WORKTREE_ROOT || os.tmpdir()>/session-manager-*-worktrees
97
+ // (schedulerPaths.worktreeRoot). The sandbox globalSetup mkdtemp's that base under
98
+ // the tmpdir it saw at startup, so a relocated TMPDIR / prior env mutation changes
99
+ // where KIND_CONFIG.epic.root points. Pin SM_WORKTREE_ROOT to a fixture dir this
100
+ // test owns and derive the cwd from KIND_CONFIG, so expected and actual roots
101
+ // resolve through the same code path regardless of TMPDIR.
102
+ originalWorktreeRoot = process.env.SM_WORKTREE_ROOT === undefined ? null : process.env.SM_WORKTREE_ROOT;
103
+ process.env.SM_WORKTREE_ROOT = mkTmpProject();
88
104
  const { KIND_CONFIG } = require('../lib/gitWorktree.cjs');
89
105
  const worktreeCwd = path.join(KIND_CONFIG.epic.root, 'fakehash', 'fake-epic-id');
90
106
  const realRoot = '/home/bilko/Projects/real-project';
@@ -103,7 +119,9 @@ test('an ephemeral cwd yields zero local lines but one telemetry record, with a
103
119
 
104
120
  opsErrorLog.appendError({ cwd: worktreeCwd, scope: 'chatRunner', message: 'worktree err' });
105
121
 
106
- expect(fs.existsSync(opsErrorLog.todayFile(worktreeCwd))).toBe(false);
122
+ // resolveProjectRoot now consumes cwdClassify (not the stubbed projectRootOf):
123
+ // an ephemeral worktree cwd is refused outright, so no local path is even built.
124
+ expect(() => opsErrorLog.todayFile(worktreeCwd)).toThrow(/ephemeral/);
107
125
  expect(reportErrorCalls).toHaveLength(1);
108
126
  expect(reportErrorCalls[0].context.cwd).toBe(realRoot);
109
127
  expect(reportErrorCalls[0].context.cwd).not.toBe(worktreeCwd);
@@ -139,6 +157,12 @@ test('a realistic error with an absolute path and a prompt-like string never rea
139
157
  message: `run failed while processing "${promptLike}" at ${absPath}`,
140
158
  });
141
159
 
160
+ // The telemetry write is fire-and-forget async; poll (bounded) for the queue
161
+ // file instead of a fixed sleep, which raced under load.
162
+ const deadline = Date.now() + 5000;
163
+ while (!fs.existsSync(client.queuePath()) && Date.now() < deadline) {
164
+ await new Promise((r) => setTimeout(r, 25));
165
+ }
142
166
  await new Promise((r) => setTimeout(r, 50));
143
167
 
144
168
  const raw = await fsp.readFile(client.queuePath(), 'utf8');
@@ -16,6 +16,16 @@
16
16
  * whether that pass ends in an actual job launch (see classifyQueueStarvation's
17
17
  * header for why this must be distinct from `lastRunAt`).
18
18
  *
19
+ * Usage-meter circuit PRD update: pollLoop no longer fires blind at a hard-
20
+ * coded cachedUtilization=0 on failure — it falls back to
21
+ * usageCircuit.degradedBudget(), which carries forward the last known-good
22
+ * BINDING window's utilization (or conservatively assumes 100/no-headroom
23
+ * when there's no known-good reading yet at all). So each test below first
24
+ * drives ONE successful ('ok') poll — with the queue still empty, so it
25
+ * can't itself cause a dispatch — to seed that known-good low-utilization
26
+ * reading, THEN seeds the pending job and drives the failure. This proves
27
+ * dispatch proceeds under the CARRIED-FORWARD degraded budget, not a blind 0.
28
+ *
19
29
  * No existing test in this repo mocks a module import (see
20
30
  * pty-epic-worktree-spawn-cwd.test.cjs's header) — this monkey-patches
21
31
  * usage.cjs's cached module object directly instead, same pattern.
@@ -110,11 +120,23 @@ async function flushPendingTick() {
110
120
  await scheduler.tickQueue();
111
121
  }
112
122
 
113
- test('pollLoop still attempts a dispatch after a transient billing failure', async () => {
123
+ test('pollLoop still attempts a dispatch after a transient billing failure, using the degraded budget (not utilization 0)', async () => {
124
+ // Seed a known-good low-utilization reading via an 'ok' poll first.
125
+ process.env.SM_E2E = '1';
126
+ process.env.SM_MOCK_BILLING_KIND = 'ok';
127
+ await scheduler.pollLoop();
128
+ // A prior test's leftover project (a REAL, still-registered project cwd —
129
+ // readQueue() merges every such cwd) can still hold its own permanently-
130
+ // pending job, so this seed poll's own maybeLaunchWhenAvailable may itself
131
+ // spawn a fire-and-forget tick; drain it before resetting/reseeding so
132
+ // that stray tick's async mutate() can't land AFTER (and clobber) the
133
+ // reset below.
134
+ await flushPendingTick();
135
+ await scheduler.writeQueue({ jobs: [], config: {}, paused: null });
136
+
114
137
  await seedOnePendingJob();
115
138
  expect(await lastDispatchAttemptAt()).toBeNull();
116
139
 
117
- process.env.SM_E2E = '1';
118
140
  process.env.SM_MOCK_BILLING_KIND = 'transient';
119
141
  await scheduler.pollLoop();
120
142
  await flushPendingTick();
@@ -122,10 +144,18 @@ test('pollLoop still attempts a dispatch after a transient billing failure', asy
122
144
  expect(await lastDispatchAttemptAt()).not.toBeNull();
123
145
  });
124
146
 
125
- test('pollLoop still attempts a dispatch after the billing poll throws', async () => {
147
+ test('pollLoop still attempts a dispatch after the billing poll throws, using the degraded budget (not utilization 0)', async () => {
148
+ process.env.SM_E2E = '1';
149
+ process.env.SM_MOCK_BILLING_KIND = 'ok';
150
+ await scheduler.pollLoop(); // seed a known-good low-utilization reading
151
+ await flushPendingTick();
152
+ await scheduler.writeQueue({ jobs: [], config: {}, paused: null });
153
+
126
154
  await seedOnePendingJob();
127
155
  expect(await lastDispatchAttemptAt()).toBeNull();
128
156
 
157
+ delete process.env.SM_E2E;
158
+ delete process.env.SM_MOCK_BILLING_KIND;
129
159
  billing.fetchUsage = async () => { throw new Error('simulated IPC transport failure'); };
130
160
  await scheduler.pollLoop();
131
161
  await flushPendingTick();
@@ -8,12 +8,12 @@
8
8
  * so a collision silently merges two unrelated projects' PRDs into one
9
9
  * group.
10
10
  *
11
- * Run: timeout 120 node --test src/main/__tests__/prd-group-allocator.test.cjs
11
+ * Run: timeout 300 npx vitest run src/main/__tests__/prd-group-allocator.test.cjs
12
12
  */
13
13
 
14
14
  'use strict';
15
15
 
16
- const { test } = require('node:test');
16
+ import { test } from 'vitest';
17
17
  const assert = require('node:assert/strict');
18
18
  const fs = require('node:fs');
19
19
  const fsp = require('node:fs/promises');
@@ -40,6 +40,8 @@ const REQUIRED_CAPABILITIES = [
40
40
  { capability: 'retag a PRD (parallelGroup/estimateMinutes)', ipcChannel: 'schedule:retag-prd', adminRoute: 'POST /admin/scheduler/retag-prd' },
41
41
  { capability: 'list scheduler jobs', ipcChannel: null, adminRoute: 'GET /admin/scheduler/jobs' },
42
42
  { capability: 'reset a stuck job', ipcChannel: 'schedule:reset-job', adminRoute: 'POST /admin/scheduler/reset-job' },
43
+ { capability: 'pause new dispatch (user-initiated)', ipcChannel: 'schedule:pause', adminRoute: 'POST /admin/scheduler/pause' },
44
+ { capability: 'resume a paused scheduler', ipcChannel: 'schedule:resume', adminRoute: 'POST /admin/scheduler/resume' },
43
45
  { capability: 'cancel a not-yet-terminal job', ipcChannel: null, adminRoute: 'POST /admin/scheduler/cancel-job' },
44
46
  ];
45
47
 
@@ -499,6 +499,12 @@ test('POST /admin/scheduler/reset-job with cwd resets the SAME-slug job belongin
499
499
  bustCwdCache();
500
500
  const slug = uniqueSlug('collision-slug');
501
501
  const epicId = 'epic-reset-collision';
502
+ // Both projects' rows need a real PRD source — a 'needs_review' row whose
503
+ // PRD is invisible to reconcile()'s onDisk scan is silently dropped (never
504
+ // backfilled to history.jsonl the way a completed/failed/skipped orphan
505
+ // is), which would make jobA vanish for a reason unrelated to what this
506
+ // test actually verifies (cwd-scoped job lookup).
507
+ await writePrdFixture({ cwd: cwdA, epicId, slug });
502
508
  await writePrdFixture({ cwd: cwdB, epicId, slug });
503
509
  await scheduler.writeQueue({
504
510
  jobs: [
@@ -518,8 +524,14 @@ test('POST /admin/scheduler/reset-job with cwd resets the SAME-slug job belongin
518
524
  expect(res.json).toEqual({ ok: true, slug, status: 'pending' });
519
525
 
520
526
  const state = await scheduler.remote.listJobs();
521
- const jobA = state.find((j) => j.cwd === cwdA);
522
- const jobB = state.find((j) => j.cwd === cwdB);
527
+ // Filter by slug too, not just cwd: cwdA is a shared fixture directory
528
+ // reused by many tests in this file, each of which leaves its own
529
+ // never-archived PRD file behind (only the queue row is cleared in
530
+ // `finally`) — reconcile()'s fresh-discovery pass can quarantine one of
531
+ // those leftovers as a NEW row under this same cwd on any later pass, so
532
+ // `cwd` alone no longer uniquely identifies this test's own job.
533
+ const jobA = state.find((j) => j.cwd === cwdA && j.slug === slug);
534
+ const jobB = state.find((j) => j.cwd === cwdB && j.slug === slug);
523
535
  expect(jobA.status).toBe('needs_review'); // untouched
524
536
  expect(jobB.status).toBe('pending'); // the one actually targeted
525
537
  } finally {
@@ -0,0 +1,39 @@
1
+ 'use strict';
2
+ const fs = require('fs');
3
+ const os = require('os');
4
+ const path = require('path');
5
+ const { seedAuthoringGuide } = require('../lib/prdAuthoringSeed.cjs');
6
+
7
+ const write = async (abs, text) => fs.writeFileSync(abs, text, 'utf8');
8
+
9
+ describe('seedAuthoringGuide', () => {
10
+ let dir, src, dest;
11
+ beforeEach(() => {
12
+ dir = fs.mkdtempSync(path.join(os.tmpdir(), 'prd-seed-'));
13
+ src = path.join(dir, 'src.md');
14
+ dest = path.join(dir, 'dest.md');
15
+ fs.writeFileSync(src, '<!-- PRD_AUTHORING.md v3 -->\nnew body\n');
16
+ });
17
+ afterEach(() => fs.rmSync(dir, { recursive: true, force: true }));
18
+
19
+ it('seeds an absent destination', async () => {
20
+ expect(await seedAuthoringGuide({ src, dest, write })).toBe(true);
21
+ expect(fs.readFileSync(dest, 'utf8')).toContain('new body');
22
+ });
23
+ it('overwrites a stale stamp', async () => {
24
+ fs.writeFileSync(dest, '<!-- PRD_AUTHORING.md v2 -->\nold\n');
25
+ expect(await seedAuthoringGuide({ src, dest, write })).toBe(true);
26
+ expect(fs.readFileSync(dest, 'utf8')).toContain('v3');
27
+ });
28
+ it('leaves a matching stamp untouched', async () => {
29
+ const same = '<!-- PRD_AUTHORING.md v3 -->\nlocal edit\n';
30
+ fs.writeFileSync(dest, same);
31
+ expect(await seedAuthoringGuide({ src, dest, write })).toBe(false);
32
+ expect(fs.readFileSync(dest, 'utf8')).toBe(same);
33
+ });
34
+ it('overwrites a destination with no stamp', async () => {
35
+ fs.writeFileSync(dest, '# no stamp\n');
36
+ expect(await seedAuthoringGuide({ src, dest, write })).toBe(true);
37
+ expect(fs.readFileSync(dest, 'utf8')).toContain('v3');
38
+ });
39
+ });