claude-code-session-manager 0.86.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 (335) hide show
  1. package/README.md +26 -53
  2. package/dist/assets/AgentLibrary-BZ6IY2g1.js +3 -0
  3. package/dist/assets/{DataModel-Q4jhl24R.js → DataModel-dAoGAODX.js} +1 -1
  4. package/dist/assets/{History-Cj2FejEo.js → History-CF157HyX.js} +2 -2
  5. package/dist/assets/{Hooks-CaelQI6t.js → Hooks-CCPPipBk.js} +3 -3
  6. package/dist/assets/{HostBilko--v7cMR8I.js → HostBilko-Dlmz2m06.js} +1 -1
  7. package/dist/assets/{Library-DgI9oCCZ.js → Library-BaXcboWX.js} +1 -1
  8. package/dist/assets/{ListDetail-DYUZN-x-.js → ListDetail-CoUCzG5S.js} +1 -1
  9. package/dist/assets/MarkdownEditor-DTg3JlOT.js +1 -0
  10. package/dist/assets/{McpServers-ypCYURh3.js → McpServers-DdjY1sA3.js} +2 -2
  11. package/dist/assets/{Memory-C2qYp-3M.js → Memory-BJ3Jlw0C.js} +4 -4
  12. package/dist/assets/{Panel-Cj2kw-Zv.js → Panel-ygRSgc41.js} +1 -1
  13. package/dist/assets/Permissions-m0KAnccF.js +3 -0
  14. package/dist/assets/{Plugins-C1Vj8_dU.js → Plugins-CJRl6B3Q.js} +2 -2
  15. package/dist/assets/{ProvenanceBadge-DczPNM5U.js → ProvenanceBadge-w8cnlVvW.js} +1 -1
  16. package/dist/assets/{SaveBar-Cd_7U6Gb.js → SaveBar-BpyPcUIk.js} +1 -1
  17. package/dist/assets/Scheduler-D2ppxSfy.js +14 -0
  18. package/dist/assets/{ScopeSwitcher-DVSyI44-.js → ScopeSwitcher-EyuIwt-j.js} +1 -1
  19. package/dist/assets/Settings-BHSGGFEc.js +3 -0
  20. package/dist/assets/{SkillReferenceGraph-CUv1_Q2c.js → SkillReferenceGraph-BNaZ2JCF.js} +1 -1
  21. package/dist/assets/{Skills-C_YHkAy-.js → Skills-Dl3q75QV.js} +2 -2
  22. package/dist/assets/{SystemPrompt-B8R7T9xn.js → SystemPrompt-Bg6F-Cyp.js} +1 -1
  23. package/dist/assets/{TagLibrary-dj9YHWyy.js → TagLibrary-BSutflCy.js} +1 -1
  24. package/dist/assets/{TiptapBody-DnSBUjHE.js → TiptapBody-CXih4xrc.js} +1 -1
  25. package/dist/assets/{Toggle-CjV_BJn6.js → Toggle-2qtUHhA9.js} +1 -1
  26. package/dist/assets/{index-CXFQIPhO.js → index-B3emy2oI.js} +583 -583
  27. package/dist/assets/{index-CDo9xBR9.css → index-BHTX4OTc.css} +1 -1
  28. package/dist/assets/settingsSchema-CdzxaqwM.js +3 -0
  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 +14 -17
  40. package/plugins/CLAUDE.md +61 -0
  41. package/plugins/session-manager-dev/.claude-plugin/plugin.json +1 -1
  42. package/plugins/session-manager-dev/skills/builder/4-manual/SKILL.md +1 -1
  43. package/plugins/session-manager-dev/skills/ops-sweep/SKILL.md +1 -1
  44. package/scripts/README.md +59 -0
  45. package/scripts/audit-ops-hygiene.cjs +350 -0
  46. package/scripts/hooks/guard-destructive-git.cjs +10 -34
  47. package/scripts/hooks/guard-inline-implementation.cjs +14 -8
  48. package/scripts/hooks/guard-prd-writes.cjs +7 -36
  49. package/scripts/hooks/guard-self-schedule.cjs +175 -0
  50. package/scripts/ops-sweep.cjs +355 -0
  51. package/scripts/scheduler-mcp-server.cjs +35 -71
  52. package/src/main/__tests__/agentModelResolve.test.cjs +100 -9
  53. package/src/main/__tests__/bilkoHost-integration.test.cjs +3 -3
  54. package/src/main/__tests__/broadcastCoalescer.test.cjs +18 -0
  55. package/src/main/__tests__/chat-cancel-terminal.test.cjs +6 -9
  56. package/src/main/__tests__/chat-exit-close-race.test.cjs +3 -3
  57. package/src/main/__tests__/chat-mcp-consent-notice.test.cjs +4 -5
  58. package/src/main/__tests__/chat-queue.test.cjs +2 -2
  59. package/src/main/__tests__/chat-stop-signal.test.cjs +2 -2
  60. package/src/main/__tests__/dep-orphan-archive-health.test.cjs +77 -0
  61. package/src/main/__tests__/dod-batchkey.test.cjs +2 -2
  62. package/src/main/__tests__/dod-drain-hook.test.cjs +2 -2
  63. package/src/main/__tests__/dod-report.test.cjs +2 -2
  64. package/src/main/__tests__/dod-reverify.test.cjs +2 -2
  65. package/src/main/__tests__/epicMint.test.cjs +4 -4
  66. package/src/main/__tests__/exchanges.test.cjs +2 -2
  67. package/src/main/__tests__/extractJson.test.cjs +2 -2
  68. package/src/main/__tests__/files-reject-credentials.test.cjs +1 -1
  69. package/src/main/__tests__/fixtures/1218-fo-01-move-scripts-lib-into-src-main-lib.log +556 -0
  70. package/src/main/__tests__/health-build-freshness.test.cjs +39 -0
  71. package/src/main/__tests__/health-delegation-chain.test.cjs +17 -2
  72. package/src/main/__tests__/health-queue-dispatch.test.cjs +58 -7
  73. package/src/main/__tests__/health-tick-liveness.test.cjs +11 -3
  74. package/src/main/__tests__/health-usage-poller.test.cjs +70 -23
  75. package/src/main/__tests__/historyRollup.test.cjs +2 -2
  76. package/src/main/__tests__/kg-augment.test.cjs +2 -2
  77. package/src/main/__tests__/mcpStatus.test.cjs +2 -2
  78. package/src/main/__tests__/memoryAggregate.test.cjs +1 -1
  79. package/src/main/__tests__/memoryStale.test.cjs +1 -1
  80. package/src/main/__tests__/needsReviewLedger.test.cjs +162 -0
  81. package/src/main/__tests__/opsErrorLogTelemetryTap.test.cjs +28 -4
  82. package/src/main/__tests__/pollLoop-dispatch-on-failure.test.cjs +48 -4
  83. package/src/main/__tests__/prd-group-allocator.test.cjs +2 -2
  84. package/src/main/__tests__/prdAdminRouteParity.test.cjs +2 -0
  85. package/src/main/__tests__/prdAdminRoutes.test.cjs +14 -2
  86. package/src/main/__tests__/prdAuthoringSeed.test.cjs +39 -0
  87. package/src/main/__tests__/prdCreateDisposition.test.cjs +201 -0
  88. package/src/main/__tests__/prdFrontmatterDisposition.test.cjs +125 -0
  89. package/src/main/__tests__/prdLocations.test.cjs +100 -2
  90. package/src/main/__tests__/prdLocationsArchived.test.cjs +50 -1
  91. package/src/main/__tests__/prdSetDisposition.test.cjs +222 -0
  92. package/src/main/__tests__/proc-role-env.test.cjs +125 -0
  93. package/src/main/__tests__/procname-claude-spawn-sites.test.cjs +304 -0
  94. package/src/main/__tests__/procname-sm-processes.test.cjs +127 -0
  95. package/src/main/__tests__/projectHomeAdminRoutes.test.cjs +81 -401
  96. package/src/main/__tests__/projectPages.test.cjs +63 -149
  97. package/src/main/__tests__/queue-health-verdict.test.cjs +170 -0
  98. package/src/main/__tests__/queue-starvation-dispatch-driver.test.cjs +117 -20
  99. package/src/main/__tests__/queue-starvation-per-project.test.cjs +14 -2
  100. package/src/main/__tests__/queueHistory.test.cjs +65 -2
  101. package/src/main/__tests__/rateLimitPollerStreak.test.cjs +38 -3
  102. package/src/main/__tests__/reconcileTiming.test.cjs +135 -0
  103. package/src/main/__tests__/runVerify-landed-commit-outranks.test.cjs +181 -0
  104. package/src/main/__tests__/runVerify.test.cjs +5 -5
  105. package/src/main/__tests__/scheduleJobStatusDrift.test.cjs +3 -3
  106. package/src/main/__tests__/scheduleJobTransitions.test.cjs +103 -3
  107. package/src/main/__tests__/scheduler-adopted-run-supervision.test.cjs +143 -0
  108. package/src/main/__tests__/scheduler-autofix-select.test.cjs +2 -2
  109. package/src/main/__tests__/scheduler-autopromote.test.cjs +2 -2
  110. package/src/main/__tests__/scheduler-bash-timeout-env.test.cjs +3 -5
  111. package/src/main/__tests__/scheduler-boot-orphans.test.cjs +79 -98
  112. package/src/main/__tests__/scheduler-broadcast-reconcile.test.cjs +121 -0
  113. package/src/main/__tests__/scheduler-cross-project-batch.test.cjs +43 -0
  114. package/src/main/__tests__/scheduler-default-eligible-heal.test.cjs +161 -0
  115. package/src/main/__tests__/scheduler-dispatch-loop.test.cjs +58 -0
  116. package/src/main/__tests__/scheduler-epic-digest.test.cjs +3 -5
  117. package/src/main/__tests__/scheduler-force-tick-outcome.test.cjs +2 -2
  118. package/src/main/__tests__/scheduler-gate-shadow.test.cjs +119 -0
  119. package/src/main/__tests__/scheduler-guard-verdict-autoresolve.test.cjs +390 -0
  120. package/src/main/__tests__/scheduler-heartbeat-payload.test.cjs +80 -0
  121. package/src/main/__tests__/scheduler-inplace-salvage.test.cjs +25 -18
  122. package/src/main/__tests__/scheduler-investigation-prompt.test.cjs +4 -4
  123. package/src/main/__tests__/scheduler-job-budget.test.cjs +172 -0
  124. package/src/main/__tests__/scheduler-launch-failure.test.cjs +3 -5
  125. package/src/main/__tests__/scheduler-looks-done.test.cjs +119 -10
  126. package/src/main/__tests__/scheduler-manual-pause.test.cjs +118 -0
  127. package/src/main/__tests__/scheduler-meta-code-sha.test.cjs +26 -3
  128. package/src/main/__tests__/scheduler-porcelain-rename.test.cjs +164 -0
  129. package/src/main/__tests__/scheduler-prd-missing-skip.test.cjs +17 -2
  130. package/src/main/__tests__/scheduler-prd-persona-spawn.test.cjs +3 -5
  131. package/src/main/__tests__/scheduler-quiet-machine-lease.test.cjs +41 -6
  132. package/src/main/__tests__/scheduler-rate-limit-pause.test.cjs +62 -6
  133. package/src/main/__tests__/scheduler-rate-limit-spin-guard.test.cjs +3 -5
  134. package/src/main/__tests__/scheduler-reap-dead-running-jobs.test.cjs +182 -3
  135. package/src/main/__tests__/scheduler-reaper-helpers-basics.test.cjs +87 -0
  136. package/src/main/__tests__/scheduler-reconcile-cwd-preserve.test.cjs +100 -0
  137. package/src/main/__tests__/scheduler-reconcile-invalid-repair.test.cjs +3 -3
  138. package/src/main/__tests__/scheduler-reconcile-quarantine.test.cjs +4 -4
  139. package/src/main/__tests__/scheduler-shard-quarantine.test.cjs +110 -0
  140. package/src/main/__tests__/scheduler-shared-tree-guard.test.cjs +168 -0
  141. package/src/main/__tests__/scheduler-starve-escalation.test.cjs +18 -8
  142. package/src/main/__tests__/scheduler-stranded-autofix-park.test.cjs +245 -0
  143. package/src/main/__tests__/scheduler-supervisor-record.test.cjs +81 -0
  144. package/src/main/__tests__/scheduler-tick-cancel-token.test.cjs +2 -2
  145. package/src/main/__tests__/scheduler-tick-wedge.test.cjs +172 -0
  146. package/src/main/__tests__/scheduler-transient-failure.test.cjs +2 -2
  147. package/src/main/__tests__/scheduler-worktree-cap-defer.test.cjs +51 -11
  148. package/src/main/__tests__/scheduler-worktree-exec-cwd.test.cjs +3 -5
  149. package/src/main/__tests__/usageSingleFlight.test.cjs +157 -0
  150. package/src/main/__tests__/workTypeLibrary.test.cjs +1 -1
  151. package/src/main/bilkoHost.cjs +16 -7
  152. package/src/main/build-info.json +8 -0
  153. package/src/main/chatRunner.cjs +12 -7
  154. package/src/main/config.cjs +2 -3
  155. package/src/main/docEdit.cjs +4 -2
  156. package/src/main/health.cjs +268 -52
  157. package/src/main/heapSnapshot.cjs +2 -2
  158. package/src/main/historyAggregator.cjs +6 -1
  159. package/src/main/index.cjs +152 -50
  160. package/src/main/ipcSchemas.cjs +53 -17
  161. package/src/main/lib/__tests__/active-sessions.test.cjs +251 -0
  162. package/src/main/lib/__tests__/auditLog.test.cjs +38 -0
  163. package/src/main/lib/__tests__/bootSelfHeal.test.cjs +107 -0
  164. package/src/main/lib/__tests__/buildIdentity.test.cjs +121 -0
  165. package/src/main/lib/__tests__/cwdClassify.test.cjs +111 -0
  166. package/src/main/lib/__tests__/definitionOfDoneSequence.test.cjs +95 -0
  167. package/src/main/lib/__tests__/delegationReadiness.test.cjs +323 -44
  168. package/src/main/lib/__tests__/dispatchLoop.test.cjs +63 -0
  169. package/src/main/lib/__tests__/effectiveModelInfo.test.cjs +239 -0
  170. package/src/main/lib/__tests__/gateFixtures.json +20 -0
  171. package/src/main/lib/__tests__/gitWorktree.test.cjs +97 -1
  172. package/src/main/lib/__tests__/guardShims.test.cjs +151 -0
  173. package/src/main/lib/__tests__/instanceLock.test.cjs +93 -7
  174. package/src/main/lib/__tests__/jobSupervisorRecord.test.cjs +78 -0
  175. package/src/main/lib/__tests__/jobWorktreeBootLive.test.cjs +11 -0
  176. package/src/main/lib/__tests__/localAdminHttp.test.cjs +1 -1
  177. package/src/main/lib/__tests__/mcpToolCatalog.test.cjs +6 -1
  178. package/src/main/lib/__tests__/opsRootAbsoluteCwd.test.cjs +5 -5
  179. package/src/main/lib/__tests__/prdDisposition.test.cjs +224 -0
  180. package/src/main/lib/__tests__/procIdentity.test.cjs +119 -0
  181. package/src/main/lib/__tests__/procName.test.cjs +92 -0
  182. package/src/main/lib/__tests__/queueStoreMachineStateRecovery.test.cjs +67 -0
  183. package/src/main/lib/__tests__/reaperHelpers.test.cjs +179 -1
  184. package/src/main/lib/__tests__/schedulerMcpServerHelp.test.cjs +6 -7
  185. package/src/main/lib/__tests__/schedulerMcpServerProjectHome.test.cjs +37 -204
  186. package/src/main/lib/__tests__/schedulerPaths.test.cjs +226 -0
  187. package/src/main/lib/__tests__/schedulerPathsWorktree.test.cjs +94 -0
  188. package/src/main/lib/__tests__/schedulerRuntimeState.test.cjs +56 -0
  189. package/src/main/lib/__tests__/sessionSlots.test.cjs +45 -0
  190. package/src/main/lib/__tests__/telemetryBacklog.test.cjs +1 -1
  191. package/src/main/lib/__tests__/upgradeDrain.test.cjs +130 -0
  192. package/src/main/lib/__tests__/usageCircuit.test.cjs +285 -0
  193. package/src/main/lib/__tests__/watchdog-helpers.test.cjs +375 -0
  194. package/src/main/lib/__tests__/watchdog-relaunch.test.cjs +266 -0
  195. package/src/main/lib/activeIndexRebuild.cjs +1 -3
  196. package/{scripts → src/main}/lib/activeSessions.cjs +67 -89
  197. package/src/main/lib/adoptedRunSupervisor.cjs +136 -0
  198. package/src/main/lib/agentModelResolve.cjs +94 -24
  199. package/src/main/lib/agentPersonaSchema.cjs +2 -2
  200. package/src/main/lib/auditLog.cjs +30 -5
  201. package/src/main/lib/bootSelfHeal.cjs +88 -0
  202. package/src/main/lib/buildIdentity.cjs +113 -0
  203. package/src/main/lib/classifyPromptTicket.cjs +3 -2
  204. package/src/main/lib/claudeBin.cjs +37 -2
  205. package/src/main/lib/cleanEnv.cjs +27 -1
  206. package/src/main/lib/credentials.cjs +4 -2
  207. package/src/main/lib/cwdClassify.cjs +185 -0
  208. package/src/main/lib/definitionOfDone.cjs +296 -52
  209. package/src/main/lib/delegationReadiness.cjs +290 -225
  210. package/src/main/lib/dispatchLoop.cjs +40 -0
  211. package/src/main/lib/effectiveModelInfo.cjs +332 -0
  212. package/src/main/lib/ephemeralCwd.cjs +7 -28
  213. package/src/main/lib/epicMint.cjs +3 -3
  214. package/src/main/lib/gitWorktree.cjs +136 -19
  215. package/src/main/lib/guardShims.cjs +156 -0
  216. package/src/main/lib/historyRollup.cjs +6 -7
  217. package/src/main/lib/instanceLock.cjs +32 -5
  218. package/src/main/lib/jobDirtFilter.cjs +7 -2
  219. package/src/main/lib/jobSupervisorRecord.cjs +147 -0
  220. package/src/main/lib/jobWorktreeBootLive.cjs +9 -5
  221. package/src/main/lib/launchFailure.cjs +2 -1
  222. package/src/main/lib/localAdminHttp.cjs +9 -27
  223. package/src/main/lib/mcpToolCatalog.cjs +33 -78
  224. package/src/main/lib/needsReviewLedger.cjs +205 -0
  225. package/src/main/lib/opsErrorLog.cjs +1 -1
  226. package/src/main/lib/opsOwnership.cjs +27 -19
  227. package/src/main/lib/prdAuthoringSeed.cjs +36 -0
  228. package/src/main/lib/prdCreate.cjs +56 -1
  229. package/src/main/lib/prdDisposition.cjs +199 -0
  230. package/src/main/lib/prdFrontmatter.cjs +8 -2
  231. package/src/main/lib/prdLocations.cjs +214 -45
  232. package/src/main/lib/procIdentity.cjs +126 -0
  233. package/src/main/lib/procName.cjs +98 -0
  234. package/src/main/lib/projectHomeAdminRoutes.cjs +56 -327
  235. package/src/main/lib/projectRootResolve.cjs +1 -1
  236. package/src/main/lib/queueHistory.cjs +27 -8
  237. package/src/main/lib/queueStore.cjs +63 -38
  238. package/src/main/lib/quietMachineLease.cjs +19 -2
  239. package/src/main/lib/reaperHelpers.cjs +181 -15
  240. package/src/main/lib/reservationExpiry.cjs +30 -0
  241. package/src/main/lib/runClaudeP.cjs +4 -2
  242. package/src/main/lib/runLogRetention.cjs +3 -2
  243. package/src/main/lib/scheduleJobSchema.cjs +10 -2
  244. package/src/main/lib/scheduleJobTransitions.cjs +60 -0
  245. package/src/main/lib/schedulerBatch.cjs +27 -2
  246. package/src/main/lib/schedulerConfig.cjs +24 -0
  247. package/src/main/lib/schedulerPaths.cjs +175 -0
  248. package/src/main/lib/schedulerRuntimeState.cjs +59 -0
  249. package/src/main/lib/sessionSlots.cjs +47 -9
  250. package/src/main/lib/smProcNames.cjs +51 -0
  251. package/src/main/lib/upgradeDrain.cjs +188 -0
  252. package/src/main/lib/usageCircuit.cjs +206 -0
  253. package/{scripts → src/main}/lib/watchdogHelpers.cjs +71 -38
  254. package/src/main/lib/withTimeout.cjs +33 -0
  255. package/src/main/mcpStatus.cjs +4 -2
  256. package/src/main/pluginInstall.cjs +6 -2
  257. package/src/main/projectPages.cjs +41 -145
  258. package/src/main/pty.cjs +8 -1
  259. package/src/main/queueOps.cjs +10 -10
  260. package/src/main/runVerify.cjs +69 -3
  261. package/src/main/scheduler/prdParser.cjs +13 -0
  262. package/src/main/scheduler.cjs +2841 -482
  263. package/src/main/seedAgentPersonas.cjs +1 -1
  264. package/src/main/seedDevPlugin.cjs +1 -1
  265. package/src/main/seedSchedulerMcp.cjs +7 -6
  266. package/src/main/seedStatus.cjs +1 -1
  267. package/src/main/supervisor.cjs +5 -3
  268. package/src/main/templates/PRD_AUTHORING.md +50 -0
  269. package/src/main/usage.cjs +140 -58
  270. package/src/preload/api.d.ts +110 -65
  271. package/src/preload/index.cjs +12 -4
  272. package/src/seed/agents/project-home-builder.md +31 -48
  273. package/dist/assets/AgentLibrary-DTFL7y8G.js +0 -3
  274. package/dist/assets/MarkdownEditor-DCIubYWf.js +0 -1
  275. package/dist/assets/Permissions-BiYZNGYW.js +0 -3
  276. package/dist/assets/Scheduler-DcLBiJBq.js +0 -14
  277. package/dist/assets/Settings-Cv-pRyms.js +0 -3
  278. package/dist/assets/settingsSchema-BJVciriw.js +0 -3
  279. package/screenshots/.gitkeep +0 -0
  280. package/screenshots/README-screenshots.md +0 -13
  281. package/scripts/project-pages-logic/dist/logic.cjs +0 -4709
  282. package/scripts/render-project-pages/dist/renderer.cjs +0 -18900
  283. package/scripts/render-project-pages.cjs +0 -70
  284. package/scripts/validate-project-pages-summary.cjs +0 -62
  285. package/src/main/lib/projectPageSummarySchema.cjs +0 -181
  286. package/src/main/teams.cjs +0 -95
  287. package/src/main/templates/project-pages-catalog.json +0 -741
  288. package/src/main/templates/project-pages-default-home.html +0 -123
  289. package/src/main/templates/project-pages-pipeline.md +0 -417
  290. package/src/seed/prompts/code-review/ac-coverage-check.md +0 -8
  291. package/src/seed/prompts/code-review/correctness-only.md +0 -8
  292. package/src/seed/prompts/code-review/full-spectrum-high.md +0 -8
  293. package/src/seed/prompts/code-review/hallucination-check.md +0 -8
  294. package/src/seed/prompts/code-review/public-api-compat.md +0 -8
  295. package/src/seed/prompts/code-review/readability-naming.md +0 -8
  296. package/src/seed/prompts/debugging/bug-as-failing-test.md +0 -8
  297. package/src/seed/prompts/debugging/git-bisect-regression.md +0 -8
  298. package/src/seed/prompts/debugging/instrument-intermittent-bug.md +0 -8
  299. package/src/seed/prompts/debugging/localize-pipeline-failure.md +0 -8
  300. package/src/seed/prompts/debugging/reproduce-then-diagnose.md +0 -8
  301. package/src/seed/prompts/documentation/adr-from-change.md +0 -8
  302. package/src/seed/prompts/documentation/module-readme.md +0 -8
  303. package/src/seed/prompts/documentation/onboarding-plan.md +0 -8
  304. package/src/seed/prompts/documentation/refresh-claude-md.md +0 -8
  305. package/src/seed/prompts/documentation/tsdoc-public-exports.md +0 -8
  306. package/src/seed/prompts/git-pr/conventional-commit.md +0 -8
  307. package/src/seed/prompts/git-pr/draft-pr-title-body.md +0 -8
  308. package/src/seed/prompts/git-pr/pre-commit-safety-sweep.md +0 -8
  309. package/src/seed/prompts/git-pr/release-notes-block.md +0 -8
  310. package/src/seed/prompts/git-pr/split-large-pr.md +0 -8
  311. package/src/seed/prompts/performance/bundle-startup-audit.md +0 -8
  312. package/src/seed/prompts/performance/complexity-audit.md +0 -8
  313. package/src/seed/prompts/performance/cpu-profile-hot-path.md +0 -8
  314. package/src/seed/prompts/performance/db-query-plan-review.md +0 -8
  315. package/src/seed/prompts/performance/memory-leak-hunt.md +0 -8
  316. package/src/seed/prompts/qa/api-contract-tests.md +0 -8
  317. package/src/seed/prompts/qa/e2e-critical-path.md +0 -8
  318. package/src/seed/prompts/qa/failing-test-for-bug.md +0 -8
  319. package/src/seed/prompts/qa/find-missing-test-coverage.md +0 -8
  320. package/src/seed/prompts/qa/stabilize-flaky-test.md +0 -8
  321. package/src/seed/prompts/qa/tdd-red-first.md +0 -8
  322. package/src/seed/prompts/qa/visual-regression-review.md +0 -8
  323. package/src/seed/prompts/qa/wcag-axe-scan.md +0 -8
  324. package/src/seed/prompts/refactoring/dead-code-sweep.md +0 -8
  325. package/src/seed/prompts/refactoring/extract-duplicated-pattern.md +0 -8
  326. package/src/seed/prompts/refactoring/modernize-legacy-file.md +0 -8
  327. package/src/seed/prompts/refactoring/reduce-cyclomatic-complexity.md +0 -8
  328. package/src/seed/prompts/refactoring/tighten-module-boundaries.md +0 -8
  329. package/src/seed/prompts/security/authz-audit.md +0 -8
  330. package/src/seed/prompts/security/crypto-correctness.md +0 -8
  331. package/src/seed/prompts/security/cwe-top-25-hunt.md +0 -8
  332. package/src/seed/prompts/security/dependency-audit.md +0 -8
  333. package/src/seed/prompts/security/ipc-boundary-hardening.md +0 -8
  334. package/src/seed/prompts/security/owasp-top-10-staged-diff.md +0 -8
  335. package/src/seed/prompts/security/secret-credential-scan.md +0 -8
@@ -0,0 +1,188 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * upgradeDrain.cjs — restart as a REQUEST that drains first and never kills.
5
+ *
6
+ * requestRestart() → writes <schedulerHome>/restart-request.json (atomic).
7
+ * evaluateDrain() → PURE state machine: none | pause | wait | restart | abort.
8
+ * scheduler.cjs → runs it from the existing 60 s heartbeat interval (no
9
+ * new driver) and owns the `drain` field on the machine
10
+ * runtime file. `drain` is deliberately NOT `state.paused`:
11
+ * a rate-limit pause can't overwrite it and the manual-clear
12
+ * cooldown (isCooldownSuppressed) can't swallow it.
13
+ *
14
+ * Lifecycle: request → pause (drain.active) → wait (running/investigating rows
15
+ * finish; nothing new dispatches) → restart (zero busy, checked inside a mutate
16
+ * right before exit) | abort (deadline; retires the request, clears the drain).
17
+ * The request is stamped drainCompletedAt before exit; the next boot clears a
18
+ * leftover drain whose request is complete.
19
+ *
20
+ * A `restarting.json` marker with a bounded lifetime covers the deliberate exit
21
+ * window so scripts/scheduler-watchdog.cjs does not relaunch a second instance.
22
+ */
23
+
24
+ const fs = require('node:fs');
25
+ const path = require('node:path');
26
+ const schedulerPaths = require('./schedulerPaths.cjs');
27
+
28
+ // Observed max job run is 240 min; 4 h converts a stuck drain to an abort.
29
+ const DEFAULT_DRAIN_DEADLINE_MS = 4 * 60 * 60_000;
30
+ // Long enough for a cold `npx` resolve + boot, short enough that a failed
31
+ // relaunch is picked up by the watchdog again.
32
+ const RESTARTING_MARKER_TTL_MS = 5 * 60_000;
33
+
34
+ function writeJsonAtomicSync(file, value) {
35
+ fs.mkdirSync(path.dirname(file), { recursive: true });
36
+ const tmp = `${file}.tmp-${process.pid}-${Date.now()}`;
37
+ fs.writeFileSync(tmp, JSON.stringify(value, null, 2) + '\n', 'utf8');
38
+ fs.renameSync(tmp, file);
39
+ }
40
+
41
+ function readJsonSafe(file) {
42
+ try {
43
+ const v = JSON.parse(fs.readFileSync(file, 'utf8'));
44
+ return v && typeof v === 'object' ? v : null;
45
+ } catch {
46
+ return null;
47
+ }
48
+ }
49
+
50
+ // ---------- request file ----------
51
+
52
+ function readRestartRequest(file = schedulerPaths.restartRequestPath()) {
53
+ return readJsonSafe(file);
54
+ }
55
+
56
+ /**
57
+ * requestRestart({reason, requestedBy}) → the request written. Idempotent: an
58
+ * already-pending (not completed) request is returned unchanged so a repeated
59
+ * click or the auto trigger can never reset the drain deadline.
60
+ */
61
+ function requestRestart({ reason, requestedBy } = {}, { file = schedulerPaths.restartRequestPath(), now = Date.now() } = {}) {
62
+ const existing = readRestartRequest(file);
63
+ if (existing && !existing.drainCompletedAt) return existing;
64
+ const request = {
65
+ reason: typeof reason === 'string' && reason ? reason : 'unspecified',
66
+ requestedBy: typeof requestedBy === 'string' && requestedBy ? requestedBy : 'unknown',
67
+ requestedAt: new Date(now).toISOString(),
68
+ };
69
+ writeJsonAtomicSync(file, request);
70
+ return request;
71
+ }
72
+
73
+ /** Stamp drainCompletedAt (called immediately before exit). */
74
+ function stampDrainCompleted(now = Date.now(), file = schedulerPaths.restartRequestPath()) {
75
+ const existing = readRestartRequest(file);
76
+ if (!existing) return null;
77
+ const stamped = { ...existing, drainCompletedAt: new Date(now).toISOString() };
78
+ writeJsonAtomicSync(file, stamped);
79
+ return stamped;
80
+ }
81
+
82
+ function retireRestartRequest(file = schedulerPaths.restartRequestPath()) {
83
+ try { fs.unlinkSync(file); } catch (e) { if (e?.code !== 'ENOENT') throw e; }
84
+ }
85
+
86
+ // ---------- pure state machine ----------
87
+
88
+ /**
89
+ * queueBusyCount(jobs, extraInFlight?) → running + investigating rows, plus any
90
+ * in-memory investigations the queue file cannot see.
91
+ */
92
+ function queueBusyCount(jobs, extraInFlight = 0) {
93
+ let n = 0;
94
+ for (const j of Array.isArray(jobs) ? jobs : []) {
95
+ if (j && (j.status === 'running' || j.status === 'investigating')) n++;
96
+ }
97
+ return n + (Number.isFinite(extraInFlight) ? extraInFlight : 0);
98
+ }
99
+
100
+ /**
101
+ * evaluateDrain({request, queueSnapshot, drainState, now, deadlineMs}) → {action, reason}
102
+ *
103
+ * queueSnapshot: { running, investigating } counts (in-memory investigations
104
+ * already folded in by the caller). drainState: the machine `drain` field
105
+ * (null / {active}). Pure — no IO, no clock.
106
+ *
107
+ * no request, or request already completed → none (abort if a stale drain is still active)
108
+ * deadline passed → abort
109
+ * request pending, drain not yet active → pause (engage drain)
110
+ * drain active, anything running/investigating → wait
111
+ * drain active, zero busy → restart
112
+ */
113
+ function evaluateDrain({ request, queueSnapshot, drainState, now, deadlineMs = DEFAULT_DRAIN_DEADLINE_MS } = {}) {
114
+ const active = Boolean(drainState && drainState.active);
115
+ if (!request || request.drainCompletedAt) {
116
+ return active && !request
117
+ ? { action: 'abort', reason: 'drain-without-request' }
118
+ : { action: 'none', reason: request ? 'request-complete' : 'no-request' };
119
+ }
120
+ const requestedMs = Date.parse(request.requestedAt ?? '');
121
+ if (Number.isFinite(requestedMs) && now - requestedMs >= deadlineMs) {
122
+ return { action: 'abort', reason: 'deadline' };
123
+ }
124
+ if (!active) return { action: 'pause', reason: 'engage' };
125
+ const busy = (queueSnapshot?.running ?? 0) + (queueSnapshot?.investigating ?? 0);
126
+ return busy > 0 ? { action: 'wait', reason: `busy:${busy}` } : { action: 'restart', reason: 'idle' };
127
+ }
128
+
129
+ /**
130
+ * bootDrainAction({drainState, request}) → 'clear' | 'keep' | 'none'.
131
+ * A drain whose request is complete (the restart happened) or gone is stale.
132
+ */
133
+ function bootDrainAction({ drainState, request } = {}) {
134
+ if (!drainState || !drainState.active) return 'none';
135
+ if (!request || request.drainCompletedAt) return 'clear';
136
+ return 'keep';
137
+ }
138
+
139
+ /** The pause-like view every dispatch classifier must use: paused OR draining. */
140
+ function effectivePaused(state) {
141
+ if (state?.paused) return state.paused;
142
+ return state?.drain?.active ? { reason: 'drain-for-upgrade' } : null;
143
+ }
144
+
145
+ // ---------- automatic trigger ----------
146
+
147
+ /**
148
+ * installedBuildDiffers({running, installed}) — true only when BOTH shas are
149
+ * known and differ (an install/update already happened). Never consults npm.
150
+ */
151
+ function installedBuildDiffers({ running, installed } = {}) {
152
+ return typeof running === 'string' && running !== ''
153
+ && typeof installed === 'string' && installed !== ''
154
+ && running !== installed;
155
+ }
156
+
157
+ // ---------- restarting marker (watchdog) ----------
158
+
159
+ function markRestarting({ file = schedulerPaths.restartingMarkerPath(), now = Date.now(), ttlMs = RESTARTING_MARKER_TTL_MS } = {}) {
160
+ writeJsonAtomicSync(file, { pid: process.pid, at: now, expiresAt: now + ttlMs });
161
+ }
162
+
163
+ function clearRestartingMarker(file = schedulerPaths.restartingMarkerPath()) {
164
+ try { fs.unlinkSync(file); } catch { /* absent */ }
165
+ }
166
+
167
+ /** True while the marker exists and has not expired (bounded lifetime). */
168
+ function isRestartingMarkerActive({ file = schedulerPaths.restartingMarkerPath(), now = Date.now() } = {}) {
169
+ const m = readJsonSafe(file);
170
+ return Boolean(m) && typeof m.expiresAt === 'number' && now < m.expiresAt;
171
+ }
172
+
173
+ module.exports = {
174
+ DEFAULT_DRAIN_DEADLINE_MS,
175
+ RESTARTING_MARKER_TTL_MS,
176
+ readRestartRequest,
177
+ requestRestart,
178
+ stampDrainCompleted,
179
+ retireRestartRequest,
180
+ queueBusyCount,
181
+ evaluateDrain,
182
+ bootDrainAction,
183
+ effectivePaused,
184
+ installedBuildDiffers,
185
+ markRestarting,
186
+ clearRestartingMarker,
187
+ isRestartingMarkerActive,
188
+ };
@@ -0,0 +1,206 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * usageCircuit.cjs — a breaker over the /api/oauth/usage meter (PRD ss-02).
5
+ * Degraded mode is explicit, never a silent 0: repeated failures open the
6
+ * circuit with jittered backoff; degradedBudget() carries forward the last
7
+ * known-good BINDING window's utilization (never 0), pinning to 100 on a
8
+ * fresh executor-observed 429 and capping concurrency at 2. Pure Node.
9
+ *
10
+ * Kill switches (read live from process.env, so a test/operator can flip
11
+ * them without restarting anything that already holds a circuit instance):
12
+ * SM_USAGE_CIRCUIT=0 — bypasses the breaker entirely: state() always
13
+ * reports 'closed' and recordFailure() never
14
+ * opens it. Escape hatch if the breaker itself
15
+ * is ever suspected of wedging dispatch.
16
+ * SM_USAGE_DEGRADED_CAP=n — overrides degradedBudget()'s concurrencyCap
17
+ * outright (bypasses the normal min(cap, 2)),
18
+ * for an operator who needs a different ceiling
19
+ * while the meter is degraded.
20
+ */
21
+
22
+ const FLOOR_MS = 30_000;
23
+ const CAP_MS = 8 * 60 * 1000;
24
+
25
+ /** Doubles from FLOOR_MS, capped at CAP_MS. 0/falsy prevMs starts at the floor. */
26
+ function doubledBase(prevMs) {
27
+ return prevMs > 0 ? Math.min(prevMs * 2, CAP_MS) : FLOOR_MS;
28
+ }
29
+
30
+ /**
31
+ * Next backoff interval: doubles `prevMs` (floor FLOOR_MS, cap CAP_MS), then
32
+ * jitters by 0.5 + rand()*0.5 so concurrent instances don't retry in lockstep.
33
+ */
34
+ function backoffWithJitter(prevMs) {
35
+ const base = doubledBase(prevMs);
36
+ return base * (0.5 + Math.random() * 0.5);
37
+ }
38
+
39
+ /**
40
+ * A cached reset timestamp is fresh only if it's strictly in the future
41
+ * relative to `now` — null/unparseable/at-or-before-now all read as stale,
42
+ * since a passed reset no longer describes a live window.
43
+ */
44
+ function isResetFresh(iso, now) {
45
+ if (!iso) return false;
46
+ const resetMs = Date.parse(iso);
47
+ if (!Number.isFinite(resetMs)) return false;
48
+ const nowMs = now instanceof Date ? now.getTime() : Number(now);
49
+ if (!Number.isFinite(nowMs)) return false;
50
+ return resetMs > nowMs;
51
+ }
52
+
53
+ /**
54
+ * Which window is actually binding dispatch. Prefers `payload.limits[]`'s
55
+ * `is_active` entry (the richer per-window shape); falls back to the flat
56
+ * `five_hour` field when `limits[]` is absent, matching usage.cjs's shape.
57
+ */
58
+ function bindingWindow(payload) {
59
+ if (payload && Array.isArray(payload.limits) && payload.limits.length) {
60
+ const active = payload.limits.find((l) => l && l.is_active);
61
+ if (active) {
62
+ return {
63
+ name: active.type || active.name || 'unknown',
64
+ utilization: active.utilization,
65
+ resets_at: active.resets_at ?? null,
66
+ };
67
+ }
68
+ }
69
+ const fh = payload && payload.five_hour;
70
+ return {
71
+ name: 'five_hour',
72
+ utilization: fh ? fh.utilization : undefined,
73
+ resets_at: fh ? fh.resets_at : null,
74
+ };
75
+ }
76
+
77
+ /**
78
+ * SM_USAGE_DEGRADED_CAP=<n> overrides the normal min(configuredCap, 2)
79
+ * ceiling outright — an operator escape hatch, not a default path. Falls
80
+ * through to the normal min(cap, 2) rule when unset/non-positive.
81
+ */
82
+ function degradedConcurrencyCap(configuredCap) {
83
+ const override = Number(process.env.SM_USAGE_DEGRADED_CAP);
84
+ if (Number.isFinite(override) && override > 0) return override;
85
+ const cap = Number.isFinite(configuredCap) ? configuredCap : 2;
86
+ return Math.min(cap, 2);
87
+ }
88
+
89
+ /**
90
+ * Conservative budget to run on while the meter is down. utilization is
91
+ * carried forward from the last known-good BINDING window — never 0, which
92
+ * would read as "plenty of headroom" instead of "we don't know." A fresh
93
+ * executor-observed 429 (its own window not yet passed) pins utilization to
94
+ * 100 regardless of the stale cached value. concurrencyCap never exceeds 2
95
+ * (or SM_USAGE_DEGRADED_CAP, if set).
96
+ */
97
+ function degradedBudget(lastGoodPayload, executorEvidence = {}) {
98
+ const window = bindingWindow(lastGoodPayload);
99
+ let utilization = Number.isFinite(window.utilization) && window.utilization > 0
100
+ ? window.utilization
101
+ : 100;
102
+
103
+ const concurrencyCap = degradedConcurrencyCap(executorEvidence.configuredCap);
104
+
105
+ if (executorEvidence.observed429 && isResetFresh(executorEvidence.resetsAt, executorEvidence.now)) {
106
+ utilization = 100;
107
+ }
108
+
109
+ return { utilization, concurrencyCap };
110
+ }
111
+
112
+ /** Wraps `fn` so concurrent callers awaiting an in-flight call share one promise. */
113
+ function singleFlight(fn) {
114
+ let inFlight = null;
115
+ return function singleFlightWrapped(...args) {
116
+ if (inFlight) return inFlight;
117
+ inFlight = Promise.resolve(fn(...args)).finally(() => { inFlight = null; });
118
+ return inFlight;
119
+ };
120
+ }
121
+
122
+ /** SM_USAGE_CIRCUIT=0 (or 'false') bypasses the breaker entirely. */
123
+ function breakerEnabled() {
124
+ const v = process.env.SM_USAGE_CIRCUIT;
125
+ return v == null || v === '' || (v !== '0' && String(v).toLowerCase() !== 'false');
126
+ }
127
+
128
+ /**
129
+ * Breaker state machine over the meter: closed -> open after 3 consecutive
130
+ * failures; open allows exactly one probe per backoff interval (half_open);
131
+ * a successful probe closes the circuit and resets backoff, a failed probe
132
+ * reopens it with backoff doubled.
133
+ */
134
+ function createUsageCircuit({ now = () => Date.now() } = {}) {
135
+ let consecutiveFailures = 0;
136
+ let currentState = 'closed';
137
+ let backoffBaseMs = 0;
138
+ let nextProbeAtMs = 0;
139
+ let probeInFlight = false;
140
+ // ms timestamp the circuit most recently transitioned closed -> open; kept
141
+ // through half_open <-> open cycles of the SAME streak (a failed probe
142
+ // re-arms the timer but the meter has been down continuously since this
143
+ // timestamp), cleared only by recordSuccess. Feeds health.cjs's
144
+ // GREEN/YELLOW/RED ladder (open duration), not just a failure count.
145
+ let openedAtMs = null;
146
+
147
+ function scheduleNextProbe() {
148
+ const intervalMs = backoffWithJitter(backoffBaseMs);
149
+ backoffBaseMs = doubledBase(backoffBaseMs);
150
+ nextProbeAtMs = now() + intervalMs;
151
+ }
152
+
153
+ function state() {
154
+ if (!breakerEnabled()) return 'closed';
155
+ if (currentState === 'open' && !probeInFlight && now() >= nextProbeAtMs) {
156
+ currentState = 'half_open';
157
+ probeInFlight = true;
158
+ }
159
+ return currentState;
160
+ }
161
+
162
+ function recordFailure(kind) { // eslint-disable-line no-unused-vars
163
+ consecutiveFailures += 1;
164
+ if (!breakerEnabled()) return;
165
+ if (currentState === 'half_open') {
166
+ probeInFlight = false;
167
+ currentState = 'open';
168
+ scheduleNextProbe();
169
+ return;
170
+ }
171
+ if (currentState === 'closed' && consecutiveFailures >= 3) {
172
+ currentState = 'open';
173
+ openedAtMs = now();
174
+ scheduleNextProbe();
175
+ }
176
+ }
177
+
178
+ function recordSuccess(payload) { // eslint-disable-line no-unused-vars
179
+ consecutiveFailures = 0;
180
+ probeInFlight = false;
181
+ backoffBaseMs = 0;
182
+ currentState = 'closed';
183
+ openedAtMs = null;
184
+ }
185
+
186
+ /** ms timestamp this streak opened, or null while closed. */
187
+ function openedAt() {
188
+ return currentState === 'open' || currentState === 'half_open' ? openedAtMs : null;
189
+ }
190
+
191
+ function getConsecutiveFailures() {
192
+ return consecutiveFailures;
193
+ }
194
+
195
+ return { recordSuccess, recordFailure, state, openedAt, getConsecutiveFailures };
196
+ }
197
+
198
+ module.exports = {
199
+ createUsageCircuit,
200
+ backoffWithJitter,
201
+ isResetFresh,
202
+ bindingWindow,
203
+ degradedBudget,
204
+ degradedConcurrencyCap,
205
+ singleFlight,
206
+ };
@@ -4,14 +4,11 @@
4
4
  // Testable without spawning the watchdog entry script.
5
5
 
6
6
  const fs = require('node:fs');
7
- const os = require('node:os');
8
7
  const path = require('node:path');
8
+ const schedulerPaths = require('./schedulerPaths.cjs');
9
+ const { isRestartingMarkerActive } = require('./upgradeDrain.cjs');
9
10
 
10
11
 
11
- // Mirrors scheduler.cjs:215 (single source of truth there; kept in sync here).
12
- const DEFAULT_HEARTBEAT_PATH = path.join(
13
- os.homedir(), '.claude', 'session-manager', 'scheduler-heartbeat.log',
14
- );
15
12
 
16
13
  // The in-app heartbeat ticks every 60 s; 3 missed ticks = stale.
17
14
  const DEFAULT_MAX_AGE_MS = 180_000;
@@ -40,7 +37,7 @@ const TAIL_BYTES = 4096;
40
37
  * Single source of truth for the file-read + reverse-scan logic; used by
41
38
  * readLastHeartbeatTs() and heartbeatFresh().
42
39
  */
43
- function readLastHeartbeat(heartbeatPath = DEFAULT_HEARTBEAT_PATH) {
40
+ function readLastHeartbeat(heartbeatPath = schedulerPaths.heartbeatPath()) {
44
41
  let buf;
45
42
  try {
46
43
  const stat = fs.statSync(heartbeatPath);
@@ -79,7 +76,7 @@ function readLastHeartbeat(heartbeatPath = DEFAULT_HEARTBEAT_PATH) {
79
76
  * Returns the `ts` field (epoch ms) from the last heartbeat entry.
80
77
  * Returns null on missing / empty / unparseable file or missing ts field.
81
78
  */
82
- function readLastHeartbeatTs(heartbeatPath = DEFAULT_HEARTBEAT_PATH) {
79
+ function readLastHeartbeatTs(heartbeatPath = schedulerPaths.heartbeatPath()) {
83
80
  const entry = readLastHeartbeat(heartbeatPath);
84
81
  return (entry !== null && typeof entry.ts === 'number') ? entry.ts : null;
85
82
  }
@@ -88,12 +85,43 @@ function readLastHeartbeatTs(heartbeatPath = DEFAULT_HEARTBEAT_PATH) {
88
85
  * heartbeatFresh(heartbeatPath?, maxAgeMs?) → boolean
89
86
  *
90
87
  * Returns true iff the last heartbeat ts is within maxAgeMs of `now`.
91
- * Missing / empty / unparseable file → false.
88
+ * Missing / empty / unparseable file → false. A `degraded: true` line (a
89
+ * heartbeat subsystem threw — see scheduler.cjs heartbeatTick) is NOT fresh:
90
+ * the loop ran but could not read state, so it must not disarm the watchdog.
92
91
  */
93
- function heartbeatFresh(heartbeatPath = DEFAULT_HEARTBEAT_PATH, maxAgeMs = DEFAULT_MAX_AGE_MS) {
94
- const ts = readLastHeartbeatTs(heartbeatPath);
95
- if (ts === null) return false;
96
- return (Date.now() - ts) < maxAgeMs;
92
+ function heartbeatFresh(heartbeatPath = schedulerPaths.heartbeatPath(), maxAgeMs = DEFAULT_MAX_AGE_MS) {
93
+ const entry = readLastHeartbeat(heartbeatPath);
94
+ if (entry === null || entry.degraded === true || typeof entry.ts !== 'number') return false;
95
+ return (Date.now() - entry.ts) < maxAgeMs;
96
+ }
97
+
98
+ const DEFAULT_DISPATCH_DEAD_MS = 30 * 60_000;
99
+
100
+ /**
101
+ * evaluateDispatchLiveness(entry, now, { deadMs }?) → { dead, reason }
102
+ *
103
+ * Pure. Observability only — a logically-dead scheduler (wedged tick chain,
104
+ * leaked slots) still writes fresh heartbeats, so freshness alone can't see
105
+ * it. `dead` only when ALL hold: heartbeat fresh (not degraded, within
106
+ * DEFAULT_MAX_AGE_MS), dispatch.lastRunAt older than deadMs (absent/unparseable
107
+ * counts as older), pendingDispatchable > 0, runningCount === 0, not paused.
108
+ * Callers must NOT relaunch on this: the process is alive, and a relaunch
109
+ * would spawn a second app on top of it.
110
+ */
111
+ function evaluateDispatchLiveness(entry, now = Date.now(), { deadMs = DEFAULT_DISPATCH_DEAD_MS } = {}) {
112
+ if (!entry || entry.degraded === true || typeof entry.ts !== 'number') {
113
+ return { dead: false, reason: 'no-fresh-heartbeat' };
114
+ }
115
+ if (now - entry.ts >= DEFAULT_MAX_AGE_MS) return { dead: false, reason: 'heartbeat-stale' };
116
+ const d = entry.dispatch;
117
+ if (!d || typeof d !== 'object') return { dead: false, reason: 'no-dispatch-field' };
118
+ if (d.paused) return { dead: false, reason: 'paused' };
119
+ if (d.drain) return { dead: false, reason: 'draining' };
120
+ if (!(d.pendingDispatchable > 0)) return { dead: false, reason: 'nothing-dispatchable' };
121
+ if (d.runningCount !== 0) return { dead: false, reason: 'jobs-running' };
122
+ const lastRunMs = Date.parse(d.lastRunAt ?? '');
123
+ if (Number.isFinite(lastRunMs) && now - lastRunMs <= deadMs) return { dead: false, reason: 'recent-dispatch' };
124
+ return { dead: true, reason: 'dispatch-dead' };
97
125
  }
98
126
 
99
127
  /**
@@ -132,12 +160,6 @@ function isPidAlive(pid) {
132
160
  // 60 s by scheduler.cjs's heartbeatInterval regardless of pause state) as the
133
161
  // liveness signal — no second heartbeat file.
134
162
 
135
- const DEFAULT_RELAUNCH_STATE_PATH = path.join(
136
- os.homedir(), '.claude', 'session-manager', 'watchdog-relaunch-state.json',
137
- );
138
- const DEFAULT_RELAUNCH_LOG_PATH = path.join(
139
- os.homedir(), '.claude', 'logs', 'scheduler-watchdog-relaunch.log',
140
- );
141
163
  const DEFAULT_RELAUNCH_DEBOUNCE_MS = 90_000;
142
164
  const DEFAULT_MAX_RELAUNCH_ATTEMPTS = 3;
143
165
 
@@ -150,7 +172,7 @@ const DEFAULT_MAX_RELAUNCH_ATTEMPTS = 3;
150
172
  * rather than relaunching on top of a live process.
151
173
  */
152
174
  function checkAppLiveness({
153
- heartbeatPath = DEFAULT_HEARTBEAT_PATH,
175
+ heartbeatPath = schedulerPaths.heartbeatPath(),
154
176
  maxAgeMs = DEFAULT_MAX_AGE_MS,
155
177
  } = {}) {
156
178
  if (heartbeatFresh(heartbeatPath, maxAgeMs)) {
@@ -164,7 +186,7 @@ function checkAppLiveness({
164
186
  }
165
187
 
166
188
  /** Default { lastAttemptTs: null, attemptCount: 0 } on missing/unparseable state file. */
167
- function readRelaunchState(statePath = DEFAULT_RELAUNCH_STATE_PATH) {
189
+ function readRelaunchState(statePath = schedulerPaths.watchdogRelaunchStatePath()) {
168
190
  try {
169
191
  const raw = JSON.parse(fs.readFileSync(statePath, 'utf8'));
170
192
  return {
@@ -203,7 +225,7 @@ function logRelaunchLine(logPath, message) {
203
225
  * (short-lived) process doesn't block on the launched app and the app
204
226
  * survives the watchdog exiting.
205
227
  */
206
- function defaultSpawnRelaunch({ logPath = DEFAULT_RELAUNCH_LOG_PATH } = {}) {
228
+ function defaultSpawnRelaunch({ logPath = schedulerPaths.watchdogRelaunchLogPath() } = {}) {
207
229
  const { spawn } = require('node:child_process');
208
230
  fs.mkdirSync(path.dirname(logPath), { recursive: true });
209
231
  const fd = fs.openSync(logPath, 'a');
@@ -236,14 +258,15 @@ function defaultSpawnRelaunch({ logPath = DEFAULT_RELAUNCH_LOG_PATH } = {}) {
236
258
  * intervenes or the app comes up some other way.
237
259
  */
238
260
  function maybeRelaunchApp({
239
- heartbeatPath = DEFAULT_HEARTBEAT_PATH,
261
+ heartbeatPath = schedulerPaths.heartbeatPath(),
240
262
  maxAgeMs = DEFAULT_MAX_AGE_MS,
241
- statePath = DEFAULT_RELAUNCH_STATE_PATH,
242
- logPath = DEFAULT_RELAUNCH_LOG_PATH,
263
+ statePath = schedulerPaths.watchdogRelaunchStatePath(),
264
+ logPath = schedulerPaths.watchdogRelaunchLogPath(),
243
265
  debounceMs = DEFAULT_RELAUNCH_DEBOUNCE_MS,
244
266
  maxAttempts = DEFAULT_MAX_RELAUNCH_ATTEMPTS,
245
267
  now = Date.now(),
246
268
  spawnFn = defaultSpawnRelaunch,
269
+ restartingMarkerPath = schedulerPaths.restartingMarkerPath(),
247
270
  } = {}) {
248
271
  const liveness = checkAppLiveness({ heartbeatPath, maxAgeMs });
249
272
  const state = readRelaunchState(statePath);
@@ -255,6 +278,14 @@ function maybeRelaunchApp({
255
278
  return { relaunched: false, reason: 'alive', attemptCount: 0 };
256
279
  }
257
280
 
281
+ // A deliberate drain-restart is mid-exit: the old process is going away ON
282
+ // PURPOSE and its successor is booting. Relaunching now would race the
283
+ // single-instance lock. The marker's lifetime is bounded, so a restart that
284
+ // never comes up is picked up again once it expires.
285
+ if (isRestartingMarkerActive({ file: restartingMarkerPath, now })) {
286
+ return { relaunched: false, reason: 'restarting', attemptCount: state.attemptCount };
287
+ }
288
+
258
289
  if (state.attemptCount >= maxAttempts) {
259
290
  logRelaunchLine(
260
291
  logPath,
@@ -286,12 +317,6 @@ function maybeRelaunchApp({
286
317
  // walk/persist logic — this is just the once-per-day scheduling + lock
287
318
  // wrapper around it, tailored to being invoked repeatedly by a systemd timer.
288
319
 
289
- const DEFAULT_STAMP_PATH = path.join(
290
- os.homedir(), '.claude', 'session-manager', 'history-rollup.stamp',
291
- );
292
- const DEFAULT_LOCK_PATH = path.join(
293
- os.homedir(), '.claude', 'session-manager', 'history-rollup.lock',
294
- );
295
320
  const DEFAULT_LOCK_STALE_MS = 10 * 60 * 1000; // 10 min
296
321
  const DEFAULT_FINALIZE_BUDGET_MS = 60_000;
297
322
 
@@ -362,7 +387,7 @@ function releaseLock(lockPath) {
362
387
  * dependency fails inside maybeFinalizeHistory's try/catch, not at require
363
388
  * time for the whole watchdog script. */
364
389
  function defaultFinalizeClosedDays(opts) {
365
- const { finalizeClosedDays } = require('../../src/main/historyAggregator.cjs');
390
+ const { finalizeClosedDays } = require('../historyAggregator.cjs');
366
391
  return finalizeClosedDays(opts);
367
392
  }
368
393
 
@@ -384,8 +409,8 @@ function defaultFinalizeClosedDays(opts) {
384
409
  * finalizeFn — injectable finalizeClosedDays for testing
385
410
  */
386
411
  async function maybeFinalizeHistory({
387
- stampPath = DEFAULT_STAMP_PATH,
388
- lockPath = DEFAULT_LOCK_PATH,
412
+ stampPath = schedulerPaths.historyRollupStampPath(),
413
+ lockPath = schedulerPaths.historyRollupLockPath(),
389
414
  staleLockMs = DEFAULT_LOCK_STALE_MS,
390
415
  budgetMs = DEFAULT_FINALIZE_BUDGET_MS,
391
416
  dryRun = process.env.SM_WATCHDOG_DRYRUN === '1',
@@ -423,6 +448,7 @@ module.exports = {
423
448
  readLastHeartbeat,
424
449
  readLastHeartbeatTs,
425
450
  heartbeatFresh,
451
+ evaluateDispatchLiveness,
426
452
  isPidAlive,
427
453
  localDateStr,
428
454
  maybeFinalizeHistory,
@@ -433,14 +459,21 @@ module.exports = {
433
459
  writeRelaunchState,
434
460
  defaultSpawnRelaunch,
435
461
  maybeRelaunchApp,
436
- DEFAULT_HEARTBEAT_PATH,
437
462
  DEFAULT_MAX_AGE_MS,
438
- DEFAULT_STAMP_PATH,
439
- DEFAULT_LOCK_PATH,
463
+ DEFAULT_DISPATCH_DEAD_MS,
440
464
  DEFAULT_LOCK_STALE_MS,
441
465
  DEFAULT_FINALIZE_BUDGET_MS,
442
- DEFAULT_RELAUNCH_STATE_PATH,
443
- DEFAULT_RELAUNCH_LOG_PATH,
444
466
  DEFAULT_RELAUNCH_DEBOUNCE_MS,
445
467
  DEFAULT_MAX_RELAUNCH_ATTEMPTS,
446
468
  };
469
+
470
+ // Lazy default-path getters (SM_SCHEDULER_HOME resolved at read, never at require).
471
+ for (const [name, resolve] of [
472
+ ['DEFAULT_HEARTBEAT_PATH', schedulerPaths.heartbeatPath],
473
+ ['DEFAULT_STAMP_PATH', schedulerPaths.historyRollupStampPath],
474
+ ['DEFAULT_LOCK_PATH', schedulerPaths.historyRollupLockPath],
475
+ ['DEFAULT_RELAUNCH_STATE_PATH', schedulerPaths.watchdogRelaunchStatePath],
476
+ ['DEFAULT_RELAUNCH_LOG_PATH', schedulerPaths.watchdogRelaunchLogPath],
477
+ ]) {
478
+ Object.defineProperty(module.exports, name, { get: resolve, enumerable: true });
479
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * withTimeout.cjs — bound an async body with a wall-clock budget.
3
+ *
4
+ * `withTimeout(promiseFactory, ms, onTimeout)` starts `promiseFactory()` and
5
+ * resolves with its result. If it has not settled after `ms`, `onTimeout()`
6
+ * is called and ITS return value becomes the resolved value; the original
7
+ * promise is abandoned (never cancelled — callers that must neutralise a
8
+ * late-resuming body do so with their own generation fence). The timer is
9
+ * always cleared on settle, and unref'd so it can never hold the process open.
10
+ * `ms <= 0` (or non-finite) disables the bound entirely.
11
+ *
12
+ * Plain Node module (no Electron deps).
13
+ */
14
+ 'use strict';
15
+
16
+ function withTimeout(promiseFactory, ms, onTimeout) {
17
+ if (!Number.isFinite(ms) || ms <= 0) return Promise.resolve().then(promiseFactory);
18
+ return new Promise((resolve, reject) => {
19
+ let done = false;
20
+ const timer = setTimeout(() => {
21
+ if (done) return;
22
+ done = true;
23
+ try { resolve(onTimeout()); } catch (e) { reject(e); }
24
+ }, ms);
25
+ if (typeof timer.unref === 'function') timer.unref();
26
+ Promise.resolve().then(promiseFactory).then(
27
+ (v) => { if (done) return; done = true; clearTimeout(timer); resolve(v); },
28
+ (e) => { if (done) return; done = true; clearTimeout(timer); reject(e); },
29
+ );
30
+ });
31
+ }
32
+
33
+ module.exports = { withTimeout };
@@ -10,7 +10,8 @@
10
10
  */
11
11
 
12
12
  const { spawn } = require('node:child_process');
13
- const { resolveClaudeBin } = require('./lib/claudeBin.cjs');
13
+ const { resolveClaudeBin, claudeSpawnTarget } = require('./lib/claudeBin.cjs');
14
+ const { withProcRole } = require('./lib/cleanEnv.cjs');
14
15
 
15
16
  const PROBE_TIMEOUT_MS = 30_000;
16
17
 
@@ -56,7 +57,8 @@ function runProbe() {
56
57
  }
57
58
  let child;
58
59
  try {
59
- child = spawn(bin, ['mcp', 'list'], { env: process.env, stdio: ['ignore', 'pipe', 'pipe'] });
60
+ const target = claudeSpawnTarget('aux', 'mcp-list', bin);
61
+ child = spawn(target.command, ['mcp', 'list'], { env: withProcRole(process.env, 'aux'), stdio: ['ignore', 'pipe', 'pipe'], ...(target.argv0 ? { argv0: target.argv0 } : {}) });
60
62
  } catch (e) {
61
63
  resolve({ ok: false, servers: [], error: e?.message || 'spawn error', checkedAt: Date.now() });
62
64
  return;