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
@@ -31,14 +31,13 @@
31
31
  const fs = require('node:fs');
32
32
  const fsp = require('node:fs/promises');
33
33
  const path = require('node:path');
34
- const os = require('node:os');
35
34
  const crypto = require('node:crypto');
36
- const { allProjectCwds, activeProjectCwds } = require('../../../scripts/lib/activeSessions.cjs');
35
+ const { allProjectCwds, activeProjectCwds, bustProjectCwdCache } = require('./activeSessions.cjs');
36
+ const { classifyCwd } = require('./cwdClassify.cjs');
37
37
  const { assertOpsWrite, resolveOpsRoot, OPS_ROOT_DIR } = require('./opsOwnership.cjs');
38
38
  const { ScheduleJobSchema } = require('./scheduleJobSchema.cjs');
39
+ const schedulerPaths = require('./schedulerPaths.cjs');
39
40
 
40
- const MACHINE_STATE_PATH = path.join(os.homedir(), '.claude', 'session-manager', 'scheduler-machine.json');
41
- const LEGACY_QUEUE_PATH = path.join(os.homedir(), '.claude', 'session-manager', 'scheduled-plans', 'queue.json');
42
41
  const OPS_DIRNAME = OPS_ROOT_DIR;
43
42
  const STATE_SUBPATH = ['scheduler', 'state'];
44
43
 
@@ -118,7 +117,12 @@ function projectHistoryPath(cwd) {
118
117
  */
119
118
  function queuePathOrSkip(cwd, context) {
120
119
  try {
121
- return projectQueuePath(cwd);
120
+ const file = projectQueuePath(cwd);
121
+ // Unprovable project identity: the row stays dispatchable and is never
122
+ // dropped or emptied — the write itself is refused by assertOpsWrite.
123
+ const { kind, reason } = classifyCwd(cwd);
124
+ if (kind === 'unknown') console.warn(`[queueStore] ${context}: project identity unprovable for "${cwd}" (${reason}) — reads allowed, writes refused`);
125
+ return file;
122
126
  } catch (e) {
123
127
  if (e?.ephemeral) {
124
128
  console.warn(`[queueStore] ${context}: refusing ephemeral cwd "${cwd}" — ${e.message}`);
@@ -190,8 +194,10 @@ async function writeJsonAtomic(file, value) {
190
194
  // list briefly. Correctness fallback: a brand-new project appears at worst
191
195
  // CACHE_MS late, and its first write goes through writeSplit which busts the
192
196
  // cache.
193
- const CACHE_MS = 30_000;
194
- let cwdCache = { at: 0, cwds: [] };
197
+ // Above the 60 s dispatch-loop interval so one pass never re-scans cold; the
198
+ // cached array is frozen (shared, never mutated).
199
+ const CACHE_MS = 120_000;
200
+ let cwdCache = { at: 0, cwds: Object.freeze([]) };
195
201
 
196
202
  function stateCwds(opts) {
197
203
  const now = Date.now();
@@ -205,12 +211,18 @@ function stateCwds(opts) {
205
211
  }
206
212
  // ...and active projects are included even before their first write.
207
213
  for (const cwd of activeProjectCwds(undefined, opts)) add(cwd);
214
+ Object.freeze(cwds);
208
215
  if (!opts) cwdCache = { at: now, cwds };
209
216
  return cwds;
210
217
  }
211
218
 
212
219
  function bustCwdCache() {
213
- cwdCache = { at: 0, cwds: [] };
220
+ cwdCache = { at: 0, cwds: Object.freeze([]) };
221
+ // Without this chain, activeSessions' own scan cache hands stateCwds a
222
+ // stale list even after this bust — turning a caller's "I just wrote a
223
+ // brand-new project's first state file, make it visible now" into a
224
+ // no-op until activeSessions' independent TTL expires.
225
+ bustProjectCwdCache();
214
226
  }
215
227
 
216
228
  // ---------- merged read ----------
@@ -226,6 +238,10 @@ function shapeMachine(data) {
226
238
  // classifyQueueStarvation's header for why the two must never merge.
227
239
  lastDispatchAttemptAt: data.lastDispatchAttemptAt ?? null,
228
240
  paused: data.paused ?? null,
241
+ // Upgrade-drain state (lib/upgradeDrain.cjs): deliberately NOT part of
242
+ // `paused`, so a rate-limit pause can neither overwrite it nor have the
243
+ // manual-clear cooldown swallow it.
244
+ drain: data.drain && typeof data.drain === 'object' ? data.drain : null,
229
245
  // Launch circuit breaker (lib/launchFailure.cjs): per-persona blocks and
230
246
  // the degraded-mode env a persona is currently launching with. Machine
231
247
  // state, not per-project: the broken thing is the installed CLI.
@@ -286,7 +302,6 @@ function findLongestValidJsonPrefix(raw) {
286
302
  // project's own ops root is the natural home for a machine-level log entry,
287
303
  // mirroring scheduler.cjs's own `job.cwd || DEFAULT_PROJECT_CWD` fallback for
288
304
  // cwd-less machine errors (schedulerBatch.cjs's DEFAULT_PROJECT_CWD).
289
- const MACHINE_STATE_LOG_CWD = path.join(os.homedir(), 'Projects', 'session-manager');
290
305
 
291
306
  // The truncated tail in the real 2026-09-11 tear contained an orphaned
292
307
  // `resumeAt` fragment with no way to reconstruct the `paused` object it
@@ -301,7 +316,7 @@ const PAUSED_HINT_RE = /"paused"|resumeAt/;
301
316
  function logMachineStateRecovery({ level, message, meta }) {
302
317
  try {
303
318
  const { appendError } = require('./opsErrorLog.cjs');
304
- appendError({ cwd: MACHINE_STATE_LOG_CWD, scope: 'scheduler', level, message, meta });
319
+ appendError({ cwd: schedulerPaths.machineStateLogCwd(), scope: 'scheduler', level, message, meta });
305
320
  } catch { /* durable logging must never block recovery */ }
306
321
  console.error(`[queueStore] ${message}`);
307
322
  }
@@ -326,7 +341,7 @@ function buildMachineStateRecovery(raw, parseError) {
326
341
  ? '; the discarded tail looks like it contained paused/resumeAt data that could not '
327
342
  + 'be reconstructed — verify pause status manually'
328
343
  : ''),
329
- meta: { path: MACHINE_STATE_PATH, prefixLength: prefix.prefixLength, totalLength: raw.length, pausedHint },
344
+ meta: { path: schedulerPaths.machineStatePath(), prefixLength: prefix.prefixLength, totalLength: raw.length, pausedHint },
330
345
  };
331
346
  }
332
347
  // No valid JSON prefix at all: fall back to defaults (shapeMachine({}) —
@@ -339,7 +354,7 @@ function buildMachineStateRecovery(raw, parseError) {
339
354
  message:
340
355
  `scheduler-machine.json unrecoverable (${parseError?.message}) — falling back to defaults `
341
356
  + 'so dispatch does not silently halt',
342
- meta: { path: MACHINE_STATE_PATH, totalLength: raw.length },
357
+ meta: { path: schedulerPaths.machineStatePath(), totalLength: raw.length },
343
358
  };
344
359
  }
345
360
 
@@ -347,7 +362,7 @@ function recoverTornMachineStateSync(raw, parseError) {
347
362
  const recovery = buildMachineStateRecovery(raw, parseError);
348
363
  logMachineStateRecovery(recovery);
349
364
  try {
350
- writeJsonAtomicSync(MACHINE_STATE_PATH, recovery.value);
365
+ writeJsonAtomicSync(schedulerPaths.machineStatePath(), recovery.value);
351
366
  } catch (e) {
352
367
  console.error(`[queueStore] failed to persist recovered machine state: ${e?.message}`);
353
368
  }
@@ -358,7 +373,7 @@ async function recoverTornMachineState(raw, parseError) {
358
373
  const recovery = buildMachineStateRecovery(raw, parseError);
359
374
  logMachineStateRecovery(recovery);
360
375
  try {
361
- await writeJsonAtomic(MACHINE_STATE_PATH, recovery.value);
376
+ await writeJsonAtomic(schedulerPaths.machineStatePath(), recovery.value);
362
377
  } catch (e) {
363
378
  console.error(`[queueStore] failed to persist recovered machine state: ${e?.message}`);
364
379
  }
@@ -373,10 +388,10 @@ async function recoverTornMachineState(raw, parseError) {
373
388
  function loadMachineStateSync() {
374
389
  let raw;
375
390
  try {
376
- raw = fs.readFileSync(MACHINE_STATE_PATH, 'utf8');
391
+ raw = fs.readFileSync(schedulerPaths.machineStatePath(), 'utf8');
377
392
  } catch (e) {
378
393
  if (e?.code === 'ENOENT') return {};
379
- return { unreadable: `machine state unreadable: ${e?.message}`, unreadablePath: MACHINE_STATE_PATH };
394
+ return { unreadable: `machine state unreadable: ${e?.message}`, unreadablePath: schedulerPaths.machineStatePath() };
380
395
  }
381
396
  try {
382
397
  return { shaped: shapeMachine(JSON.parse(raw)) };
@@ -389,10 +404,10 @@ function loadMachineStateSync() {
389
404
  async function loadMachineState() {
390
405
  let raw;
391
406
  try {
392
- raw = await fsp.readFile(MACHINE_STATE_PATH, 'utf8');
407
+ raw = await fsp.readFile(schedulerPaths.machineStatePath(), 'utf8');
393
408
  } catch (e) {
394
409
  if (e?.code === 'ENOENT') return {};
395
- return { unreadable: `machine state unreadable: ${e?.message}`, unreadablePath: MACHINE_STATE_PATH };
410
+ return { unreadable: `machine state unreadable: ${e?.message}`, unreadablePath: schedulerPaths.machineStatePath() };
396
411
  }
397
412
  try {
398
413
  return { shaped: shapeMachine(JSON.parse(raw)) };
@@ -437,15 +452,17 @@ function shapeJobs(raw, file) {
437
452
 
438
453
  /**
439
454
  * readMergedSync(opts?) → { config, jobs, scheduledFor, lastRunAt, paused,
440
- * unreadable?, unreadablePath?, sourceCwds }.
455
+ * unreadable?, unreadablePath?, unreadableCwds, sourceCwds }.
441
456
  *
442
- * `unreadable` mirrors the old single-file semantics: ANY source file that
443
- * exists but fails to parse halts scheduling (never treat a project's queue
444
- * as empty because it read corrupt). `sourceCwds` records every project file
445
- * consulted so writeSplit can persist "this project now has zero jobs".
457
+ * `unreadable` is reserved for the machine-runtime file alone (a machine-wide
458
+ * halt). A project shard that exists but fails to read/parse is quarantined
459
+ * per cwd in `unreadableCwds: [{cwd, file, error}]` — other projects keep
460
+ * dispatching, and the torn shard is never treated as empty, recovered, or
461
+ * written. `sourceCwds` records every healthy project file consulted so
462
+ * writeSplit can persist "this project now has zero jobs".
446
463
  */
447
464
  function readMergedSync(opts) {
448
- const out = { config: {}, jobs: [], scheduledFor: null, lastRunAt: null, lastDispatchAttemptAt: null, paused: null, launchBlocks: {}, launchMitigations: {}, invalidJobs: [] };
465
+ const out = { config: {}, jobs: [], scheduledFor: null, lastRunAt: null, lastDispatchAttemptAt: null, paused: null, drain: null, launchBlocks: {}, launchMitigations: {}, invalidJobs: [], unreadableCwds: [] };
449
466
  const sourceCwds = [];
450
467
  const machine = loadMachineStateSync();
451
468
  if (machine.shaped) {
@@ -468,8 +485,9 @@ function readMergedSync(opts) {
468
485
  sourceCwds.push(cwd);
469
486
  } catch (e) {
470
487
  if (e?.code === 'ENOENT') { sourceCwds.push(cwd); continue; }
471
- out.unreadable = out.unreadable || `project queue unreadable (${file}): ${e?.message}`;
472
- out.unreadablePath = out.unreadablePath || file;
488
+ // Quarantine this ONE shard; never recover or rewrite it (a job shard is a
489
+ // ledger — a prefix could time-travel a running row back to pending).
490
+ out.unreadableCwds.push({ cwd, file, error: e?.message ?? String(e) });
473
491
  }
474
492
  }
475
493
  defineSources(out, sourceCwds);
@@ -478,7 +496,7 @@ function readMergedSync(opts) {
478
496
 
479
497
  /** Async twin of readMergedSync for IPC hot paths. */
480
498
  async function readMerged(opts) {
481
- const out = { config: {}, jobs: [], scheduledFor: null, lastRunAt: null, lastDispatchAttemptAt: null, paused: null, launchBlocks: {}, launchMitigations: {}, invalidJobs: [] };
499
+ const out = { config: {}, jobs: [], scheduledFor: null, lastRunAt: null, lastDispatchAttemptAt: null, paused: null, drain: null, launchBlocks: {}, launchMitigations: {}, invalidJobs: [], unreadableCwds: [] };
482
500
  const sourceCwds = [];
483
501
  const machine = await loadMachineState();
484
502
  if (machine.shaped) {
@@ -501,8 +519,9 @@ async function readMerged(opts) {
501
519
  sourceCwds.push(cwd);
502
520
  } catch (e) {
503
521
  if (e?.code === 'ENOENT') { sourceCwds.push(cwd); continue; }
504
- out.unreadable = out.unreadable || `project queue unreadable (${file}): ${e?.message}`;
505
- out.unreadablePath = out.unreadablePath || file;
522
+ // Quarantine this ONE shard; never recover or rewrite it (a job shard is a
523
+ // ledger — a prefix could time-travel a running row back to pending).
524
+ out.unreadableCwds.push({ cwd, file, error: e?.message ?? String(e) });
506
525
  }
507
526
  }
508
527
  defineSources(out, sourceCwds);
@@ -526,21 +545,25 @@ function defineSources(state, sourceCwds) {
526
545
  * deletions stick.
527
546
  */
528
547
  async function writeSplit(state, defaultCwd) {
529
- await writeJsonAtomic(MACHINE_STATE_PATH, {
548
+ await writeJsonAtomic(schedulerPaths.machineStatePath(), {
530
549
  config: state.config,
531
550
  scheduledFor: state.scheduledFor ?? null,
532
551
  lastRunAt: state.lastRunAt ?? null,
533
552
  lastDispatchAttemptAt: state.lastDispatchAttemptAt ?? null,
534
553
  paused: state.paused ?? null,
554
+ drain: state.drain ?? null,
535
555
  launchBlocks: state.launchBlocks ?? {},
536
556
  launchMitigations: state.launchMitigations ?? {},
537
557
  });
538
558
 
559
+ // A quarantined shard is left byte-identical: never re-created or clobbered,
560
+ // even if merged rows carry that cwd.
561
+ const quarantined = new Set((state.unreadableCwds ?? []).map((u) => u.cwd));
539
562
  const byCwd = new Map();
540
- for (const cwd of state.sourceCwds ?? []) byCwd.set(cwd, []);
563
+ for (const cwd of state.sourceCwds ?? []) if (!quarantined.has(cwd)) byCwd.set(cwd, []);
541
564
  for (const job of state.jobs ?? []) {
542
565
  const cwd = job.cwd || defaultCwd;
543
- if (!cwd) continue; // nowhere to put it; job is dropped from persistence rather than crashing
566
+ if (!cwd || quarantined.has(cwd)) continue; // nowhere to put it; job is dropped from persistence rather than crashing
544
567
  if (!byCwd.has(cwd)) byCwd.set(cwd, []);
545
568
  byCwd.get(cwd).push(job);
546
569
  }
@@ -571,7 +594,7 @@ async function writeSplit(state, defaultCwd) {
571
594
  async function migrateLegacyGlobalQueue(defaultCwd) {
572
595
  let raw;
573
596
  try {
574
- raw = await fsp.readFile(LEGACY_QUEUE_PATH, 'utf8');
597
+ raw = await fsp.readFile(schedulerPaths.legacyQueuePath(), 'utf8');
575
598
  } catch {
576
599
  return { migrated: false };
577
600
  }
@@ -583,8 +606,8 @@ async function migrateLegacyGlobalQueue(defaultCwd) {
583
606
  return { migrated: false, error: e?.message };
584
607
  }
585
608
 
586
- if (!fs.existsSync(MACHINE_STATE_PATH)) {
587
- await writeJsonAtomic(MACHINE_STATE_PATH, {
609
+ if (!fs.existsSync(schedulerPaths.machineStatePath())) {
610
+ await writeJsonAtomic(schedulerPaths.machineStatePath(), {
588
611
  config: legacy.config || {},
589
612
  scheduledFor: legacy.scheduledFor ?? null,
590
613
  lastRunAt: legacy.lastRunAt ?? null,
@@ -617,14 +640,12 @@ async function migrateLegacyGlobalQueue(defaultCwd) {
617
640
  }
618
641
  }
619
642
 
620
- await fsp.rename(LEGACY_QUEUE_PATH, `${LEGACY_QUEUE_PATH}.retired-${Date.now()}`);
643
+ await fsp.rename(schedulerPaths.legacyQueuePath(), `${schedulerPaths.legacyQueuePath()}.retired-${Date.now()}`);
621
644
  bustCwdCache();
622
645
  return { migrated: true, moved, projects: byCwd.size };
623
646
  }
624
647
 
625
648
  module.exports = {
626
- MACHINE_STATE_PATH,
627
- LEGACY_QUEUE_PATH,
628
649
  STATE_SUBPATH,
629
650
  projectStateDir,
630
651
  projectQueuePath,
@@ -640,3 +661,7 @@ module.exports = {
640
661
  writeJsonAtomicSync,
641
662
  findLongestValidJsonPrefix,
642
663
  };
664
+
665
+ // Lazy path getters (SM_SCHEDULER_HOME resolved at read, never at require).
666
+ Object.defineProperty(module.exports, 'MACHINE_STATE_PATH', { get: schedulerPaths.machineStatePath, enumerable: true });
667
+ Object.defineProperty(module.exports, 'LEGACY_QUEUE_PATH', { get: schedulerPaths.legacyQueuePath, enumerable: true });
@@ -12,17 +12,22 @@
12
12
  */
13
13
  'use strict';
14
14
 
15
+ const { appendAuditEvent } = require('./auditLog.cjs');
16
+ const { isProvablyDead, DEFAULT_GRACE_MS } = require('./reservationExpiry.cjs');
17
+
15
18
  // slug of the job currently holding the lease, or null.
16
19
  let heldBySlug = null;
20
+ let heldClaimedAt = 0;
17
21
 
18
22
  function isHeld() {
19
23
  return heldBySlug !== null;
20
24
  }
21
25
 
22
26
  /** acquire(slug) → true if the lease was free and is now held by `slug`. */
23
- function acquire(slug) {
27
+ function acquire(slug, { claimedAt = Date.now() } = {}) {
24
28
  if (heldBySlug !== null) return false;
25
29
  heldBySlug = String(slug);
30
+ heldClaimedAt = claimedAt;
26
31
  return true;
27
32
  }
28
33
 
@@ -36,6 +41,17 @@ function release(slug) {
36
41
  return true;
37
42
  }
38
43
 
44
+ /** expireDead — free a lease whose holder has no running row and is past the grace window
45
+ * (the lease never carries a pid; the row-liveness + grace proof is the whole test). */
46
+ function expireDead({ liveSlugs, now = Date.now(), graceMs = DEFAULT_GRACE_MS } = {}) {
47
+ if (heldBySlug === null) return false;
48
+ const live = liveSlugs instanceof Set ? liveSlugs : new Set(liveSlugs || []);
49
+ if (!isProvablyDead({ claimedAt: heldClaimedAt, pid: null }, { live: live.has(heldBySlug), now, graceMs })) return false;
50
+ appendAuditEvent('quiet_lease_expired', { slug: heldBySlug });
51
+ heldBySlug = null;
52
+ return true;
53
+ }
54
+
39
55
  function holder() {
40
56
  return heldBySlug;
41
57
  }
@@ -43,6 +59,7 @@ function holder() {
43
59
  /** Test hook: force the lease back to free. */
44
60
  function __resetForTests() {
45
61
  heldBySlug = null;
62
+ heldClaimedAt = 0;
46
63
  }
47
64
 
48
- module.exports = { isHeld, acquire, release, holder, __resetForTests };
65
+ module.exports = { isHeld, acquire, release, expireDead, holder, __resetForTests };
@@ -36,26 +36,37 @@ function claudePidAlive(pid) {
36
36
  }
37
37
 
38
38
  /**
39
- * findLiveProcessForJob(job, { worktreeDir }) → pid | null
39
+ * findLiveProcessForJob(job, { worktreeDir, runCwd }) → pid | null
40
40
  *
41
41
  * Positive liveness scan for a 'running' row whose `runtime.pid` is missing —
42
42
  * the case a missing pid must NOT be read as "the process is gone" (2026-09-06
43
43
  * incident: 234-uranus-eight-tails-ox marked failed/never_ran while PID
44
44
  * 2174739 was a live `claude -p` still writing to that job's own worktree).
45
45
  *
46
- * Linux-`/proc` only. Scans every numeric `/proc/<pid>` entry and matches
47
- * either: `/proc/<pid>/cwd` resolves to `worktreeDir` (or a path nested under
48
- * it), or `/proc/<pid>/cmdline` contains both `claude` and the job's slug
49
- * (fallback for a worktree-disabled/in-place run, where there is no dedicated
50
- * worktreeDir to match against). Returns the first matching pid, or null if
51
- * none is found.
46
+ * Linux-`/proc` only. Scans every numeric `/proc/<pid>` entry and matches any
47
+ * of, in order:
48
+ * - `/proc/<pid>/cwd` resolves to `worktreeDir` (or a path nested under it)
49
+ * — sufficient alone, no cmdline check needed.
50
+ * - `/proc/<pid>/cmdline` contains both `claude` and the job's slug —
51
+ * fallback for a worktree-disabled/in-place run with no dedicated
52
+ * worktreeDir to match against.
53
+ * - `/proc/<pid>/cwd` resolves to `runCwd` (or nested under it) AND
54
+ * `/proc/<pid>/cmdline` contains `claude` (2026-09-13 incident:
55
+ * runId 2026-09-13T16-15-56-818Z ran in-place in the shared tree — no
56
+ * worktree, and dispatch never puts the slug into argv — so neither of
57
+ * the above could ever match a live pid 3138458). `runCwd` is deliberately
58
+ * NEVER matched alone: several concurrent jobs commonly share one tree's
59
+ * cwd, so a bare cwd match without the process-identity check would
60
+ * cross-match the wrong job's process.
61
+ *
62
+ * Returns the first matching pid, or null if none is found.
52
63
  *
53
64
  * Safe fallback by construction: on any platform without `/proc` (macOS,
54
65
  * Windows) `fs.readdirSync('/proc')` throws and this returns null immediately
55
66
  * — i.e. exactly today's behaviour (fail toward terminalizing), never a hang
56
67
  * or a thrown error propagating to the caller.
57
68
  */
58
- function findLiveProcessForJob(job, { worktreeDir } = {}) {
69
+ function findLiveProcessForJob(job, { worktreeDir, runCwd } = {}) {
59
70
  const slug = job?.slug;
60
71
  let entries;
61
72
  try {
@@ -67,27 +78,107 @@ function findLiveProcessForJob(job, { worktreeDir } = {}) {
67
78
  if (!/^\d+$/.test(name)) continue;
68
79
  const pid = Number(name);
69
80
  if (!pid || pid <= 1) continue;
70
- if (worktreeDir) {
81
+
82
+ let cwdLink = null;
83
+ if (worktreeDir || runCwd) {
71
84
  try {
72
- const cwdLink = fs.readlinkSync(`/proc/${pid}/cwd`);
73
- if (cwdLink === worktreeDir || cwdLink.startsWith(worktreeDir + path.sep)) return pid;
85
+ cwdLink = fs.readlinkSync(`/proc/${pid}/cwd`);
74
86
  } catch {
75
87
  // Process exited mid-scan, or permission denied — try the argv
76
88
  // fallback below before giving up on this pid.
77
89
  }
78
90
  }
79
- if (slug) {
91
+
92
+ if (worktreeDir && cwdLink && (cwdLink === worktreeDir || cwdLink.startsWith(worktreeDir + path.sep))) {
93
+ return pid;
94
+ }
95
+
96
+ let cmd = null;
97
+ if (slug || (runCwd && cwdLink)) {
80
98
  try {
81
- const cmd = fs.readFileSync(`/proc/${pid}/cmdline`, 'utf8').replace(/\0/g, ' ');
82
- if (/\bclaude\b/.test(cmd) && cmd.includes(slug)) return pid;
99
+ cmd = fs.readFileSync(`/proc/${pid}/cmdline`, 'utf8').replace(/\0/g, ' ');
83
100
  } catch {
84
101
  // Same as above — process gone or unreadable, keep scanning.
85
102
  }
86
103
  }
104
+
105
+ if (slug && cmd && /\bclaude\b/.test(cmd) && cmd.includes(slug)) return pid;
106
+
107
+ if (runCwd && cwdLink && cmd && /\bclaude\b/.test(cmd) &&
108
+ (cwdLink === runCwd || cwdLink.startsWith(runCwd + path.sep))) {
109
+ return pid;
110
+ }
87
111
  }
88
112
  return null;
89
113
  }
90
114
 
115
+ /**
116
+ * readSpawnedPidFromLog(logPath) → pid | null
117
+ *
118
+ * Recovers the pid a pidless row's spawn actually produced by reading the
119
+ * run's own log — proof of life that pre-dates and is independent of the
120
+ * `runtime.pid` persistence this reap path exists to route around (2026-09-13
121
+ * incident: the mutate that stamps runtime.pid failed silently while the
122
+ * spawn itself, and this exact log line, succeeded). Reads only the first
123
+ * ~8KB — scheduler.cjs's spawnJob writes `[scheduler] spawned pid=<n> ...`
124
+ * synchronously right after fork, always near the top of the log — so this
125
+ * stays cheap against a multi-MB log. Pure IO, never throws: a missing file,
126
+ * an empty file, or a log with no such line all return null.
127
+ */
128
+ function readSpawnedPidFromLog(logPath) {
129
+ if (!logPath) return null;
130
+ let fd;
131
+ try {
132
+ fd = fs.openSync(logPath, 'r');
133
+ const buf = Buffer.alloc(8192);
134
+ const bytesRead = fs.readSync(fd, buf, 0, buf.length, 0);
135
+ const text = buf.toString('utf8', 0, bytesRead);
136
+ const match = text.match(/\[scheduler\] spawned pid=(\d+)/);
137
+ if (!match) return null;
138
+ const pid = Number(match[1]);
139
+ return Number.isFinite(pid) && pid > 1 ? pid : null;
140
+ } catch {
141
+ return null;
142
+ } finally {
143
+ if (fd !== undefined) {
144
+ try { fs.closeSync(fd); } catch { /* already closed / never opened */ }
145
+ }
146
+ }
147
+ }
148
+
149
+ /**
150
+ * readLogMtimeMs(logPath) → number | null
151
+ *
152
+ * Same never-throws IO contract as logHasOutput — returns the run log's
153
+ * last-modified time, or null when it can't be statted (missing file,
154
+ * permission denied).
155
+ */
156
+ function readLogMtimeMs(logPath) {
157
+ if (!logPath) return null;
158
+ try {
159
+ return fs.statSync(logPath).mtimeMs;
160
+ } catch {
161
+ return null;
162
+ }
163
+ }
164
+
165
+ /**
166
+ * isLogFresh(logMtimeMs, now, windowMs) → boolean
167
+ *
168
+ * Pure predicate: true when a run log was modified within `windowMs` of
169
+ * `now`. A log that grew recently is positive, independent proof its process
170
+ * is still alive — stronger evidence than "no runtime.pid recorded", which is
171
+ * only bookkeeping about a side-channel write that can fail on its own (see
172
+ * readSpawnedPidFromLog's header). Returns false for a missing/invalid mtime
173
+ * or window so a caller that couldn't stat the log falls through to the
174
+ * existing reap logic rather than treating "unknown" as "fresh".
175
+ */
176
+ function isLogFresh(logMtimeMs, now, windowMs) {
177
+ if (typeof logMtimeMs !== 'number' || Number.isNaN(logMtimeMs)) return false;
178
+ if (typeof windowMs !== 'number' || windowMs <= 0) return false;
179
+ return (now - logMtimeMs) < windowMs;
180
+ }
181
+
91
182
  /**
92
183
  * True when `logPath` exists and has non-zero content — the literal "did the
93
184
  * run dir produce any log output" check that gates whether a pidless reap may
@@ -224,6 +315,20 @@ const ORPHAN_REQUEUE_CAP = 5;
224
315
  * work (2026-09-06 incident — see findLiveProcessForJob's header). Omitting
225
316
  * `findLiveProcess` (existing callers/tests) preserves prior behaviour
226
317
  * exactly: every pidless row past grace reaps, none are ever recovered.
318
+ *
319
+ * Two more optional injections, checked BEFORE `findLiveProcess`, for a
320
+ * pidless row past grace (2026-09-13 incident — see readSpawnedPidFromLog's
321
+ * and isLogFresh's own headers):
322
+ * - `getLogMtimeMs` (`(job) → number | null`): if the row's run log was
323
+ * modified within `logFreshWindowMs`, the row is skipped entirely — not
324
+ * pushed to `reapable` OR `recovered` — because a growing log is positive
325
+ * proof of life even with no pid evidence at all. One `warnings` entry is
326
+ * still emitted so a permanently-pidless-but-chatty row stays visible.
327
+ * - `getLogPid` (`(job) → pid | null`): reads the run's own log for the pid
328
+ * its spawn actually produced. If `pidAlive(pid)` is true, the row is
329
+ * diverted into `recovered` exactly like a `findLiveProcess` hit.
330
+ * Both default to a no-op when omitted, so existing callers/tests are
331
+ * unaffected.
227
332
  */
228
333
  /**
229
334
  * Pure, never-throws formatter for the dispatch-phase breadcrumb appended to
@@ -246,7 +351,47 @@ function formatDispatchPhaseSuffix(j) {
246
351
  : ` (last dispatch phase: ${j.dispatchPhase})`;
247
352
  }
248
353
 
249
- function selectReapableJobs(jobs, now, { pidAlive, grace, findLiveProcess } = {}) {
354
+ /**
355
+ * resolvePidlessFailureOverride(job) → { verdict, landedCommit, reason } | null
356
+ *
357
+ * The evidence-before-failure guard this PRD adds (1173): a pidless reap
358
+ * about to stamp 'failed' purely because `runtime.pid` was never recorded
359
+ * must first check whether THIS ROW already carries a `landedCommit` — proof
360
+ * (from an earlier dispatch of the same slug, since `landedCommit` is
361
+ * deliberately never cleared by a reset — see scheduler.cjs's comment near
362
+ * resetJobFields) that a real commit landed on the branch. Returns null when
363
+ * there is no such evidence, leaving the existing 'failed' transition for a
364
+ * genuinely-never-spawned row completely untouched.
365
+ *
366
+ * DESTINATION CHOICE — needs_review, never completed. `landedCommit` is
367
+ * real git evidence (it is only ever stamped from an actual HEAD advance or
368
+ * a proven branch-integration — see jobLandedCommitThisRun / the dead-pid
369
+ * reap's own isBranchAlreadyIntegrated+computeCommittedDuringRun proof in
370
+ * scheduler.cjs), so it is never speculative in the sense of "guessed before
371
+ * the work ran". But the pidless-reap path performs NO fresh integration
372
+ * check of its own for the CURRENT run (that proof only runs for the dead-pid
373
+ * branch's `dead` entries with outcome==='success' — see
374
+ * scheduler.cjs's integrationResults loop) — so at this call site there is no
375
+ * re-verified guarantee that the recorded commit corresponds to *this*
376
+ * dispatch rather than surviving, unreaped, from an earlier one. Real
377
+ * evidence that under-specifies correspondence to the current run is exactly
378
+ * the needs_review shape, not completed: a human (or the finish-protocol
379
+ * commit-guard on the next dispatch) can resolve it conclusively with
380
+ * `git show <sha>`, but this pure predicate must not guess.
381
+ */
382
+ function resolvePidlessFailureOverride(job) {
383
+ const landedCommit = job?.landedCommit || null;
384
+ if (!landedCommit) return null;
385
+ return {
386
+ verdict: 'pidless_reap_with_landed_commit',
387
+ landedCommit,
388
+ reason: `pidless reap found no runtime.pid, but this row already carries landedCommit ${landedCommit} — routing to needs_review instead of failed; verify with \`git show ${landedCommit}\``,
389
+ };
390
+ }
391
+
392
+ function selectReapableJobs(jobs, now, {
393
+ pidAlive, grace, findLiveProcess, getLogMtimeMs, getLogPid, logFreshWindowMs,
394
+ } = {}) {
250
395
  const reapable = [];
251
396
  const warnings = [];
252
397
  const recovered = [];
@@ -265,6 +410,22 @@ function selectReapableJobs(jobs, now, { pidAlive, grace, findLiveProcess } = {}
265
410
  }
266
411
  const ageMs = now - startedAt;
267
412
  if (ageMs < grace) continue; // spawn may still be mid-flight
413
+
414
+ const logMtimeMs = typeof getLogMtimeMs === 'function' ? getLogMtimeMs(j) : null;
415
+ if (isLogFresh(logMtimeMs, now, logFreshWindowMs)) {
416
+ warnings.push({
417
+ slug: j.slug,
418
+ reason: `pidless row skipped — run log modified ${Math.round((now - logMtimeMs) / 1000)}s ago, still alive by log evidence`,
419
+ });
420
+ continue; // neither reapable nor recovered — the log proves it's alive
421
+ }
422
+
423
+ const logPid = typeof getLogPid === 'function' ? getLogPid(j) : null;
424
+ if (logPid && pidAlive(logPid)) {
425
+ recovered.push({ slug: j.slug, pid: logPid });
426
+ continue;
427
+ }
428
+
268
429
  const livePid = typeof findLiveProcess === 'function' ? findLiveProcess(j) : null;
269
430
  if (livePid) {
270
431
  recovered.push({ slug: j.slug, pid: livePid });
@@ -275,6 +436,7 @@ function selectReapableJobs(jobs, now, { pidAlive, grace, findLiveProcess } = {}
275
436
  pid: null,
276
437
  pidless: true,
277
438
  reason: `reaped: no runtime.pid recorded after ${Math.round(grace / 60_000)}m — spawn never completed${formatDispatchPhaseSuffix(j)}`,
439
+ failureOverride: resolvePidlessFailureOverride(j),
278
440
  });
279
441
  }
280
442
  return { reapable, warnings, recovered };
@@ -343,7 +505,11 @@ module.exports = {
343
505
  findLiveProcessForJob,
344
506
  logHasOutput,
345
507
  resolvePidlessGateOutcome,
508
+ resolvePidlessFailureOverride,
346
509
  isAlreadySatisfiedOnMain,
347
510
  resolveCommitGuardOutcome,
348
511
  formatDispatchPhaseSuffix,
512
+ readSpawnedPidFromLog,
513
+ readLogMtimeMs,
514
+ isLogFresh,
349
515
  };
@@ -0,0 +1,30 @@
1
+ /**
2
+ * reservationExpiry.cjs — the single fail-CLOSED "is this reservation's owner
3
+ * provably dead?" predicate shared by sessionSlots, quietMachineLease, the
4
+ * investigation set and gitWorktree's accounting. A reservation is released
5
+ * only on proof: no live row for its slug AND (a stamped pid that is dead, or
6
+ * no pid ever stamped and claimedAt older than graceMs). Anything ambiguous
7
+ * (live row, live pid, inside the grace window, claimed after the pass began)
8
+ * is kept. Never signals a process.
9
+ */
10
+ 'use strict';
11
+
12
+ const DEFAULT_GRACE_MS = 10 * 60 * 1000;
13
+
14
+ function defaultPidAlive(pid) {
15
+ try { process.kill(pid, 0); return true; } catch (e) { return e?.code === 'EPERM'; }
16
+ }
17
+
18
+ /**
19
+ * @param {{ claimedAt: number, pid: number|null }} res
20
+ * @param {{ live: boolean, pidAlive?: (pid:number)=>boolean, now: number, graceMs?: number }} ctx
21
+ */
22
+ function isProvablyDead(res, { live, pidAlive = defaultPidAlive, now, graceMs = DEFAULT_GRACE_MS }) {
23
+ if (live) return false;
24
+ // Made after the pass began (i.e. this tick) — never expire it.
25
+ if (!Number.isFinite(res.claimedAt) || res.claimedAt > now) return false;
26
+ if (Number.isInteger(res.pid) && res.pid > 0) return !pidAlive(res.pid);
27
+ return now - res.claimedAt > graceMs;
28
+ }
29
+
30
+ module.exports = { isProvablyDead, defaultPidAlive, DEFAULT_GRACE_MS };
@@ -15,7 +15,8 @@
15
15
  */
16
16
 
17
17
  const { spawn } = require('node:child_process');
18
- const { resolveClaudeBin } = require('./claudeBin.cjs');
18
+ const { resolveClaudeBin, claudeSpawnTarget } = require('./claudeBin.cjs');
19
+ const { withProcRole } = require('./cleanEnv.cjs');
19
20
 
20
21
  const MAX_OUT_BYTES = 8 * 1024 * 1024;
21
22
 
@@ -31,7 +32,8 @@ function runClaudeP(prompt, { model = 'sonnet', timeoutMs = 180_000, systemPromp
31
32
  '--output-format', 'text',
32
33
  ];
33
34
  if (systemPrompt) args.push('--append-system-prompt', systemPrompt);
34
- const child = spawn(bin, args, { env: { ...process.env, SM_KG_INTERNAL: '1' }, stdio: ['ignore', 'pipe', 'pipe'] });
35
+ const target = claudeSpawnTarget('aux', 'runclaudep', bin);
36
+ const child = spawn(target.command, args, { env: withProcRole({ ...process.env, SM_KG_INTERNAL: '1' }, 'aux'), stdio: ['ignore', 'pipe', 'pipe'], ...(target.argv0 ? { argv0: target.argv0 } : {}) });
35
37
  let out = '';
36
38
  let err = '';
37
39
  let killedForSize = false;