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,80 @@
1
+ /**
2
+ * scheduler-heartbeat-payload.test.cjs — the heartbeat line carries build
3
+ * identity, a subsystem throw yields a `degraded: true` line, and both
4
+ * watchdogHelpers.heartbeatFresh and health.readFreshHeartbeat treat a
5
+ * degraded line as NOT fresh.
6
+ *
7
+ * Run: timeout 120 npx vitest run src/main/__tests__/scheduler-heartbeat-payload.test.cjs
8
+ */
9
+ 'use strict';
10
+
11
+ import { test, expect, beforeEach, vi } from 'vitest';
12
+ const fs = require('node:fs');
13
+ const schedulerPaths = require('../lib/schedulerPaths.cjs');
14
+ const scheduler = require('../scheduler.cjs');
15
+ const { heartbeatFresh } = require('../lib/watchdogHelpers.cjs');
16
+ const { readFreshHeartbeat } = require('../health.cjs');
17
+
18
+ const HB = () => schedulerPaths.heartbeatPath();
19
+ const lastLine = () => JSON.parse(fs.readFileSync(HB(), 'utf8').trim().split('\n').pop());
20
+
21
+ beforeEach(() => {
22
+ fs.mkdirSync(require('node:path').dirname(HB()), { recursive: true });
23
+ fs.rmSync(HB(), { force: true });
24
+ vi.spyOn(console, 'error').mockImplementation(() => {});
25
+ });
26
+
27
+ test('healthy tick writes build {version, codeSha, builtAt} and is fresh', () => {
28
+ const entry = scheduler.heartbeatTick({ readQueueSync: () => ({ jobs: [], unreadable: true, paused: null }) });
29
+ expect(entry.degraded).toBeUndefined();
30
+ const line = lastLine();
31
+ expect(line.build).toEqual(expect.objectContaining({ version: expect.anything() }));
32
+ expect(Object.keys(line.build).sort()).toEqual(['builtAt', 'codeSha', 'version']);
33
+ expect(line.counts).toBeDefined();
34
+ expect(heartbeatFresh(HB())).toBe(true);
35
+ expect(readFreshHeartbeat(HB())).not.toBeNull();
36
+ });
37
+
38
+ test('a throwing queue read writes a degraded line with the error, never a fresh one', () => {
39
+ scheduler.heartbeatTick({ readQueueSync: () => { throw new Error('boom-queue'); } });
40
+ const line = lastLine();
41
+ expect(line.degraded).toBe(true);
42
+ expect(line.errors[0]).toEqual({ subsystem: 'queue-read-starvation-watchdog', error: 'boom-queue' });
43
+ expect(line.build).toBeDefined();
44
+ expect(line.utilization).toBeUndefined();
45
+ expect(heartbeatFresh(HB())).toBe(false);
46
+ expect(readFreshHeartbeat(HB())).toBeNull();
47
+ });
48
+
49
+ test('a throw in the stall detector degrades the line too', () => {
50
+ scheduler.heartbeatTick({ readQueueSync: () => ({ jobs: null, unreadable: true, paused: null }) });
51
+ const line = lastLine();
52
+ expect(line.degraded).toBe(true);
53
+ expect(line.errors.some((e) => e.subsystem === 'stall-detector' || e.subsystem === 'heartbeat-write')).toBe(true);
54
+ expect(heartbeatFresh(HB())).toBe(false);
55
+ });
56
+
57
+ test('watchdogHelpers treats a hand-written degraded line as stale', () => {
58
+ fs.writeFileSync(HB(), JSON.stringify({ ts: Date.now(), degraded: true }) + '\n');
59
+ expect(heartbeatFresh(HB())).toBe(false);
60
+ fs.writeFileSync(HB(), JSON.stringify({ ts: Date.now() }) + '\n');
61
+ expect(heartbeatFresh(HB())).toBe(true);
62
+ });
63
+
64
+ test('healthy tick carries dispatch {..., pendingDispatchable excludes blocked chains}', () => {
65
+ const jobs = [
66
+ { slug: '1-a', cwd: '/p', status: 'failed' },
67
+ { slug: '2-b', cwd: '/p', status: 'pending', dependsOn: ['1-a'] },
68
+ { slug: '3-c', cwd: '/p', status: 'pending' },
69
+ ];
70
+ const entry = scheduler.heartbeatTick({
71
+ readQueueSync: () => ({ jobs, unreadable: true, paused: null, lastRunAt: '2026-01-01T00:00:00.000Z' }),
72
+ });
73
+ expect(entry.dispatch).toEqual(expect.objectContaining({
74
+ lastRunAt: '2026-01-01T00:00:00.000Z', pendingDispatchable: 1, runningCount: 0, paused: false,
75
+ }));
76
+ expect(Object.keys(entry.dispatch).sort()).toEqual(
77
+ ['drain', 'lastDispatchAttemptAt', 'lastRunAt', 'lastTickReason', 'paused', 'pendingDispatchable', 'runningCount'],
78
+ );
79
+ expect(entry.dispatch.drain).toBeNull();
80
+ });
@@ -18,6 +18,7 @@ import { test, expect, beforeAll, afterAll, afterEach } from 'vitest';
18
18
  const fs = require('node:fs');
19
19
  const os = require('node:os');
20
20
  const path = require('node:path');
21
+ const claudeStub = require('../../../tests/helpers/claudeStub.cjs');
21
22
  const { execFileSync } = require('node:child_process');
22
23
 
23
24
  let tmpHome;
@@ -52,6 +53,12 @@ function registerActiveProject(cwd) {
52
53
  fs.writeFileSync(path.join(slugDir, 'transcript.jsonl'), JSON.stringify({ cwd }) + '\n');
53
54
  }
54
55
 
56
+ // Every PRD fixture below lives Epic-scoped, not in the legacy flat prds/
57
+ // dir: the flat dir is retired and swept into prds-archived/ by
58
+ // consolidateFlatPrds the moment a job is no longer pending/running, which
59
+ // would strand a just-completed/failed row the instant reconcile()'s
60
+ // auto-archive-drop path (see scheduler-reconcile-history-backfill.test.cjs)
61
+ // treats the now-missing PRD as "archived on purpose" and drops it.
55
62
  function writeProjectQueue(cwd, jobs) {
56
63
  const stateDir = path.join(cwd, 'session-manager-operations', 'scheduler', 'state');
57
64
  fs.mkdirSync(stateDir, { recursive: true });
@@ -68,16 +75,13 @@ function writeProjectQueue(cwd, jobs) {
68
75
  // which is not the scenario under test here — this must land as a genuine
69
76
  // non-zero exit.
70
77
  function writeKilledClaudeStub() {
71
- const stubPath = path.join(os.tmpdir(), `sm-claude-stub-killed-${process.pid}-${Math.floor(Math.random() * 1e9)}.cjs`);
72
- const body = `
78
+ return claudeStub.writeClaudeStub({ body: `
73
79
  const fs = require('fs');
74
80
  const path = require('path');
75
81
  fs.writeFileSync(path.join(process.cwd(), 'README.md'), 'hello\\nedited by job\\n', 'utf8');
76
82
  fs.writeFileSync(path.join(process.cwd(), 'job-output.txt'), 'work the job produced before dying\\n', 'utf8');
77
83
  process.exit(137);
78
- `;
79
- fs.writeFileSync(stubPath, `#!${process.execPath}\n${body}\n`, { mode: 0o755 });
80
- return stubPath;
84
+ ` });
81
85
  }
82
86
 
83
87
  beforeAll(() => {
@@ -117,7 +121,7 @@ test('an in-place job killed (exit 137) mid-run salvages a delta-scoped patch th
117
121
  fs.appendFileSync(path.join(projectCwd, 'human-wip.txt'), 'more human edits\n', 'utf8');
118
122
 
119
123
  const slug = `1098-test-inplace-killed-${process.pid}-${Math.floor(Math.random() * 1e6)}`;
120
- const prdsDir = path.join(projectCwd, 'session-manager-operations', 'scheduler', 'prds');
124
+ const prdsDir = path.join(projectCwd, 'session-manager-operations', 'scheduler', 'epics', 'test-fixture-epic', 'prds');
121
125
  fs.mkdirSync(prdsDir, { recursive: true });
122
126
  fs.writeFileSync(path.join(prdsDir, `${slug}.md`), 'Do a thing that gets killed mid-run.', 'utf8');
123
127
 
@@ -192,13 +196,10 @@ test('an in-place job killed (exit 137) mid-run salvages a delta-scoped patch th
192
196
  // — a job whose only working-tree dirt is pre-existing human/sibling WIP
193
197
  // present before the run even started.
194
198
  function writeNoopClaudeStub() {
195
- const stubPath = path.join(os.tmpdir(), `sm-claude-stub-noop-${process.pid}-${Math.floor(Math.random() * 1e9)}.cjs`);
196
- const body = `
199
+ return claudeStub.writeClaudeStub({ body: `
197
200
  process.stdout.write(JSON.stringify({ type: 'result', subtype: 'success', result: 'nothing to do', is_error: false }) + '\\n');
198
201
  process.exit(0);
199
- `;
200
- fs.writeFileSync(stubPath, `#!${process.execPath}\n${body}\n`, { mode: 0o755 });
201
- return stubPath;
202
+ ` });
202
203
  }
203
204
 
204
205
  // Stub `claude` binary that blocks until a `go` marker file appears in its
@@ -206,11 +207,19 @@ function writeNoopClaudeStub() {
206
207
  // window to read the queue row's intermediate dispatchPhase before the run
207
208
  // finalizes and deletes it.
208
209
  function writeGatedClaudeStub() {
209
- const stubPath = path.join(os.tmpdir(), `sm-claude-stub-gated-${process.pid}-${Math.floor(Math.random() * 1e9)}.cjs`);
210
- const body = `
210
+ return claudeStub.writeClaudeStub({ body: `
211
211
  const fs = require('fs');
212
212
  const path = require('path');
213
213
  const { execFileSync } = require('child_process');
214
+ // spawnJob's dispatch path probes 'claude --version' (claudeBin.cjs)
215
+ // BEFORE the real dispatch — a real claude binary answers that
216
+ // near-instantly regardless of any in-flight work, so the stub must
217
+ // too, or the probe's own 8s exec timeout eats almost this whole test's
218
+ // poll window before the row ever advances past 'running-stamped'.
219
+ if (process.argv.includes('--version')) {
220
+ process.stdout.write('1.0.0 (stub)\\n');
221
+ process.exit(0);
222
+ }
214
223
  const goFile = path.join(process.cwd(), 'go.marker');
215
224
  const deadline = Date.now() + 10_000;
216
225
  while (!fs.existsSync(goFile) && Date.now() < deadline) {
@@ -218,9 +227,7 @@ function writeGatedClaudeStub() {
218
227
  }
219
228
  process.stdout.write(JSON.stringify({ type: 'result', subtype: 'success', result: 'ok', is_error: false }) + '\\n');
220
229
  process.exit(0);
221
- `;
222
- fs.writeFileSync(stubPath, `#!${process.execPath}\n${body}\n`, { mode: 0o755 });
223
- return stubPath;
230
+ ` });
224
231
  }
225
232
 
226
233
  test('dispatchPhase breadcrumb advances to "spawned" mid-run and is gone after finalize', async () => {
@@ -229,7 +236,7 @@ test('dispatchPhase breadcrumb advances to "spawned" mid-run and is gone after f
229
236
  registerActiveProject(projectCwd);
230
237
 
231
238
  const slug = `1163-test-dispatchphase-${process.pid}-${Math.floor(Math.random() * 1e6)}`;
232
- const prdsDir = path.join(projectCwd, 'session-manager-operations', 'scheduler', 'prds');
239
+ const prdsDir = path.join(projectCwd, 'session-manager-operations', 'scheduler', 'epics', 'test-fixture-epic', 'prds');
233
240
  fs.mkdirSync(prdsDir, { recursive: true });
234
241
  fs.writeFileSync(path.join(prdsDir, `${slug}.md`), 'Do a thing, gated on a marker file.', 'utf8');
235
242
 
@@ -285,7 +292,7 @@ test('a job whose tree is dirty only from pre-existing baseline WIP (human/sibli
285
292
  fs.writeFileSync(path.join(projectCwd, 'human-wip.txt'), 'human work in progress\n', 'utf8');
286
293
 
287
294
  const slug = `1098-test-inplace-noop-${process.pid}-${Math.floor(Math.random() * 1e6)}`;
288
- const prdsDir = path.join(projectCwd, 'session-manager-operations', 'scheduler', 'prds');
295
+ const prdsDir = path.join(projectCwd, 'session-manager-operations', 'scheduler', 'epics', 'test-fixture-epic', 'prds');
289
296
  fs.mkdirSync(prdsDir, { recursive: true });
290
297
  fs.writeFileSync(path.join(prdsDir, `${slug}.md`), 'Do nothing.', 'utf8');
291
298
 
@@ -1,12 +1,12 @@
1
1
  /**
2
2
  * scheduler-investigation-prompt.test.cjs — unit tests for buildInvestigationPrompt.
3
3
  *
4
- * Run: timeout 120 node --test src/main/__tests__/scheduler-investigation-prompt.test.cjs
4
+ * Run: timeout 300 npx vitest run src/main/__tests__/scheduler-investigation-prompt.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 fs = require('node:fs');
12
12
  const os = require('node:os');
@@ -93,7 +93,7 @@ test('prompt instructs that cwd must be the git repo root where the fix will lan
93
93
  assert.match(prompt, /needs_review/);
94
94
  });
95
95
 
96
- test('prompt frontmatter block still declares exactly the four expected keys', () => {
96
+ test('prompt frontmatter block still declares exactly the five expected keys', () => {
97
97
  const prompt = buildInvestigationPrompt(makeArgs());
98
98
  const fenceMatch = prompt.match(/```\s*\n\s*---\n([\s\S]*?)\n\s*---\n\s*```/);
99
99
  assert.ok(fenceMatch, 'expected a fenced frontmatter block in the prompt');
@@ -101,7 +101,7 @@ test('prompt frontmatter block still declares exactly the four expected keys', (
101
101
  .split('\n')
102
102
  .filter((line) => line.trim().length > 0)
103
103
  .map((line) => line.split(':')[0].trim());
104
- assert.deepEqual(keys, ['title', 'cwd', 'parallelGroup', 'estimateMinutes']);
104
+ assert.deepEqual(keys, ['title', 'cwd', 'parallelGroup', 'estimateMinutes', 'isFixPlan']);
105
105
  });
106
106
 
107
107
  // ─── isGitRepoSync (scheduler.cjs) ─────────────────────────────────────────────
@@ -20,6 +20,7 @@ import { test, expect, beforeAll, afterAll, afterEach } from 'vitest';
20
20
  const fs = require('node:fs');
21
21
  const os = require('node:os');
22
22
  const path = require('node:path');
23
+ const claudeStub = require('../../../tests/helpers/claudeStub.cjs');
23
24
 
24
25
  let tmpHome;
25
26
  let originalHome;
@@ -50,8 +51,7 @@ afterEach(() => {
50
51
  const THINKING_400 = 'API Error: 400 {"detail":{"error":"{\\"message\\":\\"\\\\\\"thinking.type.enabled\\\\\\" is not supported for this model. Use \\\\\\"thinking.type.adaptive\\\\\\" and \\\\\\"output_config.effort\\\\\\" to control thinking behavior.\\"}"}}';
51
52
 
52
53
  function writeStub({ mode }) {
53
- const stubPath = path.join(os.tmpdir(), `sm-claude-stub-launch-${process.pid}-${Math.floor(Math.random() * 1e9)}.cjs`);
54
- const body = `
54
+ return claudeStub.writeClaudeStub({ body: `
55
55
  const fs = require('fs');
56
56
  const path = require('path');
57
57
  fs.writeFileSync(path.join(process.cwd(), 'env.marker'), JSON.stringify({
@@ -67,9 +67,7 @@ function writeStub({ mode }) {
67
67
  }
68
68
  process.stdout.write(JSON.stringify({ type: 'result', subtype: 'error', is_error: true, num_turns: 9, usage: { input_tokens: 100, output_tokens: 250 }, result: 'Error: tests failed' }) + '\\n');
69
69
  process.exit(1);
70
- `;
71
- fs.writeFileSync(stubPath, `#!${process.execPath}\n${body}\n`, { mode: 0o755 });
72
- return stubPath;
70
+ ` });
73
71
  }
74
72
 
75
73
  function mkProject() {
@@ -34,7 +34,7 @@ const tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), 'looks-done-test-'));
34
34
  process.env.HOME = tmpHome;
35
35
 
36
36
  const { reverifyNeedsReview, computeLooksDone, applyNeedsReviewAutoResolve, findSatisfyingCommitOnMain } = require('../scheduler.cjs');
37
- const { resolvePrdWriteDir } = require('../lib/prdLocations.cjs');
37
+ const { resolveEpicPrdWriteDir } = require('../lib/prdLocations.cjs');
38
38
  const { bustCwdCache } = require('../lib/queueStore.cjs');
39
39
 
40
40
  function git(args, cwd) {
@@ -92,13 +92,32 @@ function writeRunLog(runId, slug, lines) {
92
92
  fs.writeFileSync(path.join(runDir, `${slug}.log`), lines.join('\n') + '\n');
93
93
  }
94
94
 
95
+ // Epic-scoped, not the legacy flat prds/ dir: the flat dir is retired and
96
+ // swept into prds-archived/ by consolidateFlatPrds the moment a job is no
97
+ // longer live (pending/running/needs_review/investigating), which would
98
+ // strand a row that starts 'failed' before reverifyNeedsReview ever gets a
99
+ // chance to decide anything about it.
95
100
  function writePrd(cwd, slug, body) {
96
- const dir = resolvePrdWriteDir(cwd);
101
+ const dir = resolveEpicPrdWriteDir(cwd, 'test-fixture-epic');
97
102
  fs.mkdirSync(dir, { recursive: true });
98
103
  const text = `---\ntitle: Test PRD\ncwd: ${cwd}\nestimateMinutes: 30\n---\n${body}\n`;
99
104
  fs.writeFileSync(path.join(dir, `${slug}.md`), text);
100
105
  }
101
106
 
107
+ // A job reverifyNeedsReview() heals to 'completed' has its PRD archived
108
+ // immediately (archiveCompletedPrd) and its row backfilled to history.jsonl
109
+ // + dropped from queue.json on the very same reconcile pass — see
110
+ // scheduler-reconcile-history-backfill.test.cjs. Look in both places.
111
+ function readTerminalRow(queuePath, projectCwd, slug) {
112
+ const jobs = JSON.parse(fs.readFileSync(queuePath, 'utf8')).jobs;
113
+ const row = jobs.find((j) => j.slug === slug);
114
+ if (row) return row;
115
+ const historyPath = path.join(projectCwd, 'session-manager-operations', 'scheduler', 'state', 'history.jsonl');
116
+ if (!fs.existsSync(historyPath)) return undefined;
117
+ const lines = fs.readFileSync(historyPath, 'utf8').trim().split('\n').filter(Boolean);
118
+ return lines.map((l) => JSON.parse(l)).reverse().find((j) => j.slug === slug);
119
+ }
120
+
102
121
  async function wait(ms) {
103
122
  await new Promise((r) => setTimeout(r, ms));
104
123
  }
@@ -220,11 +239,11 @@ test('needs_review with an in-window commit heals exactly as today (regression
220
239
 
221
240
  await reverifyNeedsReview();
222
241
 
223
- const jobs = JSON.parse(fs.readFileSync(queuePath, 'utf8')).jobs;
224
- assert.equal(jobs.length, 1);
225
- assert.equal(jobs[0].status, 'completed', 'a clean re-verify on a RESCANNABLE verdict must still heal');
226
- assert.equal(jobs[0].verifierVerdict, undefined);
227
- assert.equal(jobs[0].error, null);
242
+ const row = readTerminalRow(queuePath, projectCwd, '20-clean-feature');
243
+ assert.ok(row, 'healed job must be found in queue.json or (once archived+backfilled) history.jsonl');
244
+ assert.equal(row.status, 'completed', 'a clean re-verify on a RESCANNABLE verdict must still heal');
245
+ assert.equal(row.verifierVerdict, undefined);
246
+ assert.equal(row.error, null);
228
247
  });
229
248
 
230
249
  test('computeLooksDone: no declared paths on the PRD → null, never fabricates evidence', async () => {
@@ -0,0 +1,118 @@
1
+ /**
2
+ * scheduler-manual-pause.test.cjs — user-initiated pause (schedule:pause /
3
+ * POST /admin/scheduler/pause). A 'manual' pause stops NEW dispatch, never
4
+ * kills running jobs, survives restart, and is cleared only by an explicit
5
+ * Resume / Run now — never by the rate-limit reset timer or network recovery.
6
+ *
7
+ * Run: timeout 120 npx vitest run src/main/__tests__/scheduler-manual-pause.test.cjs
8
+ */
9
+
10
+ 'use strict';
11
+
12
+ import { test, expect, beforeAll, afterAll, beforeEach } from 'vitest';
13
+ const fs = require('node:fs');
14
+ const os = require('node:os');
15
+ const path = require('node:path');
16
+
17
+ let tmpHome;
18
+ let originalHome;
19
+ let scheduler;
20
+ let queueStore;
21
+
22
+ beforeAll(() => {
23
+ originalHome = process.env.HOME;
24
+ tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), 'sm-manual-pause-'));
25
+ process.env.HOME = tmpHome;
26
+ process.env.SM_JOB_WORKTREE_DISABLE = '1';
27
+ scheduler = require('../scheduler.cjs');
28
+ queueStore = require('../lib/queueStore.cjs');
29
+ });
30
+
31
+ afterAll(() => {
32
+ process.env.HOME = originalHome;
33
+ delete process.env.SM_JOB_WORKTREE_DISABLE;
34
+ fs.rmSync(tmpHome, { recursive: true, force: true });
35
+ });
36
+
37
+ beforeEach(async () => {
38
+ await scheduler.clearPause('manual');
39
+ });
40
+
41
+ test('pause engages reason "manual" with no resumeAt, even inside the manual-override cooldown', async () => {
42
+ await scheduler.clearPause('manual'); // starts the 5-minute cooldown
43
+ await scheduler.setPaused('manual', null);
44
+ const state = await queueStore.readMerged();
45
+ expect(state.paused).toBeTruthy();
46
+ expect(state.paused.reason).toBe('manual');
47
+ expect(state.paused.resumeAt).toBeNull();
48
+ });
49
+
50
+ test('a manual pause never arms a resume time, even if a resumeAt is supplied', async () => {
51
+ await scheduler.setPaused('manual', new Date(Date.now() + 3_600_000).toISOString());
52
+ const state = await queueStore.readMerged();
53
+ expect(state.paused.resumeAt).toBeNull();
54
+ });
55
+
56
+ test('pause blocks tickQueue dispatch', async () => {
57
+ await scheduler.setPaused('manual', null);
58
+ const result = await scheduler.tickQueue();
59
+ expect(result.fired).toBe(false);
60
+ expect(result.reason).toBe('paused');
61
+ });
62
+
63
+ test('an auto rate_limit / network / auth pause cannot displace a manual pause', async () => {
64
+ await scheduler.setPaused('manual', null);
65
+ await scheduler.setPaused('rate_limit', new Date(Date.now() + 3_600_000).toISOString());
66
+ await scheduler.setPaused('network', null);
67
+ await scheduler.setPaused('auth', null);
68
+ const state = await queueStore.readMerged();
69
+ expect(state.paused.reason).toBe('manual');
70
+ expect(state.paused.resumeAt).toBeNull();
71
+ });
72
+
73
+ test('poll recovery and the resume-time computation never clear/arm a manual pause', () => {
74
+ expect(scheduler.pollRecoveryClearSource('manual', true)).toBeNull();
75
+ expect(scheduler.pollRecoveryClearSource('manual', false)).toBeNull();
76
+ expect(scheduler.computeEffectiveResumeAt('manual', null)).toBeNull();
77
+ });
78
+
79
+ test('a running job survives a pause untouched', async () => {
80
+ const projectCwd = fs.mkdtempSync(path.join(tmpHome, 'pause-survivor-'));
81
+ const slugDir = path.join(tmpHome, '.claude', 'projects', 'pause-survivor-slug');
82
+ fs.mkdirSync(slugDir, { recursive: true });
83
+ fs.writeFileSync(path.join(slugDir, 'transcript.jsonl'), JSON.stringify({ cwd: projectCwd }) + '\n');
84
+ queueStore.bustCwdCache();
85
+ const stateDir = path.join(projectCwd, 'session-manager-operations', 'scheduler', 'state');
86
+ fs.mkdirSync(stateDir, { recursive: true });
87
+ fs.writeFileSync(path.join(stateDir, 'queue.json'), JSON.stringify({
88
+ jobs: [{ slug: 'pause-survivor', status: 'running', cwd: projectCwd, pid: process.pid, runId: 'r1', startedAt: new Date().toISOString() }],
89
+ }));
90
+ await scheduler.setPaused('manual', null);
91
+ const state = await queueStore.readMerged();
92
+ const row = state.jobs.find((j) => j.slug === 'pause-survivor');
93
+ expect(row.status).toBe('running');
94
+ expect(row.runId).toBe('r1');
95
+ });
96
+
97
+ test('a manual pause survives restart (re-read from disk; boot never clears it)', async () => {
98
+ await scheduler.setPaused('manual', null);
99
+ const onDisk = await queueStore.readMerged(); // what a fresh process reads at boot
100
+ expect(onDisk.paused.reason).toBe('manual');
101
+ // Boot only clears a pause whose resumeAt has elapsed / re-arms one with a
102
+ // resumeAt; a manual pause has none, so neither branch applies.
103
+ expect(onDisk.paused.resumeAt).toBeNull();
104
+ });
105
+
106
+ test('resume clears a manual pause', async () => {
107
+ await scheduler.setPaused('manual', null);
108
+ await scheduler.clearPause('manual');
109
+ const state = await queueStore.readMerged();
110
+ expect(state.paused).toBeNull();
111
+ });
112
+
113
+ test('remote.pause / remote.resume are the admin-route twins of the IPC handlers', async () => {
114
+ expect(await scheduler.remote.pause()).toEqual({ ok: true });
115
+ expect((await queueStore.readMerged()).paused.reason).toBe('manual');
116
+ expect(await scheduler.remote.resume()).toEqual({ ok: true });
117
+ expect((await queueStore.readMerged()).paused).toBeNull();
118
+ });
@@ -5,19 +5,42 @@
5
5
  * scheduler process is otherwise invisible in the run record, which cost a
6
6
  * full investigation to diagnose a false pass_no_commit.
7
7
  *
8
+ * SCHEDULER_CODE_SHA now resolves through src/main/lib/buildIdentity.cjs
9
+ * (build-identity-stamp PRD): a production npx install ships no .git, so a
10
+ * runtime `git rev-parse` was structurally always null there — this file
11
+ * used to accept that null unconditionally, which made the dead stamp green
12
+ * forever. With src/main/build-info.json present (ensured below — the exact
13
+ * artifact scripts/write-build-info.cjs bakes at publish time), the sha must
14
+ * be real.
15
+ *
8
16
  * Run: timeout 120 npx vitest run src/main/__tests__/scheduler-meta-code-sha.test.cjs
9
17
  */
10
18
 
11
19
  'use strict';
12
20
 
13
- import { test, expect } from 'vitest';
21
+ import { test, expect, afterAll } from 'vitest';
22
+ const fs = require('node:fs');
23
+ const path = require('node:path');
24
+
25
+ const BUILD_INFO_PATH = path.join(__dirname, '..', 'build-info.json');
26
+ const preexistingBuildInfo = fs.existsSync(BUILD_INFO_PATH) ? fs.readFileSync(BUILD_INFO_PATH, 'utf8') : null;
27
+ if (preexistingBuildInfo === null) {
28
+ // eslint-disable-next-line global-require
29
+ require('../../../scripts/write-build-info.cjs').writeBuildInfo();
30
+ }
31
+
14
32
  const { SCHEDULER_BOOTED_AT, SCHEDULER_CODE_SHA } = require('../scheduler.cjs');
15
33
 
34
+ afterAll(() => {
35
+ if (preexistingBuildInfo === null) fs.rmSync(BUILD_INFO_PATH, { force: true });
36
+ else fs.writeFileSync(BUILD_INFO_PATH, preexistingBuildInfo);
37
+ });
38
+
16
39
  test('SCHEDULER_BOOTED_AT is a valid ISO date string', () => {
17
40
  expect(typeof SCHEDULER_BOOTED_AT).toBe('string');
18
41
  expect(Number.isNaN(new Date(SCHEDULER_BOOTED_AT).getTime())).toBe(false);
19
42
  });
20
43
 
21
- test('SCHEDULER_CODE_SHA is null or a short git hex SHA', () => {
22
- expect(SCHEDULER_CODE_SHA === null || /^[0-9a-f]{7,40}$/.test(SCHEDULER_CODE_SHA)).toBe(true);
44
+ test('SCHEDULER_CODE_SHA is a real short git hex SHA when build-info.json is present — never null', () => {
45
+ expect(SCHEDULER_CODE_SHA).toMatch(/^[0-9a-f]{7,40}$/);
23
46
  });
@@ -103,6 +103,22 @@ function writeProjectQueue(cwd, jobs) {
103
103
  return path.join(stateDir, 'queue.json');
104
104
  }
105
105
 
106
+ // This test's whole premise is a PRD source missing EVERYWHERE — so the row
107
+ // this produces is exactly what reconcile()'s auto-archive-drop path treats
108
+ // as "archived on purpose": backfilled to history.jsonl and dropped from
109
+ // queue.json in the very same reconcile pass that spawnJob's own
110
+ // broadcast({flush:true}) triggers on completion (see
111
+ // scheduler-reconcile-history-backfill.test.cjs). Look in both places.
112
+ function readTerminalRow(queuePath, projectCwd, slug) {
113
+ const jobs = JSON.parse(fs.readFileSync(queuePath, 'utf8')).jobs;
114
+ const row = jobs.find((j) => j.slug === slug);
115
+ if (row) return row;
116
+ const historyPath = path.join(projectCwd, 'session-manager-operations', 'scheduler', 'state', 'history.jsonl');
117
+ if (!fs.existsSync(historyPath)) return undefined;
118
+ const lines = fs.readFileSync(historyPath, 'utf8').trim().split('\n').filter(Boolean);
119
+ return lines.map((l) => JSON.parse(l)).reverse().find((j) => j.slug === slug);
120
+ }
121
+
106
122
  // This is the actual regression case: PRD 812's fix only covered executeJob's
107
123
  // own return value. The bug lived one layer up, in spawnJob's post-run
108
124
  // handler, which only short-circuited on skipped === 'prd-archived' and let
@@ -123,8 +139,7 @@ test("spawnJob's post-run handler retires a prd-missing skip as a plain completi
123
139
  const job = { slug, cwd: projectCwd };
124
140
  await spawnJob(job, 'run-spawnjob-missing', runDir, projectCwd);
125
141
 
126
- const state = JSON.parse(fs.readFileSync(queuePath, 'utf8'));
127
- const row = state.jobs.find((j) => j.slug === slug);
142
+ const row = readTerminalRow(queuePath, projectCwd, slug);
128
143
  expect(row).toBeDefined();
129
144
  // A 'prd-missing' skip means no executor ever ran — it must land on the
130
145
  // distinct 'skipped' status, never 'completed' (the status a genuine
@@ -17,6 +17,7 @@ import { test, expect, beforeAll, afterAll, afterEach } from 'vitest';
17
17
  const fs = require('node:fs');
18
18
  const os = require('node:os');
19
19
  const path = require('node:path');
20
+ const claudeStub = require('../../../tests/helpers/claudeStub.cjs');
20
21
 
21
22
  let tmpHome;
22
23
  let originalHome;
@@ -66,16 +67,13 @@ test('buildClaudeSpawnArgs: model is never omitted even without a systemPrompt',
66
67
  // stream-json success result and exits 0 — same technique as
67
68
  // scheduler-bash-timeout-env.test.cjs's env-var proof, applied to argv.
68
69
  function writeClaudeStub() {
69
- const stubPath = path.join(os.tmpdir(), `sm-claude-stub-argv-${process.pid}-${Math.floor(Math.random() * 1e9)}.cjs`);
70
- const body = `
70
+ return claudeStub.writeClaudeStub({ body: `
71
71
  const fs = require('fs');
72
72
  const path = require('path');
73
73
  fs.writeFileSync(path.join(process.cwd(), 'argv.marker'), JSON.stringify(process.argv.slice(2)), 'utf8');
74
74
  process.stdout.write(JSON.stringify({ type: 'result', subtype: 'success', result: 'ok' }) + '\\n');
75
75
  process.exit(0);
76
- `;
77
- fs.writeFileSync(stubPath, `#!${process.execPath}\n${body}\n`, { mode: 0o755 });
78
- return stubPath;
76
+ ` });
79
77
  }
80
78
 
81
79
  function writePersona(dir, name, frontmatterLines, bodyText) {
@@ -19,6 +19,7 @@ import { test, expect, beforeAll, afterAll, afterEach } from 'vitest';
19
19
  const fs = require('node:fs');
20
20
  const os = require('node:os');
21
21
  const path = require('node:path');
22
+ const claudeStub = require('../../../tests/helpers/claudeStub.cjs');
22
23
  const { execFileSync } = require('node:child_process');
23
24
 
24
25
  let tmpHome;
@@ -52,10 +53,41 @@ function registerActiveProject(cwd) {
52
53
  fs.writeFileSync(path.join(slugDir, 'transcript.jsonl'), JSON.stringify({ cwd }) + '\n');
53
54
  }
54
55
 
56
+ // Every real scheduler row is born from a real PRD .md file — reconcile()'s
57
+ // auto-archive-drop path (a terminal job whose slug has no on-disk PRD
58
+ // anywhere is treated as "archived on purpose": dropped from queue.json and
59
+ // backfilled to history.jsonl, see scheduler-reconcile-history-backfill.
60
+ // test.cjs) fires the moment a completion path reconciles with
61
+ // broadcast({flush:true}). Without a matching PRD source, a row this test
62
+ // drives to a terminal status vanishes from queue.json before the assertions
63
+ // below ever read it back. Epic-scoped (not the legacy flat prds/ dir) so
64
+ // it's never swept by consolidateFlatPrds either.
65
+ // Some tests author their own real PRD (e.g. with `quietMachine: true`
66
+ // frontmatter) before calling writeProjectQueue — never shadow that with a
67
+ // blank fixture, which would silently overwrite its parsed frontmatter on
68
+ // the next reconcile() refresh (onDisk.set(slug, ...) keeps whichever file a
69
+ // later scan pass parses).
70
+ function prdAlreadyExists(cwd, slug) {
71
+ const flatPath = path.join(cwd, 'session-manager-operations', 'scheduler', 'prds', `${slug}.md`);
72
+ if (fs.existsSync(flatPath)) return true;
73
+ const epicsRoot = path.join(cwd, 'session-manager-operations', 'scheduler', 'epics');
74
+ if (!fs.existsSync(epicsRoot)) return false;
75
+ return fs.readdirSync(epicsRoot).some((epicId) => fs.existsSync(path.join(epicsRoot, epicId, 'prds', `${slug}.md`)));
76
+ }
77
+
78
+ function writeFixturePrd(cwd, slug) {
79
+ const prdsDir = path.join(cwd, 'session-manager-operations', 'scheduler', 'epics', 'test-fixture-epic', 'prds');
80
+ fs.mkdirSync(prdsDir, { recursive: true });
81
+ fs.writeFileSync(path.join(prdsDir, `${slug}.md`), 'Test fixture PRD.', 'utf8');
82
+ }
83
+
55
84
  function writeProjectQueue(cwd, jobs) {
56
85
  const stateDir = path.join(cwd, 'session-manager-operations', 'scheduler', 'state');
57
86
  fs.mkdirSync(stateDir, { recursive: true });
58
87
  fs.writeFileSync(path.join(stateDir, 'queue.json'), JSON.stringify({ jobs }, null, 2));
88
+ for (const job of jobs) {
89
+ if (job && job.slug && !prdAlreadyExists(job.cwd || cwd, job.slug)) writeFixturePrd(job.cwd || cwd, job.slug);
90
+ }
59
91
  return path.join(stateDir, 'queue.json');
60
92
  }
61
93
 
@@ -66,15 +98,12 @@ function writeProjectQueue(cwd, jobs) {
66
98
  // as a genuine kill-with-partial-work, not an auto-retryable transient blip
67
99
  // with no evidence of real execution.
68
100
  function writeKilledClaudeStub() {
69
- const stubPath = path.join(os.tmpdir(), `sm-claude-stub-quiet-killed-${process.pid}-${Math.floor(Math.random() * 1e9)}.cjs`);
70
- const body = `
101
+ return claudeStub.writeClaudeStub({ body: `
71
102
  const fs = require('fs');
72
103
  const path = require('path');
73
104
  fs.writeFileSync(path.join(process.cwd(), 'job-output.txt'), 'work in progress when killed\\n', 'utf8');
74
105
  process.exit(137);
75
- `;
76
- fs.writeFileSync(stubPath, `#!${process.execPath}\n${body}\n`, { mode: 0o755 });
77
- return stubPath;
106
+ ` });
78
107
  }
79
108
 
80
109
  let reapDeadRunningJobs;
@@ -109,7 +138,13 @@ test('a killed quietMachine job releases the exclusive lease, and an ordinary jo
109
138
 
110
139
  const quietSlug = `1107-test-quiet-killed-${process.pid}-${Math.floor(Math.random() * 1e6)}`;
111
140
  const ordinarySlug = `1107-test-ordinary-${process.pid}-${Math.floor(Math.random() * 1e6)}`;
112
- const prdsDir = path.join(projectCwd, 'session-manager-operations', 'scheduler', 'prds');
141
+ // Epic-scoped, not the legacy flat prds/ dir: the flat dir is retired and
142
+ // swept into prds-archived/ by consolidateFlatPrds the moment a job is no
143
+ // longer pending/running, which would silently strip this PRD's
144
+ // `quietMachine: true` frontmatter from the row on the very next
145
+ // reconcile() refresh (or drop the row entirely once terminal — see
146
+ // scheduler-reconcile-history-backfill.test.cjs).
147
+ const prdsDir = path.join(projectCwd, 'session-manager-operations', 'scheduler', 'epics', 'test-fixture-epic', 'prds');
113
148
  fs.mkdirSync(prdsDir, { recursive: true });
114
149
  fs.writeFileSync(path.join(prdsDir, `${quietSlug}.md`), '---\nquietMachine: true\n---\nMeasure timing without contention, then get killed.', 'utf8');
115
150
  fs.writeFileSync(path.join(prdsDir, `${ordinarySlug}.md`), 'An ordinary PRD queued behind the quiet job.', 'utf8');