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
@@ -1,17 +1,49 @@
1
1
  // PRD 834 — machine-wide scheduler-ownership lock. Secondary instances must
2
2
  // go scheduler-passive instead of running boot reconciliation / admin server
3
3
  // against the owner's queue state (live incident 2026-07-31).
4
- import { describe, it, expect, beforeEach, afterEach } from 'vitest';
4
+ //
5
+ // HOME is stubbed to a mkdtemp dir BEFORE requiring instanceLock.cjs (mirrors
6
+ // scheduler-reconcile-invalid-repair.test.cjs): instanceLock.cjs pulls in
7
+ // auditLog.cjs, whose audit log path resolves from
8
+ // os.homedir() — so real ~/.claude/session-manager state must never be
9
+ // touched by this suite.
10
+ import { describe, it, expect, beforeAll, afterAll, beforeEach, afterEach } from 'vitest';
5
11
  import fs from 'node:fs';
6
12
  import os from 'node:os';
7
13
  import path from 'node:path';
8
14
 
9
- const {
10
- acquireSchedulerOwnership,
11
- releaseSchedulerOwnership,
12
- pidAlive,
13
- lockPath,
14
- } = require('../instanceLock.cjs');
15
+ let originalHome;
16
+ let tmpHome;
17
+ let instanceLock;
18
+ let auditLogPath;
19
+
20
+ beforeAll(() => {
21
+ originalHome = process.env.HOME;
22
+ tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), 'sm-instance-lock-home-'));
23
+ process.env.HOME = tmpHome;
24
+ instanceLock = require('../instanceLock.cjs');
25
+ ({ auditLogPath } = require('../auditLog.cjs'));
26
+ if (!auditLogPath().startsWith(tmpHome)) {
27
+ throw new Error(`refusing to run: auditLogPath() (${auditLogPath()}) is not under the temp HOME (${tmpHome})`);
28
+ }
29
+ });
30
+
31
+ afterAll(() => {
32
+ process.env.HOME = originalHome;
33
+ fs.rmSync(tmpHome, { recursive: true, force: true });
34
+ });
35
+
36
+ // Thin pass-throughs so every test body below can keep calling these as
37
+ // bare names, resolved lazily against the module loaded in beforeAll.
38
+ const acquireSchedulerOwnership = (...args) => instanceLock.acquireSchedulerOwnership(...args);
39
+ const releaseSchedulerOwnership = (...args) => instanceLock.releaseSchedulerOwnership(...args);
40
+ const pidAlive = (...args) => instanceLock.pidAlive(...args);
41
+ const lockPath = (...args) => instanceLock.lockPath(...args);
42
+
43
+ function readAuditEvents() {
44
+ if (!fs.existsSync(auditLogPath())) return [];
45
+ return fs.readFileSync(auditLogPath(), 'utf8').trim().split('\n').filter(Boolean).map((l) => JSON.parse(l));
46
+ }
15
47
 
16
48
  let dir;
17
49
 
@@ -22,7 +54,9 @@ beforeEach(() => {
22
54
 
23
55
  afterEach(() => {
24
56
  delete process.env.SM_SCHEDULER_LOCK_PATH;
57
+ delete process.env.SM_PROC_ROOT;
25
58
  fs.rmSync(dir, { recursive: true, force: true });
59
+ if (fs.existsSync(auditLogPath())) fs.rmSync(auditLogPath(), { force: true });
26
60
  });
27
61
 
28
62
  describe('instanceLock (PRD 834)', () => {
@@ -84,4 +118,56 @@ describe('instanceLock (PRD 834)', () => {
84
118
  expect(pidAlive(-5)).toBe(false);
85
119
  expect(pidAlive(0)).toBe(false);
86
120
  });
121
+
122
+ it('a legacy lock ({pid, startedAt} only, no identity) is respected while its pid is alive', () => {
123
+ // Mirrors the on-disk shape of every lock written before this PRD.
124
+ fs.mkdirSync(path.dirname(lockPath()), { recursive: true });
125
+ fs.writeFileSync(lockPath(), JSON.stringify({ pid: 1, startedAt: 'x' }));
126
+ const res = acquireSchedulerOwnership();
127
+ expect(res.owner).toBe(false);
128
+ expect(res.holderPid).toBe(1);
129
+ expect(JSON.parse(fs.readFileSync(lockPath(), 'utf8')).pid).toBe(1);
130
+ // No identity information to compare, so no stale-lock-broken audit event.
131
+ expect(readAuditEvents().some((e) => e.kind === 'stale_lock_broken')).toBe(false);
132
+ });
133
+
134
+ it('a recorded identity that no longer matches the live process at that (recycled) pid is broken, and audited', () => {
135
+ // process.ppid is guaranteed alive for the lifetime of this test (the
136
+ // vitest worker's own parent) — stand-in for "a hard crash left a lock
137
+ // whose pid was later reused by an unrelated live process".
138
+ const recycledPid = process.ppid;
139
+ const procRoot = path.join(dir, 'proc');
140
+ fs.mkdirSync(path.join(procRoot, String(recycledPid)), { recursive: true });
141
+ fs.writeFileSync(
142
+ path.join(procRoot, String(recycledPid), 'stat'),
143
+ `${recycledPid} (bash) S 1 ${recycledPid} ${recycledPid} 0 -1 4194304 0 0 0 0 0 0 0 0 20 0 1 0 222222 0 0 0 0 0 0 0 0 0 0 0 0 0 17 0 0 0 0 0 0 0 0 0 0 0 0\n`,
144
+ );
145
+ fs.writeFileSync(path.join(procRoot, String(recycledPid), 'cmdline'), 'bash\0');
146
+ process.env.SM_PROC_ROOT = procRoot;
147
+
148
+ fs.mkdirSync(path.dirname(lockPath()), { recursive: true });
149
+ fs.writeFileSync(lockPath(), JSON.stringify({
150
+ pid: recycledPid,
151
+ startedAt: 'x',
152
+ // The identity the crashed scheduler instance actually recorded — a
153
+ // different cmdline/startTicks than the process now holding this pid.
154
+ identity: { pid: recycledPid, startTicks: 111111, cmdline: 'claude --session-id old', complete: true },
155
+ }));
156
+
157
+ const res = acquireSchedulerOwnership();
158
+ expect(res.owner).toBe(true);
159
+ expect(JSON.parse(fs.readFileSync(lockPath(), 'utf8')).pid).toBe(process.pid);
160
+
161
+ const auditEvents = readAuditEvents().filter((e) => e.kind === 'stale_lock_broken');
162
+ expect(auditEvents.length).toBe(1);
163
+ expect(auditEvents[0].recordedPid).toBe(recycledPid);
164
+ });
165
+
166
+ it('writeLockExclusive (via acquire) records a complete identity for this process', () => {
167
+ acquireSchedulerOwnership();
168
+ const onDisk = JSON.parse(fs.readFileSync(lockPath(), 'utf8'));
169
+ expect(onDisk.identity).toBeTruthy();
170
+ expect(onDisk.identity.pid).toBe(process.pid);
171
+ expect(onDisk.identity.complete).toBe(true);
172
+ });
87
173
  });
@@ -0,0 +1,78 @@
1
+ /**
2
+ * jobSupervisorRecord.test.cjs — record round-trip, live listing, retention
3
+ * claim, and the pure classifyAdoption matrix.
4
+ *
5
+ * Run: timeout 120 npx vitest run src/main/lib/__tests__/jobSupervisorRecord.test.cjs
6
+ */
7
+ 'use strict';
8
+
9
+ import { test, expect, beforeEach, afterEach } from 'vitest';
10
+ const fs = require('node:fs');
11
+ const os = require('node:os');
12
+ const path = require('node:path');
13
+ const rec = require('../jobSupervisorRecord.cjs');
14
+
15
+ let root;
16
+ beforeEach(() => { root = fs.mkdtempSync(path.join(os.tmpdir(), 'sm-supervisor-')); });
17
+ afterEach(() => { fs.rmSync(root, { recursive: true, force: true }); });
18
+
19
+ const base = (over = {}) => ({
20
+ runDir: path.join(root, 'run-1'), slug: 'p1', cwd: '/x', runId: 'run-1', pid: 42, pgid: 42,
21
+ identity: { pid: 42, startTicks: 7, cmdline: 'claude -p', complete: true },
22
+ execCwd: '/wt', worktreeDir: '/wt', worktreeBranch: 'sm-job/p1', sessionId: 's',
23
+ startedAt: 1_000, budgetMs: 10_000, maxDurationMs: null, idleKillMs: 5_000, schedulerPid: 1, codeSha: 'abc', ...over,
24
+ });
25
+
26
+ test('write → read round-trips every field at <runDir>/<slug>.supervisor.json', () => {
27
+ const input = base();
28
+ rec.writeSupervisorRecord(input);
29
+ expect(fs.existsSync(path.join(input.runDir, 'p1.supervisor.json'))).toBe(true);
30
+ const back = rec.readSupervisorRecord(input.runDir, 'p1');
31
+ const { runDir, ...persisted } = input;
32
+ expect(back).toMatchObject({ ...persisted, kind: 'job' });
33
+ expect(back.runDir).toBeUndefined();
34
+ });
35
+
36
+ test('readSupervisorRecord returns null for absent or torn files', () => {
37
+ expect(rec.readSupervisorRecord(path.join(root, 'nope'), 'p1')).toBeNull();
38
+ fs.mkdirSync(path.join(root, 'r'));
39
+ fs.writeFileSync(path.join(root, 'r', 'p1.supervisor.json'), '{"pid":');
40
+ expect(rec.readSupervisorRecord(path.join(root, 'r'), 'p1')).toBeNull();
41
+ });
42
+
43
+ test('listLiveSupervisorRecords skips exited (meta.json) and stale records', () => {
44
+ rec.writeSupervisorRecord(base({ slug: 'live' }));
45
+ rec.writeSupervisorRecord(base({ slug: 'done' }));
46
+ fs.writeFileSync(path.join(root, 'run-1', 'done.meta.json'), '{}');
47
+ const live = rec.listLiveSupervisorRecords(root, { maxAgeMs: 60_000 });
48
+ expect(live.map((r) => r.slug)).toEqual(['live']);
49
+ expect(live[0].runDir).toBe(path.join(root, 'run-1'));
50
+ expect(rec.listLiveSupervisorRecords(root, { maxAgeMs: 1, now: Date.now() + 10_000 })).toEqual([]);
51
+ expect(rec.listLiveSupervisorRecords(path.join(root, 'missing'), { maxAgeMs: 1 })).toEqual([]);
52
+ });
53
+
54
+ test('investigation records use the .investigation.log exit line as their marker', () => {
55
+ const runDir = path.join(root, 'run-1');
56
+ rec.writeSupervisorRecord(base({ slug: 'p1.investigation', kind: 'investigation' }));
57
+ fs.writeFileSync(path.join(runDir, 'p1.investigation.log'), 'working\n');
58
+ expect(rec.listLiveSupervisorRecords(root, { maxAgeMs: 60_000 })).toHaveLength(1);
59
+ fs.appendFileSync(path.join(runDir, 'p1.investigation.log'), '\n[scheduler] investigation exit code=0\n');
60
+ expect(rec.listLiveSupervisorRecords(root, { maxAgeMs: 60_000 })).toEqual([]);
61
+ });
62
+
63
+ test('classifyAdoption matrix', () => {
64
+ const r = base();
65
+ const same = { pid: 42, startTicks: 7, cmdline: 'claude -p', complete: true };
66
+ const other = { pid: 42, startTicks: 99, cmdline: 'claude -p', complete: true };
67
+ const now = 2_000;
68
+ const c = (o) => rec.classifyAdoption(r, { identity: same, pidAlive: true, logMtimeMs: now, exitMarker: false, now, ...o });
69
+ expect(c({})).toBe('adopt');
70
+ expect(c({ pidAlive: false })).toBe('dead');
71
+ expect(c({ pidAlive: false, exitMarker: true })).toBe('exited');
72
+ expect(c({ identity: other })).toBe('foreign-pid');
73
+ expect(c({ identity: { pid: 42, startTicks: null, cmdline: null, complete: false } })).toBe('adopt'); // fail-closed
74
+ expect(c({ now: 1_000 + 10_001, logMtimeMs: 1_000 + 10_000 })).toBe('over-budget');
75
+ expect(c({ now: 1_000 + 5_001, logMtimeMs: 1_000 })).toBe('over-budget'); // log stalled past idleKillMs
76
+ expect(rec.classifyAdoption(base({ budgetMs: null, maxDurationMs: 100 }), { identity: same, pidAlive: true, logMtimeMs: 1_050, now: 1_200 })).toBe('over-budget');
77
+ expect(rec.classifyAdoption(base({ budgetMs: null, idleKillMs: null }), { identity: same, pidAlive: true, now: 9e9 })).toBe('adopt');
78
+ });
@@ -69,3 +69,14 @@ test('a precomputed cwdHolders set is forwarded through to hasLiveHolder unchang
69
69
  isLive('job-d', { worktree: '/tmp/checkout-d' });
70
70
  expect(hasLiveHolder).toHaveBeenCalledWith('/tmp/checkout-d', holders);
71
71
  });
72
+
73
+ test('rowPid (the record/log ladder) is consulted instead of runtime.pid alone', () => {
74
+ const claudePidAlive = vi.fn((pid) => pid === 777);
75
+ const isLive = buildJobWorktreeIsLive({
76
+ bootJobs: [{ slug: 'job-e', status: 'running' }],
77
+ claudePidAlive,
78
+ hasLiveHolder: () => false,
79
+ rowPid: () => 777,
80
+ });
81
+ expect(isLive('job-e', { worktree: '/tmp/checkout-e' })).toBe(true);
82
+ });
@@ -173,7 +173,7 @@ test('with neither SM_DEV nor SM_E2E set, resolves to the original admin-api.jso
173
173
  delete process.env.SM_E2E;
174
174
  delete process.env.SM_ADMIN_TOKEN_PATH;
175
175
  try {
176
- expect(resolveTokenPath()).toBe(TOKEN_PATH);
176
+ expect(resolveTokenPath()).toBe(path.join(require('../schedulerPaths.cjs').schedulerHome(), 'admin-api.json'));
177
177
  } finally {
178
178
  if (prevDev === undefined) delete process.env.SM_DEV; else process.env.SM_DEV = prevDev;
179
179
  if (prevE2e === undefined) delete process.env.SM_E2E; else process.env.SM_E2E = prevE2e;
@@ -78,12 +78,17 @@ test.each(MCP_TOOL_CATALOG)('$name composeDescription joins purpose/whenToUse/wh
78
78
  expect(composeDescription(entry).length).toBeGreaterThan(0);
79
79
  });
80
80
 
81
+ test('project-home group is exactly the single project_home_write tool', () => {
82
+ expect(MCP_TOOL_CATALOG.filter((e) => e.group === 'project-home').map((e) => e.name)).toEqual(['project_home_write']);
83
+ });
84
+
81
85
  test('MCP_RECIPES covers queue-work, unstick-needs-review, and hand-off-to-another-project', () => {
82
86
  const ids = MCP_RECIPES.map((r) => r.id);
83
87
  expect(ids).toContain('queue-work-via-develop');
84
88
  expect(ids).toContain('unstick-needs-review-job');
85
89
  expect(ids).toContain('hand-finding-to-another-project');
86
- expect(ids).toContain('generate-project-home');
90
+ // the single-tool project_home_write needs no recipe; the old 4-step one is gone
91
+ expect(ids).not.toContain('generate-project-home');
87
92
  for (const recipe of MCP_RECIPES) {
88
93
  expect(recipe.title).toBeTruthy();
89
94
  expect(Array.isArray(recipe.steps)).toBe(true);
@@ -29,7 +29,7 @@ const fsp = require('node:fs/promises');
29
29
  const os = require('node:os');
30
30
  const path = require('node:path');
31
31
  const queueStore = require('../queueStore.cjs');
32
- const { activeProjectCwds } = require('../../../../scripts/lib/activeSessions.cjs');
32
+ const { activeProjectCwds } = require('../activeSessions.cjs');
33
33
 
34
34
  const tmpDirs = [];
35
35
 
@@ -149,7 +149,7 @@ test('a transcript cwd inside an ops root is normalized to the project root, not
149
149
  });
150
150
 
151
151
  test('normalization keeps the active-project signal rather than dropping the row', () => {
152
- const { projectRootOf } = require('../../../../scripts/lib/activeSessions.cjs');
152
+ const { projectRootOf } = require('../activeSessions.cjs');
153
153
  expect(projectRootOf('/p/session-manager-operations/prompt-sessions')).toBe('/p');
154
154
  expect(projectRootOf('/p/session-manager-operations')).toBe('/p');
155
155
  expect(projectRootOf('/p/session-manager-operations/scheduler/state')).toBe('/p');
@@ -185,7 +185,7 @@ test('projectStateDir fails closed on an ops-internal cwd, naming the offending
185
185
  // list at all. `worktreeMainRootOf` reverses a linked worktree's `.git` FILE
186
186
  // (`gitdir: <main>/.git/worktrees/<name>`) back to `<main>`.
187
187
 
188
- const { worktreeMainRootOf } = require('../../../../scripts/lib/activeSessions.cjs');
188
+ const { worktreeMainRootOf } = require('../activeSessions.cjs');
189
189
  const { KIND_CONFIG: WORKTREE_KIND_CONFIG } = require('../gitWorktree.cjs');
190
190
 
191
191
  // A worktree's MAIN tree root must live outside os.tmpdir() for these fixtures
@@ -285,13 +285,13 @@ test('worktreeMainRootOf returns null for a plain non-git directory', async () =
285
285
 
286
286
  test('projectRootOf resolves a worktree cwd to the main tree root', async () => {
287
287
  const { main, worktree } = await makeWorktreeFixture();
288
- const { projectRootOf } = require('../../../../scripts/lib/activeSessions.cjs');
288
+ const { projectRootOf } = require('../activeSessions.cjs');
289
289
  expect(projectRootOf(worktree)).toBe(main);
290
290
  });
291
291
 
292
292
  test('projectRootOf resolves a cwd nested inside a worktree\'s OWN ops tree to the main root', async () => {
293
293
  const { main, worktree } = await makeWorktreeFixture();
294
- const { projectRootOf } = require('../../../../scripts/lib/activeSessions.cjs');
294
+ const { projectRootOf } = require('../activeSessions.cjs');
295
295
  const nestedOps = path.join(worktree, 'session-manager-operations', 'scheduler', 'state');
296
296
  fs.mkdirSync(nestedOps, { recursive: true });
297
297
  expect(projectRootOf(nestedOps)).toBe(main);
@@ -0,0 +1,224 @@
1
+ /**
2
+ * prdDisposition.test.cjs — pure logic behind the scheduler wave-disposition
3
+ * PRD: does a new PRD wave extend an Epic's existing dependsOn chain
4
+ * ('append', gaining a dependsOn on the chain's current terminal PRD(s)) or
5
+ * start an independent, parallel-eligible root ('new-head')? And, after the
6
+ * fact, can a human safely rewrite that choice from the Scheduler UI?
7
+ *
8
+ * Run: timeout 120 npx vitest run src/main/lib/__tests__/prdDisposition.test.cjs
9
+ */
10
+
11
+ 'use strict';
12
+
13
+ import { test, expect } from 'vitest';
14
+
15
+ const {
16
+ isIncomplete,
17
+ resolveChainTerminals,
18
+ wouldCreateCycle,
19
+ computeDispositionRewrite,
20
+ } = require('../prdDisposition.cjs');
21
+
22
+ function row(overrides) {
23
+ return { slug: 'x', status: 'pending', dependsOn: [], ...overrides };
24
+ }
25
+
26
+ // ─────────────────────────────────────────── isIncomplete
27
+
28
+ test('isIncomplete: completed is the only complete status', () => {
29
+ expect(isIncomplete('completed')).toBe(false);
30
+ for (const status of [null, undefined, 'pending', 'running', 'needs_review', 'failed', 'quarantined']) {
31
+ expect(isIncomplete(status)).toBe(true);
32
+ }
33
+ });
34
+
35
+ // ─────────────────────────────────────────── resolveChainTerminals
36
+
37
+ test('resolveChainTerminals: a lone root is its own terminal', () => {
38
+ const rows = [row({ slug: 'a' })];
39
+ expect(resolveChainTerminals(rows)).toEqual(['a']);
40
+ });
41
+
42
+ test('resolveChainTerminals: a linear chain terminates at its tail', () => {
43
+ const rows = [
44
+ row({ slug: 'a' }),
45
+ row({ slug: 'b', dependsOn: ['a'] }),
46
+ row({ slug: 'c', dependsOn: ['b'] }),
47
+ ];
48
+ expect(resolveChainTerminals(rows)).toEqual(['c']);
49
+ });
50
+
51
+ test('resolveChainTerminals: a fork yields both leaves', () => {
52
+ const rows = [
53
+ row({ slug: 'a' }),
54
+ row({ slug: 'b', dependsOn: ['a'] }),
55
+ row({ slug: 'c', dependsOn: ['a'] }),
56
+ ];
57
+ expect(resolveChainTerminals(rows).sort()).toEqual(['b', 'c']);
58
+ });
59
+
60
+ test('resolveChainTerminals: two independent roots are both terminals (existing multi-head Epic)', () => {
61
+ const rows = [row({ slug: 'a' }), row({ slug: 'z' })];
62
+ expect(resolveChainTerminals(rows).sort()).toEqual(['a', 'z']);
63
+ });
64
+
65
+ test('resolveChainTerminals: a dependsOn cycle contributes no terminal', () => {
66
+ const rows = [
67
+ row({ slug: 'a', dependsOn: ['b'] }),
68
+ row({ slug: 'b', dependsOn: ['a'] }),
69
+ ];
70
+ expect(resolveChainTerminals(rows)).toEqual([]);
71
+ });
72
+
73
+ test('resolveChainTerminals: empty input yields no terminals', () => {
74
+ expect(resolveChainTerminals([])).toEqual([]);
75
+ });
76
+
77
+ // ─────────────────────────────────────────── wouldCreateCycle
78
+
79
+ test('wouldCreateCycle: false for a fresh, unrelated edge', () => {
80
+ const rows = [row({ slug: 'a' }), row({ slug: 'b' })];
81
+ expect(wouldCreateCycle(rows, 'b', ['a'])).toBe(false);
82
+ });
83
+
84
+ test('wouldCreateCycle: true when the target already (transitively) depends on the source', () => {
85
+ // a depends on b; proposing b depends on a closes the loop.
86
+ const rows = [
87
+ row({ slug: 'a', dependsOn: ['b'] }),
88
+ row({ slug: 'b' }),
89
+ ];
90
+ expect(wouldCreateCycle(rows, 'b', ['a'])).toBe(true);
91
+ });
92
+
93
+ test('wouldCreateCycle: true for a direct self-reference', () => {
94
+ const rows = [row({ slug: 'a' })];
95
+ expect(wouldCreateCycle(rows, 'a', ['a'])).toBe(true);
96
+ });
97
+
98
+ test('wouldCreateCycle: false through an unrelated third row', () => {
99
+ const rows = [
100
+ row({ slug: 'a' }),
101
+ row({ slug: 'b', dependsOn: ['a'] }),
102
+ row({ slug: 'c' }),
103
+ ];
104
+ expect(wouldCreateCycle(rows, 'c', ['b'])).toBe(false);
105
+ });
106
+
107
+ // ─────────────────────────────────────────── computeDispositionRewrite
108
+
109
+ test('append sets the expected edges', () => {
110
+ const rows = [
111
+ row({ slug: 'wave1-tail' }),
112
+ row({ slug: 'wave2-root' }),
113
+ ];
114
+ const result = computeDispositionRewrite({
115
+ slug: 'wave2-root',
116
+ disposition: 'append',
117
+ dependsOn: ['wave1-tail'],
118
+ rows,
119
+ });
120
+ expect(result).toEqual({ ok: true, dependsOn: ['wave1-tail'] });
121
+ });
122
+
123
+ test('new-head sets none', () => {
124
+ const rows = [
125
+ row({ slug: 'wave1-tail' }),
126
+ row({ slug: 'wave2-root', dependsOn: ['wave1-tail'] }),
127
+ ];
128
+ const result = computeDispositionRewrite({
129
+ slug: 'wave2-root',
130
+ disposition: 'new-head',
131
+ dependsOn: [],
132
+ rows,
133
+ });
134
+ expect(result).toEqual({ ok: true, dependsOn: [] });
135
+ });
136
+
137
+ test('promoting a wave to a head removes exactly its root edges', () => {
138
+ const rows = [
139
+ row({ slug: 'wave1-tail' }),
140
+ row({ slug: 'other-head' }),
141
+ row({ slug: 'wave2-root', dependsOn: ['wave1-tail'] }),
142
+ ];
143
+ const result = computeDispositionRewrite({
144
+ slug: 'wave2-root',
145
+ disposition: 'new-head',
146
+ dependsOn: [],
147
+ rows,
148
+ });
149
+ expect(result.ok).toBe(true);
150
+ expect(result.dependsOn).toEqual([]);
151
+ // The unrelated head's own row is untouched — this function only ever
152
+ // returns the target row's new dependsOn, never mutates `rows`.
153
+ expect(rows.find((r) => r.slug === 'other-head').dependsOn).toEqual([]);
154
+ });
155
+
156
+ test('a change that would create a cycle is rejected', () => {
157
+ const rows = [
158
+ row({ slug: 'a', dependsOn: ['b'] }),
159
+ row({ slug: 'b' }),
160
+ ];
161
+ const result = computeDispositionRewrite({
162
+ slug: 'b',
163
+ disposition: 'append',
164
+ dependsOn: ['a'],
165
+ rows,
166
+ });
167
+ expect(result.ok).toBe(false);
168
+ expect(result.error).toMatch(/cycle/i);
169
+ });
170
+
171
+ test('running rows are untouched', () => {
172
+ const rows = [
173
+ row({ slug: 'wave1-tail' }),
174
+ row({ slug: 'wave2-root', status: 'running', dependsOn: ['wave1-tail'] }),
175
+ ];
176
+ const result = computeDispositionRewrite({
177
+ slug: 'wave2-root',
178
+ disposition: 'new-head',
179
+ dependsOn: [],
180
+ rows,
181
+ });
182
+ expect(result.ok).toBe(false);
183
+ expect(result.error).toMatch(/running/i);
184
+ });
185
+
186
+ test('completed rows are untouched', () => {
187
+ const rows = [
188
+ row({ slug: 'wave1-tail' }),
189
+ row({ slug: 'wave2-root', status: 'completed', dependsOn: ['wave1-tail'] }),
190
+ ];
191
+ const result = computeDispositionRewrite({
192
+ slug: 'wave2-root',
193
+ disposition: 'append',
194
+ dependsOn: ['wave1-tail'],
195
+ rows,
196
+ });
197
+ expect(result.ok).toBe(false);
198
+ expect(result.error).toMatch(/completed/i);
199
+ });
200
+
201
+ test('refuses to rewrite a row whose existing blocker has already completed', () => {
202
+ const rows = [
203
+ row({ slug: 'blocker', status: 'completed' }),
204
+ row({ slug: 'dependent', status: 'pending', dependsOn: ['blocker'] }),
205
+ ];
206
+ const result = computeDispositionRewrite({
207
+ slug: 'dependent',
208
+ disposition: 'new-head',
209
+ dependsOn: [],
210
+ rows,
211
+ });
212
+ expect(result.ok).toBe(false);
213
+ expect(result.error).toMatch(/already completed|satisfied/i);
214
+ });
215
+
216
+ test('unknown slug is refused, not silently ignored', () => {
217
+ const result = computeDispositionRewrite({
218
+ slug: 'ghost',
219
+ disposition: 'new-head',
220
+ dependsOn: [],
221
+ rows: [row({ slug: 'a' })],
222
+ });
223
+ expect(result.ok).toBe(false);
224
+ });
@@ -0,0 +1,119 @@
1
+ // PRD: fail-closed process identity so a recycled pid can never pin
2
+ // instanceLock ownership or dodge killOrphanClaudePid's veto. Stat parsing is
3
+ // exercised against fixture files under SM_PROC_ROOT (never real /proc) so a
4
+ // synthetic `comm` containing parens/spaces can be asserted precisely.
5
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
6
+ import fs from 'node:fs';
7
+ import os from 'node:os';
8
+ import path from 'node:path';
9
+
10
+ const { identity, isDifferentProcess } = require('../procIdentity.cjs');
11
+
12
+ let dir;
13
+
14
+ function writeProcEntry(root, pid, { stat, cmdline }) {
15
+ const entryDir = path.join(root, String(pid));
16
+ fs.mkdirSync(entryDir, { recursive: true });
17
+ if (stat !== undefined) fs.writeFileSync(path.join(entryDir, 'stat'), stat);
18
+ if (cmdline !== undefined) fs.writeFileSync(path.join(entryDir, 'cmdline'), cmdline);
19
+ }
20
+
21
+ // Real /proc/<pid>/stat line, comm replaced with one containing a space and
22
+ // an embedded ')' — field 22 (starttime) is `124686`, at the position AFTER
23
+ // the LAST ')', not the first.
24
+ function statLine(comm, startTicks) {
25
+ return `12345 (${comm}) R 1 12345 12345 0 -1 4194304 118 0 0 0 0 0 0 0 32 12 1 0 ${startTicks} 18698240 440 0 0 0 0 0 0 0 0 0 0 0 17 7 0 0 0 0 0 0 0 0 0 0 0 0\n`;
26
+ }
27
+
28
+ beforeEach(() => {
29
+ dir = fs.mkdtempSync(path.join(os.tmpdir(), 'sm-proc-identity-'));
30
+ process.env.SM_PROC_ROOT = dir;
31
+ });
32
+
33
+ afterEach(() => {
34
+ delete process.env.SM_PROC_ROOT;
35
+ fs.rmSync(dir, { recursive: true, force: true });
36
+ });
37
+
38
+ describe('procIdentity.identity — /proc parsing', () => {
39
+ it('parses field 22 located after the LAST ")" when comm contains a paren and a space', () => {
40
+ writeProcEntry(dir, 4242, {
41
+ stat: statLine('claude (worker)', 999888),
42
+ cmdline: 'node\0claude\0--session-id\0abc\0',
43
+ });
44
+ const id = identity(4242);
45
+ expect(id.pid).toBe(4242);
46
+ expect(id.startTicks).toBe(999888);
47
+ expect(id.cmdline).toBe('node claude --session-id abc');
48
+ expect(id.complete).toBe(true);
49
+ });
50
+
51
+ it('reads the live node process itself from real /proc (sanity check against production code path)', () => {
52
+ delete process.env.SM_PROC_ROOT;
53
+ const id = identity(process.pid);
54
+ expect(id.pid).toBe(process.pid);
55
+ expect(typeof id.startTicks).toBe('number');
56
+ expect(id.startTicks).toBeGreaterThan(0);
57
+ expect(typeof id.cmdline).toBe('string');
58
+ expect(id.cmdline.length).toBeGreaterThan(0);
59
+ expect(id.complete).toBe(true);
60
+ process.env.SM_PROC_ROOT = dir; // restore for afterEach
61
+ });
62
+
63
+ it('tri-state: missing stat file → incomplete (startTicks null) even though cmdline is readable', () => {
64
+ writeProcEntry(dir, 5001, { cmdline: 'node\0claude\0' });
65
+ const id = identity(5001);
66
+ expect(id.startTicks).toBeNull();
67
+ expect(id.cmdline).toBe('node claude');
68
+ expect(id.complete).toBe(false);
69
+ });
70
+
71
+ it('tri-state: missing cmdline file → incomplete (cmdline null) even though stat is readable', () => {
72
+ writeProcEntry(dir, 5002, { stat: statLine('claude', 111) });
73
+ const id = identity(5002);
74
+ expect(id.startTicks).toBe(111);
75
+ expect(id.cmdline).toBeNull();
76
+ expect(id.complete).toBe(false);
77
+ });
78
+
79
+ it('tri-state: neither file exists (pid never existed / already reaped) → incomplete, never throws', () => {
80
+ const id = identity(999999);
81
+ expect(id.startTicks).toBeNull();
82
+ expect(id.cmdline).toBeNull();
83
+ expect(id.complete).toBe(false);
84
+ });
85
+
86
+ it('rejects a non-positive-integer pid without touching the filesystem', () => {
87
+ expect(identity(-1).complete).toBe(false);
88
+ expect(identity(0).complete).toBe(false);
89
+ expect(identity(NaN).complete).toBe(false);
90
+ });
91
+ });
92
+
93
+ describe('procIdentity.isDifferentProcess — fail-closed decision', () => {
94
+ const complete = (over = {}) => ({ pid: 100, startTicks: 555, cmdline: 'node claude', complete: true, ...over });
95
+
96
+ it('true only when both sides are complete AND a field differs', () => {
97
+ expect(isDifferentProcess(complete(), complete({ startTicks: 999 }))).toBe(true);
98
+ expect(isDifferentProcess(complete(), complete({ cmdline: 'node other' }))).toBe(true);
99
+ expect(isDifferentProcess(complete(), complete())).toBe(false);
100
+ });
101
+
102
+ it('an incomplete recorded or live side never reads as different (fail closed)', () => {
103
+ const incomplete = { pid: 100, startTicks: null, cmdline: null, complete: false };
104
+ expect(isDifferentProcess(incomplete, complete())).toBe(false);
105
+ expect(isDifferentProcess(complete(), incomplete)).toBe(false);
106
+ expect(isDifferentProcess(incomplete, incomplete)).toBe(false);
107
+ });
108
+
109
+ it('a null/undefined probe (unreadable /proc, process gone) never reads as different', () => {
110
+ expect(isDifferentProcess(complete(), null)).toBe(false);
111
+ expect(isDifferentProcess(null, complete())).toBe(false);
112
+ expect(isDifferentProcess(undefined, undefined)).toBe(false);
113
+ });
114
+
115
+ it('a legacy record with no identity fields at all never reads as different', () => {
116
+ const legacy = { pid: 100 }; // e.g. an old {pid, startedAt}-only lock body
117
+ expect(isDifferentProcess(legacy, complete())).toBe(false);
118
+ });
119
+ });