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
@@ -12,7 +12,8 @@
12
12
  *
13
13
  * Directory shape: one subdirectory per tick (`RUNS_DIR/<iso-ts>/`, minted by
14
14
  * scheduler.cjs's pickRunDir), which commonly holds MANY different PRD
15
- * slugs' artifacts side by side — `<slug>.log`, `<slug>.meta.json`, and an
15
+ * slugs' artifacts side by side — `<slug>.log`, `<slug>.meta.json`, the
16
+ * `<slug>.supervisor.json` dispatch record (jobSupervisorRecord.cjs), and an
16
17
  * optional `root-cause-<slug>.md` (rcaReport.cjs) per slug. Retention is
17
18
  * therefore computed per (runId, slug) ENTRY, not per directory: a directory
18
19
  * is only "fully removable" once every entry it holds is independently
@@ -131,7 +132,7 @@ function scanRunEntries(runsDir) {
131
132
  for (const metaFile of metaFiles) {
132
133
  const slug = metaFile.slice(0, -META_SUFFIX.length);
133
134
  const relatedNames = files.filter(
134
- (f) => f === metaFile || f === `${slug}.log` || f === `root-cause-${slug}.md`
135
+ (f) => f === metaFile || f === `${slug}.log` || f === `${slug}.supervisor.json` || f === `root-cause-${slug}.md`
135
136
  );
136
137
 
137
138
  let sizeBytes = 0;
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Canonical runtime schema for a ScheduleJob record, mirroring the TS
3
- * `ScheduleJob` interface at src/preload/api.d.ts:375-422 field-for-field.
3
+ * `ScheduleJob` interface (`ScheduleJob` in src/preload/api.d.ts) field-for-field.
4
4
  *
5
5
  * Modelled directly on promptSessionSchema.cjs's "zod schema asserted at the
6
6
  * main-process boundary" pattern: the Epic entity has had this guarantee
@@ -52,7 +52,7 @@ const ScheduleJobStatusHistoryEntrySchema = z
52
52
  .passthrough();
53
53
 
54
54
  // Only `slug` and `status` are required. Every other ScheduleJob field
55
- // (src/preload/api.d.ts:375-422) is declared here for documentation and type
55
+ // (`ScheduleJob` in src/preload/api.d.ts) is declared here for documentation and type
56
56
  // checking when present, but kept optional: a job row moves through several
57
57
  // legitimate partial shapes across its life (freshly minted with nulls,
58
58
  // mid-run with runtime fields, terminal with exitCode/error) and plenty of
@@ -124,6 +124,14 @@ const ScheduleJobSchema = z
124
124
  // finalize (both the normal completion path and reapDeadRunningJobs).
125
125
  dispatchPhase: z.string().optional(),
126
126
  dispatchPhaseAt: z.string().optional(),
127
+ // needs_review episode bookkeeping (needsReviewLedger.cjs / PRD: needs_review
128
+ // durability): stamped by scheduleJobTransitions.cjs the instant a job
129
+ // transitions INTO 'needs_review', so the RESOLUTION line written when it
130
+ // later transitions back OUT can reference the same runId and compute a
131
+ // truthful dwell time — same lifecycle as heldReason/dispatchPhase (stamped
132
+ // at entry, deleted at resolution).
133
+ needsReviewEntryRunId: z.string().nullable().optional(),
134
+ needsReviewEnteredAt: z.string().nullable().optional(),
127
135
  })
128
136
  .passthrough();
129
137
 
@@ -32,6 +32,8 @@
32
32
 
33
33
  const { appendAuditEvent } = require('./auditLog.cjs');
34
34
  const telemetryCounters = require('./telemetryCounters.cjs');
35
+ const queueHistory = require('./queueHistory.cjs');
36
+ const { buildNeedsReviewEntryLine, buildNeedsReviewResolutionLine } = require('./needsReviewLedger.cjs');
35
37
 
36
38
  // A run genuinely finished when it leaves 'running' for one of these —
37
39
  // distinct from every other legal edge in LEGAL_TRANSITIONS (retries,
@@ -195,6 +197,64 @@ function transitionJob(job, toStatus, { reason, source, allowAnyFrom = false } =
195
197
  telemetryCounters.trackSchedulerJobFinish({ status: toStatus });
196
198
  }
197
199
 
200
+ // needs_review ledger (PRD: needs_review durability). This IS the single
201
+ // funnel every needs_review transition — entry or resolution — passes
202
+ // through (see this file's own header comment: transitionJob is the ONE
203
+ // place `status` is assigned), so hooking here, rather than any one of
204
+ // needs_review's several distinct entry call sites in scheduler.cjs,
205
+ // guarantees no path can bypass it.
206
+ //
207
+ // Fire-and-forget, exactly like every other best-effort side-channel write
208
+ // in this codebase (rcaReport.cjs, auditLog.cjs's own posture) — a ledger
209
+ // write failure must never fail or delay the transition it's recording.
210
+ // 'investigating' is a PAUSE, not a resolution, when it's reached FROM
211
+ // needs_review: spawnInvestigation always restores the job to exactly the
212
+ // status it left (a snapshot taken before the pause), so
213
+ // needs_review -> investigating -> needs_review is one continuous episode,
214
+ // not two. Without this check, the pause mints a spurious 'auto-fix'
215
+ // resolution mid-episode, then the resume mints a spurious fresh entry
216
+ // that overwrites the original episode's runId/enteredAt/reason in the
217
+ // ledger's by-key maps (keyed on slug+runId) — corrupting byLadderRung and
218
+ // understating dwell time. Detected via the stamped episode fields
219
+ // themselves rather than the transition's source string, since the same
220
+ // 'investigating' status is also reached fresh from 'failed' (a job that
221
+ // was never parked needs_review at all, and has no episode to pause).
222
+ const isNeedsReviewPause = from === 'needs_review' && toStatus === 'investigating';
223
+ const pausedEpisodeOpen = job.needsReviewEntryRunId !== undefined;
224
+ const isNeedsReviewResume = from === 'investigating' && pausedEpisodeOpen;
225
+
226
+ if (toStatus === 'needs_review' && from !== 'needs_review' && !isNeedsReviewResume) {
227
+ // ENTRY: stamp the episode's runId/enteredAt on the job itself so the
228
+ // RESOLUTION line (below) can reference the SAME runId even if the job's
229
+ // own `runId` is reassigned in between (e.g. a resume-recovery
230
+ // re-dispatch mints a fresh one before this episode resolves).
231
+ job.needsReviewEntryRunId = job.runId ?? null;
232
+ job.needsReviewEnteredAt = entry.at;
233
+ queueHistory.appendHistory([buildNeedsReviewEntryLine(job, entry)]).catch((e) => {
234
+ console.error('[scheduleJobTransitions] failed to append needs_review entry', e?.message ?? String(e));
235
+ });
236
+ } else if (
237
+ (from === 'needs_review' && toStatus !== 'needs_review' && !isNeedsReviewPause)
238
+ || (isNeedsReviewResume && toStatus !== 'needs_review')
239
+ ) {
240
+ // RESOLUTION: read the episode stamped at entry, then clear it — the
241
+ // episode is over regardless of what this resolution line looks like.
242
+ // The `isNeedsReviewResume` arm covers an episode that paused for
243
+ // investigation and is now resolving to something other than
244
+ // needs_review (e.g. the investigation actually fixed it) — the episode
245
+ // stamps were deliberately left untouched across the pause, so this
246
+ // still references the ORIGINAL entry time, not the pause's.
247
+ const episode = { runId: job.needsReviewEntryRunId ?? null, enteredAt: job.needsReviewEnteredAt ?? null };
248
+ delete job.needsReviewEntryRunId;
249
+ delete job.needsReviewEnteredAt;
250
+ queueHistory.appendHistory([buildNeedsReviewResolutionLine(job, entry, episode)]).catch((e) => {
251
+ console.error('[scheduleJobTransitions] failed to append needs_review resolution', e?.message ?? String(e));
252
+ });
253
+ }
254
+ // else: isNeedsReviewPause (needs_review -> investigating), or
255
+ // isNeedsReviewResume && toStatus === 'needs_review' (investigating ->
256
+ // needs_review, still the same open episode) — intentionally a no-op.
257
+
198
258
  return true;
199
259
  }
200
260
 
@@ -75,7 +75,7 @@ function dedupeCycles(cycles) {
75
75
  return out;
76
76
  }
77
77
 
78
- function findBlockingDep(job, projectJobs, satisfiedSlugs = new Set()) {
78
+ function makeDepResolver(projectJobs, satisfiedSlugs = new Set()) {
79
79
  const rowBySlug = new Map(projectJobs.map((j) => [j.slug, j]));
80
80
  const rowsByBareSlug = new Map();
81
81
  for (const j of projectJobs) {
@@ -102,6 +102,31 @@ function findBlockingDep(job, projectJobs, satisfiedSlugs = new Set()) {
102
102
  }
103
103
  return satisfiedBareSlugs.has(bareSlug(slug));
104
104
  };
105
+ return { rowsForDep, isKnownSatisfied };
106
+ }
107
+
108
+ /**
109
+ * findUnresolvableDepRoots(projectJobs, satisfiedSlugs) → string[]
110
+ *
111
+ * Pure. The dep slugs that name NO live row and NO completion/archive record
112
+ * — the roots findBlockingDep holds as 'unresolved'. Unlike a failed/pending
113
+ * dep row, nothing in the queue will ever change one of these; only a human
114
+ * (or a restored record) can. Sorted, de-duplicated. O(jobs + deps).
115
+ */
116
+ function findUnresolvableDepRoots(projectJobs, satisfiedSlugs = new Set()) {
117
+ const { rowsForDep, isKnownSatisfied } = makeDepResolver(projectJobs, satisfiedSlugs);
118
+ const roots = new Set();
119
+ for (const j of projectJobs) {
120
+ if (j.status !== 'pending') continue;
121
+ for (const dep of j.dependsOn ?? []) {
122
+ if (rowsForDep(dep).length === 0 && !isKnownSatisfied(dep)) roots.add(dep);
123
+ }
124
+ }
125
+ return [...roots].sort();
126
+ }
127
+
128
+ function findBlockingDep(job, projectJobs, satisfiedSlugs = new Set()) {
129
+ const { rowsForDep, isKnownSatisfied } = makeDepResolver(projectJobs, satisfiedSlugs);
105
130
  // A dep row is blocking unless it's 'completed', OR it's a 'skipped' row
106
131
  // stamped `needsReviewAutoResolvedSkip` — the bounded needs_review
107
132
  // auto-resolve terminal decision (scheduler.cjs's
@@ -658,6 +683,6 @@ function findStarvedProjects(jobs, now, thresholdMs) {
658
683
  }
659
684
 
660
685
  module.exports = {
661
- pickForProject, pickNextBatch, enqueueTimestamp, findStarvedProjects, findBlockingDep, DEFAULT_PROJECT_CWD,
686
+ pickForProject, pickNextBatch, enqueueTimestamp, findStarvedProjects, findBlockingDep, findUnresolvableDepRoots, DEFAULT_PROJECT_CWD,
662
687
  DEP_HISTORY_FAIL_OPEN,
663
688
  };
@@ -56,6 +56,16 @@ module.exports = {
56
56
  // (pause/resume, job start/finish/reap/reset) bypass this via
57
57
  // broadcast({ flush: true }).
58
58
  BROADCAST_COALESCE_MS: 200,
59
+ // reconcile() pass duration above which a single warn-level breakdown is
60
+ // logged (per-phase ms, PRD file count, resolved dir count) — the only
61
+ // evidence trail for a slow pass otherwise requires hand-timing the scan
62
+ // from a throwaway script. Baseline measured 2026-09-13 on a machine with
63
+ // ~160 PRD dirs: allProjectCwds() ~270ms x5/pass, resolvePrdsDirs()
64
+ // ~430ms, historyTerminalBySlug() ~418ms over 784 entries — a healthy pass
65
+ // today is comfortably under 2s; this should trip well before a
66
+ // regression back toward those numbers compounds into the reported
67
+ // "schedule.state timed out after 5000ms" toast.
68
+ RECONCILE_SLOW_PASS_MS: 2_000,
59
69
  // Terminal (completed/failed) jobs older than this move from queue.json's
60
70
  // hot jobs[] into the append-only history.jsonl sidecar. See
61
71
  // src/main/lib/queueHistory.cjs.
@@ -109,6 +119,20 @@ module.exports = {
109
119
  // SM_JOB_OVERRUN_FLOOR_MINUTES.
110
120
  JOB_OVERRUN_FLOOR_MS: 45 * 60_000,
111
121
 
122
+ // Wall-clock KILL budget (distinct from JOB_OVERRUN_FACTOR/JOB_OVERRUN_FLOOR_MS
123
+ // above, which only ever ESCALATE — log + audit, deliberately never kill).
124
+ // This one ACTS: a RUNNING job past `budgetMs = clamp(estimateMinutes *
125
+ // JOB_BUDGET_FACTOR, JOB_BUDGET_FLOOR_MS, JOB_BUDGET_CEILING_MS)` is
126
+ // SIGTERMed and parked needs_review. Calibrated against 606 run-meta
127
+ // records since 2026-08-25: p50=13m, p90=60m, max=240m, against a
128
+ // concurrencyCap of 4 — two 4h outliers hold half the machine's dispatch
129
+ // capacity for an afternoon with nothing noticing or acting on it. Override
130
+ // with SM_JOB_BUDGET_FACTOR / SM_JOB_BUDGET_FLOOR_MINUTES /
131
+ // SM_JOB_BUDGET_CEILING_MINUTES.
132
+ JOB_BUDGET_FACTOR: 3,
133
+ JOB_BUDGET_FLOOR_MS: 45 * 60_000,
134
+ JOB_BUDGET_CEILING_MS: 180 * 60_000,
135
+
112
136
  // A 'running' row with no runtime.pid recorded (spawnJob's status:running
113
137
  // mutate at scheduler.cjs:~3524 landed, but the pid-bearing runtime={}
114
138
  // mutate that follows executeJob's spawn callback never did — the spawn
@@ -0,0 +1,175 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * schedulerPaths.cjs — single resolver for every machine-wide, home-rooted
5
+ * scheduler path. Plain Node (no electron) so queueStore.cjs and the watchdog
6
+ * can load it.
7
+ *
8
+ * Every export is a LAZY function: the root is read from the environment at
9
+ * point of use, never at require time, so a test (or a second instance) that
10
+ * sets SM_SCHEDULER_HOME redirects ALL scheduler state — queue shards, machine
11
+ * state, runs, PRDs, heartbeat, slot config, admin token — with no
12
+ * half-redirect. SM_SCHEDULER_HOME set on the app propagates to spawned
13
+ * executor jobs BY DESIGN (children inherit env), so a redirected app runs
14
+ * redirected jobs.
15
+ *
16
+ * Worktree roots are NOT under schedulerHome (they live on the tmp filesystem)
17
+ * but are resolved here all the same, via SM_WORKTREE_ROOT (else os.tmpdir()),
18
+ * so the job and epic roots always move together under one override.
19
+ */
20
+
21
+ const os = require('node:os');
22
+ const path = require('node:path');
23
+
24
+ function isUnder(child, parent) {
25
+ const rel = path.relative(path.resolve(parent), path.resolve(child));
26
+ return rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel));
27
+ }
28
+
29
+ /**
30
+ * Test-time fail-closed guard (same shape as opsOwnership.assertOpsWrite): under
31
+ * vitest (process.env.VITEST) a resolved path must sit under SM_SCHEDULER_HOME,
32
+ * SM_WORKTREE_ROOT, or os.tmpdir() — anything else is a LIVE root (real ~/.claude,
33
+ * real home) that a unit test must never touch. Returns `resolved` so resolvers
34
+ * can `return assertNotLiveRoot(...)`. O(#roots) per call.
35
+ */
36
+ function assertNotLiveRoot(resolved, resolverName) {
37
+ if (!process.env.VITEST) return resolved;
38
+ // Narrow, per-test opt-in for a READ-ONLY probe of this machine's real user config
39
+ // (health-delegation-chain.test.cjs). Never set it around anything that writes or sweeps.
40
+ if (process.env.SM_ALLOW_LIVE_ROOT_READS === '1') return resolved;
41
+ // HOLE CLOSED (2026-09-18 defaultCwd leak): the allow-list below is env-derived, and so was
42
+ // the notion of "live" — both follow process.env.HOME / SM_SCHEDULER_HOME, which a test (or an
43
+ // env inherited from the live app) freely reassigns. A live SM_SCHEDULER_HOME therefore
44
+ // ALLOW-LISTED the very root the guard exists to refuse, and a HOME swapped mid-test moved
45
+ // "live" out from under it. The passwd-database home (os.userInfo(), immune to $HOME) is the
46
+ // one anchor a test cannot rewrite: nothing under its ~/.claude may be touched, whatever the
47
+ // allow-list says.
48
+ if (isLivePasswdRoot(resolved)) throw liveRootError(resolved, resolverName);
49
+ const roots = [process.env.SM_SCHEDULER_HOME, process.env.SM_WORKTREE_ROOT, os.tmpdir()].filter(Boolean);
50
+ if (roots.some((r) => isUnder(resolved, r))) return resolved;
51
+ throw liveRootError(resolved, resolverName);
52
+ }
53
+
54
+ function liveRootError(resolved, resolverName) {
55
+ return new Error(
56
+ `schedulerPaths.${resolverName}() resolved to live root ${resolved} under vitest — ` +
57
+ 'set SM_SCHEDULER_HOME / SM_WORKTREE_ROOT / HOME to a temp dir (tests/setup/schedulerSandbox.cjs does this per run)',
58
+ );
59
+ }
60
+
61
+ /** True when `resolved` sits under the passwd-home's ~/.claude (HOME-env independent). */
62
+ function isLivePasswdRoot(resolved) {
63
+ let home;
64
+ try { home = os.userInfo().homedir; } catch { return false; }
65
+ if (!home) return false;
66
+ // A sandbox that genuinely lives under the passwd home's tmp is not live; only ~/.claude is.
67
+ return isUnder(resolved, path.join(home, '.claude'));
68
+ }
69
+
70
+ /** SM_SCHEDULER_HOME, else ~/.claude/session-manager. */
71
+ function schedulerHome() {
72
+ return assertNotLiveRoot(
73
+ process.env.SM_SCHEDULER_HOME || path.join(os.homedir(), '.claude', 'session-manager'),
74
+ 'schedulerHome',
75
+ );
76
+ }
77
+
78
+ /** ~/.claude — Claude Code's own config dir (NOT redirected by SM_SCHEDULER_HOME). */
79
+ function claudeHome() { return assertNotLiveRoot(path.join(os.homedir(), '.claude'), 'claudeHome'); }
80
+ function claudeProjectsDir() { return assertNotLiveRoot(path.join(claudeHome(), 'projects'), 'claudeProjectsDir'); }
81
+
82
+ function scheduledPlansRoot() { return assertNotLiveRoot(path.join(schedulerHome(), 'scheduled-plans'), 'scheduledPlansRoot'); }
83
+ function runsDir() { return assertNotLiveRoot(path.join(scheduledPlansRoot(), 'runs'), 'runsDir'); }
84
+ function prdsRoot() { return assertNotLiveRoot(path.join(scheduledPlansRoot(), 'prds'), 'prdsRoot'); }
85
+ function machineStatePath() { return assertNotLiveRoot(path.join(schedulerHome(), 'scheduler-machine.json'), 'machineStatePath'); }
86
+ function legacyQueuePath() { return assertNotLiveRoot(path.join(scheduledPlansRoot(), 'queue.json'), 'legacyQueuePath'); }
87
+ function schedulerStatePath() { return assertNotLiveRoot(path.join(schedulerHome(), 'scheduler-state.json'), 'schedulerStatePath'); }
88
+ function heartbeatPath() { return assertNotLiveRoot(path.join(schedulerHome(), 'scheduler-heartbeat.log'), 'heartbeatPath'); }
89
+ function sessionSlotsConfigPath() { return assertNotLiveRoot(path.join(schedulerHome(), 'session-slots-config.json'), 'sessionSlotsConfigPath'); }
90
+ function watchdogLogsDir() { return assertNotLiveRoot(path.join(schedulerHome(), 'logs'), 'watchdogLogsDir'); }
91
+ function restartRequestPath() { return assertNotLiveRoot(path.join(schedulerHome(), 'restart-request.json'), 'restartRequestPath'); }
92
+ function restartingMarkerPath() { return assertNotLiveRoot(path.join(schedulerHome(), 'restarting.json'), 'restartingMarkerPath'); }
93
+ function watchdogRelaunchStatePath() { return assertNotLiveRoot(path.join(schedulerHome(), 'watchdog-relaunch-state.json'), 'watchdogRelaunchStatePath'); }
94
+ // Sibling of schedulerHome() (i.e. ~/.claude/logs by default), not inside it.
95
+ function watchdogRelaunchLogPath() {
96
+ return assertNotLiveRoot(
97
+ path.join(path.dirname(schedulerHome()), 'logs', 'scheduler-watchdog-relaunch.log'),
98
+ 'watchdogRelaunchLogPath',
99
+ );
100
+ }
101
+ function historyRollupStampPath() { return assertNotLiveRoot(path.join(schedulerHome(), 'history-rollup.stamp'), 'historyRollupStampPath'); }
102
+ function historyRollupLockPath() { return assertNotLiveRoot(path.join(schedulerHome(), 'history-rollup.lock'), 'historyRollupLockPath'); }
103
+ function historyRollupPath() { return path.join(schedulerHome(), 'history-rollup.jsonl'); }
104
+ function auditLogPath() { return path.join(schedulerHome(), 'audit-log.jsonl'); }
105
+ function procnamesRoot() { return path.join(schedulerHome(), 'procnames'); }
106
+ /** Legacy global history sidecar (read-only; new appends go to per-project shards). */
107
+ function queueHistoryPath() { return path.join(scheduledPlansRoot(), 'history.jsonl'); }
108
+ function instanceLockPath() { return path.join(schedulerHome(), 'scheduler-owner.lock'); }
109
+ /** Where heap snapshots are written — the scheduler home itself. */
110
+ function heapSnapshotDir() { return schedulerHome(); }
111
+
112
+ /** SM_WORKTREE_ROOT, else os.tmpdir() — the parent of every managed worktree root. */
113
+ function worktreeBase() { return assertNotLiveRoot(process.env.SM_WORKTREE_ROOT || os.tmpdir(), 'worktreeBase'); }
114
+
115
+ /** Managed worktree root for `kind` ('job' | 'epic'), resolved fresh on every call. */
116
+ function worktreeRoot(kind) {
117
+ return assertNotLiveRoot(path.join(worktreeBase(), `session-manager-${kind}-worktrees`), 'worktreeRoot');
118
+ }
119
+
120
+ /**
121
+ * Mode-aware admin token path, resolved fresh on every call. Precedence:
122
+ * SM_ADMIN_TOKEN_PATH (explicit override) > SM_DEV > SM_E2E > production file.
123
+ * index.cjs treats SM_DEV and SM_E2E as one OR'd boolean; if both are set the
124
+ * SM_DEV path wins (arbitrary but harmless — real launches set one).
125
+ */
126
+ function adminTokenPath() {
127
+ if (process.env.SM_ADMIN_TOKEN_PATH) return assertNotLiveRoot(process.env.SM_ADMIN_TOKEN_PATH, 'adminTokenPath');
128
+ if (process.env.SM_DEV === '1') return assertNotLiveRoot(path.join(schedulerHome(), 'admin-api.dev.json'), 'adminTokenPath');
129
+ if (process.env.SM_E2E === '1') return assertNotLiveRoot(path.join(schedulerHome(), 'admin-api.e2e.json'), 'adminTokenPath');
130
+ return assertNotLiveRoot(path.join(schedulerHome(), 'admin-api.json'), 'adminTokenPath');
131
+ }
132
+
133
+ /**
134
+ * cwd that machine-level scheduler errors are attributed to (they have no
135
+ * owning project). SM_SCHEDULER_LOG_CWD, else the literal
136
+ * ~/Projects/session-manager — deliberately NOT inferred from process.cwd().
137
+ */
138
+ function machineStateLogCwd() {
139
+ return assertNotLiveRoot(
140
+ process.env.SM_SCHEDULER_LOG_CWD || path.join(os.homedir(), 'Projects', 'session-manager'),
141
+ 'machineStateLogCwd',
142
+ );
143
+ }
144
+
145
+ module.exports = {
146
+ assertNotLiveRoot,
147
+ claudeHome,
148
+ claudeProjectsDir,
149
+ schedulerHome,
150
+ scheduledPlansRoot,
151
+ runsDir,
152
+ prdsRoot,
153
+ machineStatePath,
154
+ legacyQueuePath,
155
+ schedulerStatePath,
156
+ heartbeatPath,
157
+ sessionSlotsConfigPath,
158
+ adminTokenPath,
159
+ machineStateLogCwd,
160
+ worktreeBase,
161
+ worktreeRoot,
162
+ watchdogLogsDir,
163
+ restartRequestPath,
164
+ restartingMarkerPath,
165
+ watchdogRelaunchStatePath,
166
+ watchdogRelaunchLogPath,
167
+ historyRollupStampPath,
168
+ historyRollupLockPath,
169
+ historyRollupPath,
170
+ auditLogPath,
171
+ procnamesRoot,
172
+ queueHistoryPath,
173
+ instanceLockPath,
174
+ heapSnapshotDir,
175
+ };
@@ -0,0 +1,59 @@
1
+ /**
2
+ * schedulerRuntimeState.cjs — process-local runtime reservations that used to
3
+ * be bare ++/-- counters in scheduler.cjs. The auto-fix investigation cap is
4
+ * now a Map of slug → { claimedAt, pid } so a reservation stranded by a
5
+ * promise that never settles can be expired when its owner is provably dead
6
+ * (reservationExpiry.isProvablyDead), instead of shrinking the cap forever.
7
+ */
8
+ 'use strict';
9
+
10
+ const { appendAuditEvent } = require('./auditLog.cjs');
11
+ const { isProvablyDead, DEFAULT_GRACE_MS } = require('./reservationExpiry.cjs');
12
+
13
+ const investigations = new Map(); // slug -> { claimedAt, pid }
14
+
15
+ function investigationCount() { return investigations.size; }
16
+
17
+ /** reserveInvestigation(slug) — false if already reserved. Cap checks stay with the caller. */
18
+ function reserveInvestigation(slug, { claimedAt = Date.now() } = {}) {
19
+ const key = String(slug);
20
+ if (investigations.has(key)) return false;
21
+ investigations.set(key, { claimedAt, pid: null });
22
+ return true;
23
+ }
24
+
25
+ function stampInvestigationPid(slug, pid) {
26
+ const r = investigations.get(String(slug));
27
+ if (!r || !Number.isInteger(pid) || pid <= 0) return false;
28
+ r.pid = pid;
29
+ return true;
30
+ }
31
+
32
+ /** releaseInvestigation — idempotent; returns whether a reservation was removed. */
33
+ function releaseInvestigation(slug) {
34
+ return investigations.delete(String(slug));
35
+ }
36
+
37
+ /** Expire investigations whose slug has no `investigating` row and whose owner is provably dead. */
38
+ function expireDeadInvestigations({ liveSlugs, pidAlive, now = Date.now(), graceMs = DEFAULT_GRACE_MS } = {}) {
39
+ const live = liveSlugs instanceof Set ? liveSlugs : new Set(liveSlugs || []);
40
+ const expired = [];
41
+ for (const [slug, r] of [...investigations]) {
42
+ if (!isProvablyDead(r, { live: live.has(slug), pidAlive, now, graceMs })) continue;
43
+ investigations.delete(slug);
44
+ expired.push(slug);
45
+ appendAuditEvent('investigation_reservation_expired', { slug, pid: r.pid });
46
+ }
47
+ return expired;
48
+ }
49
+
50
+ function __resetForTests() { investigations.clear(); }
51
+
52
+ module.exports = {
53
+ investigationCount,
54
+ reserveInvestigation,
55
+ stampInvestigationPid,
56
+ releaseInvestigation,
57
+ expireDeadInvestigations,
58
+ __resetForTests,
59
+ };
@@ -25,14 +25,15 @@
25
25
 
26
26
  const crypto = require('node:crypto');
27
27
  const fs = require('node:fs');
28
- const os = require('node:os');
29
28
  const path = require('node:path');
29
+ const { sessionSlotsConfigPath } = require('./schedulerPaths.cjs');
30
+ const { appendAuditEvent } = require('./auditLog.cjs');
31
+ const { isProvablyDead, DEFAULT_GRACE_MS } = require('./reservationExpiry.cjs');
30
32
 
31
33
  const MIN_SLOTS = 0;
32
34
  const MAX_SLOTS = 10;
33
35
  const DEFAULT_SLOTS = 5;
34
36
 
35
- const CONFIG_PATH = path.join(os.homedir(), '.claude', 'session-manager', 'session-slots-config.json');
36
37
 
37
38
  function clamp(n) {
38
39
  return Math.min(MAX_SLOTS, Math.max(MIN_SLOTS, n));
@@ -40,7 +41,7 @@ function clamp(n) {
40
41
 
41
42
  function readPersistedCap() {
42
43
  try {
43
- const raw = fs.readFileSync(CONFIG_PATH, 'utf8');
44
+ const raw = fs.readFileSync(sessionSlotsConfigPath(), 'utf8');
44
45
  const parsed = JSON.parse(raw);
45
46
  const cap = Number(parsed.cap);
46
47
  return Number.isFinite(cap) ? clamp(Math.trunc(cap)) : DEFAULT_SLOTS;
@@ -55,10 +56,11 @@ function setCap(cap) {
55
56
  if (!Number.isFinite(n) || Math.trunc(n) !== n || n < MIN_SLOTS || n > MAX_SLOTS) {
56
57
  throw new Error(`sessionSlots.setCap: cap must be an integer in [${MIN_SLOTS}, ${MAX_SLOTS}]`);
57
58
  }
58
- fs.mkdirSync(path.dirname(CONFIG_PATH), { recursive: true });
59
- const tmp = `${CONFIG_PATH}.${process.pid}.${Date.now()}.tmp`;
59
+ const configPath = sessionSlotsConfigPath();
60
+ fs.mkdirSync(path.dirname(configPath), { recursive: true });
61
+ const tmp = `${configPath}.${process.pid}.${Date.now()}.tmp`;
60
62
  fs.writeFileSync(tmp, JSON.stringify({ cap: n }, null, 2));
61
- fs.renameSync(tmp, CONFIG_PATH);
63
+ fs.renameSync(tmp, configPath);
62
64
  for (const fn of listeners) {
63
65
  try { fn(); } catch { /* a consumer's pump error is its own problem */ }
64
66
  }
@@ -73,7 +75,7 @@ function totalSlots() {
73
75
  return readPersistedCap();
74
76
  }
75
77
 
76
- // token → { owner, at }
78
+ // token → { owner, at, claimedAt (ms), pid (null until stampPid) }
77
79
  const holders = new Map();
78
80
 
79
81
  function inUse() {
@@ -89,13 +91,47 @@ function available() {
89
91
  * `owner` is a diagnostic label ("scheduler:<slug>", "chat:<tabId>") shown in
90
92
  * snapshot() so a stuck holder is attributable.
91
93
  */
92
- function acquire(owner) {
94
+ function acquire(owner, { claimedAt = Date.now() } = {}) {
93
95
  if (holders.size >= totalSlots()) return null;
94
96
  const token = crypto.randomUUID();
95
- holders.set(token, { owner: String(owner || 'unknown'), at: new Date().toISOString() });
97
+ holders.set(token, { owner: String(owner || 'unknown'), at: new Date(claimedAt).toISOString(), claimedAt, pid: null });
96
98
  return token;
97
99
  }
98
100
 
101
+ /** stampPid(token, pid) — record the child pid once known, so expireDead can prove death by pid. */
102
+ function stampPid(token, pid) {
103
+ const h = holders.get(token);
104
+ if (!h || !Number.isInteger(pid) || pid <= 0) return false;
105
+ h.pid = pid;
106
+ return true;
107
+ }
108
+
109
+ /**
110
+ * expireDead — release `scheduler:` reservations whose owner is provably dead
111
+ * (see reservationExpiry.isProvablyDead). `chat:` / `project-brief:` tokens are
112
+ * never touched. Accounting only: no process is signalled. Returns the released
113
+ * owners. Each release is audited once (the token is gone after release).
114
+ * @param {{ liveSlugs: Set<string>, pidAlive?: Function, now?: number, graceMs?: number }} opts
115
+ */
116
+ function expireDead({ liveSlugs, pidAlive, now = Date.now(), graceMs = DEFAULT_GRACE_MS } = {}) {
117
+ const live = liveSlugs instanceof Set ? liveSlugs : new Set(liveSlugs || []);
118
+ const expired = [];
119
+ for (const [token, h] of [...holders]) {
120
+ if (!h.owner.startsWith('scheduler:')) continue;
121
+ const slug = h.owner.slice('scheduler:'.length);
122
+ if (!isProvablyDead(h, { live: live.has(slug), pidAlive, now, graceMs })) continue;
123
+ holders.delete(token);
124
+ expired.push(h.owner);
125
+ appendAuditEvent('slot_reservation_expired', { owner: h.owner, pid: h.pid, claimedAt: h.at });
126
+ }
127
+ if (expired.length) {
128
+ for (const fn of listeners) {
129
+ try { fn(); } catch { /* a consumer's pump error is its own problem */ }
130
+ }
131
+ }
132
+ return expired;
133
+ }
134
+
99
135
  // Release listeners: each consumer registers its own "a slot freed — try to
100
136
  // start work" pump so a scheduler release wakes the chat lane and vice versa.
101
137
  // Also fired when the cap itself increases (setCap), for the same reason.
@@ -143,6 +179,8 @@ module.exports = {
143
179
  inUse,
144
180
  available,
145
181
  acquire,
182
+ stampPid,
183
+ expireDead,
146
184
  release,
147
185
  subscribe,
148
186
  snapshot,
@@ -0,0 +1,51 @@
1
+ /**
2
+ * smProcNames.cjs — kernel `comm` names for the processes Session Manager
3
+ * itself leaves in the user's process list (pn-03). `claude` children are
4
+ * named separately (procName.cjs smArgv0 / pn-02).
5
+ *
6
+ * Every name is <= 15 chars (Linux TASK_COMM_LEN). `session-manager` is
7
+ * EXACTLY 15, so NEVER derive another name from it by suffixing — it would
8
+ * truncate back to `session-manager` and be indistinguishable from main.
9
+ */
10
+ 'use strict';
11
+
12
+ const path = require('node:path');
13
+ const { aliasBinFor } = require('./procName.cjs');
14
+
15
+ const PROC_NAMES = Object.freeze({
16
+ main: 'session-manager',
17
+ mcpServer: 'sm-mcp-server',
18
+ watchdog: 'sm-watchdog',
19
+ inhibitHold: 'sm-inhibit-hold',
20
+ shell: 'sm-shell',
21
+ });
22
+
23
+ // Shells whose argv0-basename behaviour (sh/rsh/ksh emulation, $0-branching rc
24
+ // files) is known to be safe under the `sm-shell` alias. Anything else spawns
25
+ // the real path — never guess at another shell's argv0 semantics.
26
+ const ALIASABLE_SHELLS = new Set(['bash', 'zsh']);
27
+
28
+ /**
29
+ * On Linux Node's process.title setter calls prctl(PR_SET_NAME), changing
30
+ * `comm`. It overwrites the argv memory region, but process.argv is a JS
31
+ * array copied at startup and is unaffected. Never throws.
32
+ */
33
+ function setProcessTitle(name) {
34
+ try { process.title = name; } catch { /* naming must never break boot */ }
35
+ }
36
+
37
+ /** Alias `shell` as `sm-shell` iff it is bash/zsh; otherwise return it unchanged. Fail-open. */
38
+ function aliasedShellBin(shell) {
39
+ try {
40
+ if (typeof shell !== 'string' || !shell) return shell;
41
+ if (!ALIASABLE_SHELLS.has(path.basename(shell))) return shell;
42
+ return aliasBinFor(shell, PROC_NAMES.shell);
43
+ } catch { return shell; }
44
+ }
45
+
46
+ /** `sh` aliased as `sm-inhibit-hold` for the systemd-inhibit poll holder. Fail-open. */
47
+ function inhibitHolderShell() {
48
+ try { return aliasBinFor('/bin/sh', PROC_NAMES.inhibitHold); } catch { return '/bin/sh'; }
49
+ }
50
+
51
+ module.exports = { PROC_NAMES, ALIASABLE_SHELLS, setProcessTitle, aliasedShellBin, inhibitHolderShell };