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
@@ -54,6 +54,8 @@ const path = require('node:path');
54
54
  const os = require('node:os');
55
55
  const crypto = require('node:crypto');
56
56
  const { execFile } = require('node:child_process');
57
+ const { OPS_ROOT_DIR } = require('./opsOwnership.cjs');
58
+ const schedulerPaths = require('./schedulerPaths.cjs');
57
59
 
58
60
  // Per-kind configuration. Roots are kept OUTSIDE any project's own tree
59
61
  // (os.tmpdir(), not `<cwd>/.git/...`) so a managed worktree never shows up in
@@ -85,7 +87,7 @@ const { execFile } = require('node:child_process');
85
87
  // days, same rationale as defaultMax above.
86
88
  const KIND_CONFIG = {
87
89
  job: {
88
- root: path.join(os.tmpdir(), 'session-manager-job-worktrees'),
90
+ get root() { return schedulerPaths.worktreeRoot('job'); },
89
91
  branchPrefix: 'sm-job/',
90
92
  disableEnv: 'SM_JOB_WORKTREE_DISABLE',
91
93
  maxEnv: 'SM_JOB_WORKTREE_MAX',
@@ -94,7 +96,7 @@ const KIND_CONFIG = {
94
96
  defaultStaleSweepAgeMs: 24 * 60 * 60 * 1000,
95
97
  },
96
98
  epic: {
97
- root: path.join(os.tmpdir(), 'session-manager-epic-worktrees'),
99
+ get root() { return schedulerPaths.worktreeRoot('epic'); },
98
100
  branchPrefix: 'sm-epic/',
99
101
  disableEnv: 'SM_EPIC_WORKTREE_DISABLE',
100
102
  maxEnv: 'SM_EPIC_WORKTREE_MAX',
@@ -183,6 +185,13 @@ function isUnderRoot(p, root) {
183
185
  // corrected down to match reality when it's proven to have leaked.
184
186
  const activeWorktreeCount = { job: 0, epic: 0 };
185
187
 
188
+ // Registered checkouts this process created and has not yet cleaned up:
189
+ // dir -> { kind, branch }. Lets `expireDeadWorktreeRegistrations` prove a
190
+ // counted checkout is dead (no cwd holder + terminal row) and release its
191
+ // count — accounting only, never a kill or an rm.
192
+ const registeredCheckouts = new Map();
193
+ const expiredCheckoutDirs = new Set(); // released by expiry, awaiting their (late) cleanupWorktree
194
+
186
195
  // Per-kind promise chain used as a simple async mutex (see `withKindLock`)
187
196
  // so the cap-check-and-reserve decision in `reserveWorktreeSlot` — which now
188
197
  // sometimes needs to `await` a fresh on-disk observation — stays a single
@@ -215,6 +224,46 @@ function execGit(args, { cwd, timeout = 20_000 } = {}) {
215
224
  });
216
225
  }
217
226
 
227
+ // Unquote a single git porcelain v1 path token. Git wraps a path in double
228
+ // quotes and C-style-escapes it (\", \\, \t, \n, and \NNN octal per raw UTF-8
229
+ // byte) whenever it contains a double quote, backslash, control character, or
230
+ // any byte >= 0x80 (core.quotepath's default "ASCII-safe" behavior) — a plain
231
+ // path with none of those passes through untouched. Pure. Lives here (not
232
+ // scheduler.cjs, which also parses porcelain output) because scheduler.cjs
233
+ // requires this module — the reverse would be circular — and every porcelain
234
+ // consumer in either file must unquote a path before treating it as a real
235
+ // on-disk path (fs.existsSync, a pathspec, a Set membership check).
236
+ function unquotePorcelainPath(raw) {
237
+ if (raw.length < 2 || raw[0] !== '"' || raw[raw.length - 1] !== '"') return raw;
238
+ const inner = raw.slice(1, -1);
239
+ const bytes = [];
240
+ const simpleEscapes = { '"': 0x22, '\\': 0x5c, n: 0x0a, t: 0x09, r: 0x0d, a: 0x07, b: 0x08, f: 0x0c, v: 0x0b };
241
+ for (let i = 0; i < inner.length; i += 1) {
242
+ const c = inner[i];
243
+ if (c === '\\' && i + 1 < inner.length) {
244
+ const next = inner[i + 1];
245
+ if (Object.prototype.hasOwnProperty.call(simpleEscapes, next)) {
246
+ bytes.push(simpleEscapes[next]);
247
+ i += 1;
248
+ } else if (next >= '0' && next <= '7') {
249
+ let octal = '';
250
+ let j = i + 1;
251
+ while (j < inner.length && octal.length < 3 && inner[j] >= '0' && inner[j] <= '7') {
252
+ octal += inner[j];
253
+ j += 1;
254
+ }
255
+ bytes.push(parseInt(octal, 8) & 0xff);
256
+ i = j - 1;
257
+ } else {
258
+ bytes.push(c.charCodeAt(0));
259
+ }
260
+ } else {
261
+ for (const b of Buffer.from(c, 'utf8')) bytes.push(b);
262
+ }
263
+ }
264
+ return Buffer.from(bytes).toString('utf8');
265
+ }
266
+
218
267
  async function isGitRepo(cwd) {
219
268
  if (!cwd) return false;
220
269
  try {
@@ -281,11 +330,11 @@ async function removeWorktreeDir(cwd, dir) {
281
330
  /* best-effort */
282
331
  }
283
332
  const parentDir = path.dirname(dir);
284
- // Guard against ever rmdir-ing os.tmpdir() itself — every real caller's
333
+ // Guard against ever rmdir-ing the worktree base (os.tmpdir() or SM_WORKTREE_ROOT) itself — every real caller's
285
334
  // `dir` is `<kind root>/<hash>/<key>`, so `parentDir` is always the hash
286
335
  // dir, never the tmpdir root, but this keeps the guarantee explicit rather
287
336
  // than relying solely on call-site discipline.
288
- if (parentDir === os.tmpdir()) return;
337
+ if (parentDir === schedulerPaths.worktreeBase()) return;
289
338
  try {
290
339
  const remaining = await fsp.readdir(parentDir);
291
340
  if (remaining.length === 0) await fsp.rmdir(parentDir);
@@ -304,7 +353,7 @@ async function removeWorktreeDir(cwd, dir) {
304
353
  * <name>` segment stripped off.
305
354
  *
306
355
  * Round-trip verified before being trusted, mirroring the same defense
307
- * `scripts/lib/activeSessions.cjs`'s `worktreeMainRootOf` already applies: a
356
+ * `src/main/lib/activeSessions.cjs`'s `worktreeMainRootOf` already applies: a
308
357
  * `.git` pointer file is a plain text file, so its content must not be
309
358
  * trusted to redirect `git worktree remove --force` at an arbitrary path
310
359
  * without the candidate main tree's OWN admin dir
@@ -481,29 +530,62 @@ async function sweepStaleWorktreeCheckouts(kind, opts = {}) {
481
530
  * copy of the human's WIP stays in `cwd` the whole time. Nothing here ever
482
531
  * `git add`s or commits the carried paths on the job's behalf, and nothing
483
532
  * ever restores them back into `cwd` — they were never removed from it.
533
+ *
534
+ * The diff itself is captured via `git diff --output=<file>` so git writes
535
+ * the patch straight to disk — never through Node's stdout buffer. A prior
536
+ * version piped the patch through `execFile`'s default-`maxBuffer` stdout
537
+ * capture, which overflowed on a real dirty tree of ~240 changed paths
538
+ * (`stdout maxBuffer length exceeded`, job 1192) and silently fell back to
539
+ * running the job in the SHARED working tree — the exact moment isolation
540
+ * matters most. `--output` removes the buffer ceiling entirely regardless of
541
+ * how large the tree's outstanding diff is. The temp patch file lives
542
+ * inside `dir` (the job's own fresh worktree checkout, not a bare
543
+ * `os.tmpdir()` path) so it is guaranteed to be cleaned up either by the
544
+ * explicit `fsp.rm` below or, on any earlier failure, by the caller tearing
545
+ * the whole `dir` down.
484
546
  */
485
547
  async function captureAndCarryBaseDiff({ cwd, dir }) {
486
548
  let paths = [];
487
- let patch = '';
488
549
  try {
489
550
  const nameOut = await execGit(['diff', 'HEAD', '--name-only'], { cwd, timeout: 15_000 });
490
551
  paths = nameOut.split('\n').map((l) => l.trim()).filter(Boolean);
491
- patch = await execGit(['diff', 'HEAD', '--binary'], { cwd, timeout: 30_000 });
492
552
  } catch (e) {
493
- return { ok: false, reason: `capturing base diff failed: ${(e && (e.stderrText || e.message)) || e}` };
553
+ return { ok: false, reason: `listing changed paths failed: ${(e && (e.stderrText || e.message)) || e}` };
494
554
  }
495
- if (!patch || !patch.trim()) return { ok: true, paths: [] };
496
555
 
497
- const patchFile = path.join(os.tmpdir(), `sm-worktree-carry-${crypto.randomBytes(8).toString('hex')}.patch`);
556
+ // Everything from here on operates on a real (or attempted) file at
557
+ // `patchFile` — wrapped in one try/finally so the temp file is removed on
558
+ // EVERY exit path, including a `git diff` that errors after partially
559
+ // writing it, not just the clean-success path.
560
+ const patchFile = path.join(dir, `.sm-worktree-carry-${crypto.randomBytes(8).toString('hex')}.patch`);
498
561
  try {
499
- await fsp.writeFile(patchFile, patch, 'utf8');
500
- await execGit(['apply', '--binary', patchFile], { cwd: dir, timeout: 30_000 });
501
- } catch (e) {
502
- return { ok: false, reason: `git apply failed: ${(e && (e.stderrText || e.message)) || e}` };
562
+ try {
563
+ await execGit(['diff', 'HEAD', '--binary', `--output=${patchFile}`], { cwd, timeout: 120_000 });
564
+ } catch (e) {
565
+ return { ok: false, reason: `capturing base diff failed: ${(e && (e.stderrText || e.message)) || e}` };
566
+ }
567
+
568
+ let stat;
569
+ try {
570
+ stat = await fsp.stat(patchFile);
571
+ } catch (e) {
572
+ return { ok: false, reason: `reading captured diff file failed: ${(e && e.message) || e}` };
573
+ }
574
+ if (stat.size === 0) return { ok: true, paths: [] };
575
+
576
+ try {
577
+ // Same 120s ceiling as the capture step above: applying a patch large
578
+ // enough to need that long to generate can just as easily need that
579
+ // long to apply — leaving this at the old 30s reintroduces the same
580
+ // large-dirty-tree degrade-to-shared-tree failure one call later.
581
+ await execGit(['apply', '--binary', patchFile], { cwd: dir, timeout: 120_000 });
582
+ } catch (e) {
583
+ return { ok: false, reason: `git apply failed: ${(e && (e.stderrText || e.message)) || e}` };
584
+ }
585
+ return { ok: true, paths };
503
586
  } finally {
504
587
  try { await fsp.rm(patchFile, { force: true }); } catch { /* best-effort */ }
505
588
  }
506
- return { ok: true, paths };
507
589
  }
508
590
 
509
591
  /**
@@ -770,9 +852,31 @@ async function createWorktree({ kind, cwd, key }) {
770
852
  carriedPaths = carry.paths;
771
853
  }
772
854
 
855
+ registeredCheckouts.set(dir, { kind, branch });
773
856
  return { ok: true, dir, branch, baseCwd: cwd, carriedPaths };
774
857
  }
775
858
 
859
+ /**
860
+ * Release the count of any registered checkout whose branch has no live
861
+ * holder (no process cwd under its dir) AND whose row is terminal
862
+ * (`isTerminalBranch(branch)`). Fail-closed: an unknown/live row or a live
863
+ * holder keeps the count. Returns the expired dirs. O(registered) plus one
864
+ * /proc scan.
865
+ */
866
+ function expireDeadWorktreeRegistrations({ isTerminalBranch, holders = listCwdHolders() } = {}) {
867
+ const expired = [];
868
+ if (typeof isTerminalBranch !== 'function') return expired;
869
+ for (const [dir, reg] of [...registeredCheckouts]) {
870
+ if (hasLiveHolder(dir, holders)) continue;
871
+ if (!isTerminalBranch(reg.branch)) continue;
872
+ registeredCheckouts.delete(dir);
873
+ expiredCheckoutDirs.add(dir);
874
+ activeWorktreeCount[reg.kind] = Math.max(0, activeWorktreeCount[reg.kind] - 1);
875
+ expired.push(dir);
876
+ }
877
+ return expired;
878
+ }
879
+
776
880
  /**
777
881
  * True iff every path in `paths` has working-tree content in `cwd`
778
882
  * byte-identical to its committed blob on `branch`. Used both by the
@@ -1034,7 +1138,10 @@ async function cleanupWorktree({ kind, cwd, dir, branch, keepBranch }) {
1034
1138
  try { await execGit(['branch', '-D', branch], { cwd, timeout: 10_000 }); } catch { /* already gone */ }
1035
1139
  }
1036
1140
  try { await execGit(['worktree', 'prune'], { cwd, timeout: 10_000 }); } catch { /* best effort */ }
1037
- activeWorktreeCount[kind] = Math.max(0, activeWorktreeCount[kind] - 1);
1141
+ // A checkout the expiry pass already released must not be counted down twice.
1142
+ const alreadyExpired = dir ? expiredCheckoutDirs.delete(dir) : false;
1143
+ if (dir) registeredCheckouts.delete(dir);
1144
+ if (!alreadyExpired) activeWorktreeCount[kind] = Math.max(0, activeWorktreeCount[kind] - 1);
1038
1145
  }
1039
1146
 
1040
1147
  /**
@@ -1113,7 +1220,12 @@ async function salvageDirtyDelta({ cwd, paths, outFile }) {
1113
1220
  for (const line of String(statusOut || '').split('\n')) {
1114
1221
  if (!line) continue;
1115
1222
  const code = line.slice(0, 2);
1116
- const p = line.slice(3);
1223
+ // Unquote before use: a raw quoted+C-escaped token (any path with a
1224
+ // quote, backslash, control char, or non-ASCII byte) is not a real
1225
+ // on-disk path — passing it straight through as a pathspec to the
1226
+ // `git diff` calls below silently fails to match the real file,
1227
+ // dropping that path's content out of the salvaged patch entirely.
1228
+ const p = unquotePorcelainPath(line.slice(3));
1117
1229
  if (!p) continue;
1118
1230
  if (code === '??') untrackedPaths.push(p);
1119
1231
  else trackedPaths.push(p);
@@ -1271,9 +1383,12 @@ async function reclaimTerminalJobOrphans({ cwd, terminalSlugs, isLive }) {
1271
1383
  } catch {
1272
1384
  continue; // can't prove the checkout is clean-of-real-work — never assume
1273
1385
  }
1274
- const dirtyPaths = statusOut.split('\n').map((l) => l.slice(3).trim()).filter(Boolean);
1386
+ const dirtyPaths = statusOut.split('\n')
1387
+ .filter((l) => l.length > 0)
1388
+ .map((l) => unquotePorcelainPath(l.slice(3).trim()))
1389
+ .filter(Boolean);
1275
1390
  const opsOnly = dirtyPaths.every(
1276
- (p) => p === 'session-manager-operations' || p.startsWith('session-manager-operations/')
1391
+ (p) => p === OPS_ROOT_DIR || p.startsWith(`${OPS_ROOT_DIR}/`)
1277
1392
  );
1278
1393
  if (!opsOnly) continue; // real non-ops dirty content present — leave for a human
1279
1394
 
@@ -1402,6 +1517,7 @@ module.exports = {
1402
1517
  getMaxConcurrentWorktrees,
1403
1518
  getStaleSweepAgeMs,
1404
1519
  isGitRepo,
1520
+ unquotePorcelainPath,
1405
1521
  isBaseTreeClean,
1406
1522
  worktreeDirFor,
1407
1523
  branchNameFor,
@@ -1424,6 +1540,7 @@ module.exports = {
1424
1540
  mainTreeFromWorktreeGitFile,
1425
1541
  getObservedWorktreeCount,
1426
1542
  reserveWorktreeSlot,
1543
+ expireDeadWorktreeRegistrations,
1427
1544
  reclaimTerminalJobOrphans,
1428
1545
  // Job-kind convenience wrappers — same call shape jobWorktree.cjs has
1429
1546
  // always exposed.
@@ -0,0 +1,156 @@
1
+ /**
2
+ * guardShims.cjs — stable, app-upgrade-proof indirection for every
3
+ * PreToolUse guard hook (each name in GUARD_NAMES) that delegationReadiness.cjs installs into an
4
+ * adopting project's `.claude/settings.json`.
5
+ *
6
+ * Why this exists: those installers used to bake
7
+ * `path.resolve(__dirname, '../../../scripts/hooks/guard-*.cjs')` — THIS
8
+ * app's own copy of the script — directly into the hook command. Run via
9
+ * `npx claude-code-session-manager@latest` (the normal install path), that
10
+ * resolves under `~/.npm/_npx/<hash>/node_modules/claude-code-session-manager/`
11
+ * — a directory scoped to ONE npx resolution. The next `npx ...@latest` (a
12
+ * new release) or an npm cache prune creates/removes a different hash dir, so
13
+ * every previously-installed hook decays into a dead path. A PreToolUse
14
+ * command pointing at a missing script exits non-zero WITHOUT exit code 2 (a
15
+ * non-blocking error to the harness), so it silently guards nothing — while
16
+ * checkPrdWriteGuard correctly flips the readiness banner back to red. That
17
+ * decay is why "Delegation not ready" reappears across projects even after a
18
+ * successful install.
19
+ *
20
+ * Fix: install a STABLE shim under the user's home
21
+ * (`~/.claude/session-manager/hooks/guard-<name>.cjs`) that never moves
22
+ * across app upgrades. The shim contains no guard logic of its own — CLAUDE.md's
23
+ * "adopt by REFERENCE, never vendor" law — it reads a pointer file
24
+ * (`app-root.json`, rewritten on every app boot with the CURRENTLY running
25
+ * app's root) and `require()`s the real `scripts/hooks/guard-<name>.cjs` from
26
+ * there. An app upgrade (a new npx hash dir) just rewrites the pointer on the
27
+ * next boot; every already-installed project hook keeps resolving through
28
+ * the same shim path with no re-install.
29
+ *
30
+ * The guard scripts run their logic as a require-time side effect (stdin ->
31
+ * decision -> process.exit, not gated on `require.main === module`), so a
32
+ * plain `require()` from the shim is enough to invoke them.
33
+ */
34
+ 'use strict';
35
+
36
+ const fs = require('node:fs');
37
+ const path = require('node:path');
38
+ const os = require('node:os');
39
+ const { writeJson, writeTextAtomic, addAllowedRoot } = require('../config.cjs');
40
+
41
+ // src/main/lib -> app root (three levels up) — the same resolution
42
+ // delegationReadiness.cjs's *_GUARD_SCRIPT constants use.
43
+ const APP_ROOT = path.resolve(__dirname, '..', '..', '..');
44
+
45
+ const GUARD_NAMES = ['guard-prd-writes.cjs', 'guard-destructive-git.cjs', 'guard-inline-implementation.cjs', 'guard-self-schedule.cjs'];
46
+
47
+ function hooksDir(homeDir = os.homedir()) {
48
+ return path.join(homeDir, '.claude', 'session-manager', 'hooks');
49
+ }
50
+
51
+ function pointerPath(homeDir = os.homedir()) {
52
+ return path.join(hooksDir(homeDir), 'app-root.json');
53
+ }
54
+
55
+ function shimPath(guardName, homeDir = os.homedir()) {
56
+ return path.join(hooksDir(homeDir), guardName);
57
+ }
58
+
59
+ function shimBody(guardName) {
60
+ return `'use strict';
61
+ const fs = require('node:fs');
62
+ const path = require('node:path');
63
+ const ptr = JSON.parse(fs.readFileSync(path.join(__dirname, 'app-root.json'), 'utf8'));
64
+ require(path.join(ptr.appRoot, 'scripts', 'hooks', '${guardName}'));
65
+ `;
66
+ }
67
+
68
+ /**
69
+ * Write the pointer file + every shim in GUARD_NAMES under `homeDir`. Idempotent: a
70
+ * re-run with the same `appRoot` produces byte-identical files; a re-run with
71
+ * a DIFFERENT `appRoot` (an app upgrade) rewrites only the pointer, which is
72
+ * exactly what lets already-installed project hooks follow the upgrade with
73
+ * no re-install. Never throws — returns `{ ok, error }` so a boot-time caller
74
+ * can fire-and-forget and an installer can refuse cleanly rather than writing
75
+ * a hook command that points at a shim that doesn't exist.
76
+ */
77
+ async function writeGuardShims({ homeDir = os.homedir(), appRoot = APP_ROOT } = {}) {
78
+ try {
79
+ addAllowedRoot(homeDir);
80
+ await writeJson(pointerPath(homeDir), { appRoot });
81
+ for (const name of GUARD_NAMES) {
82
+ await writeTextAtomic(shimPath(name, homeDir), shimBody(name));
83
+ }
84
+ return { ok: true };
85
+ } catch (err) {
86
+ return { ok: false, error: err?.message ?? String(err) };
87
+ }
88
+ }
89
+
90
+ /** True when every shim + the pointer file already exist on disk under `homeDir`. */
91
+ function guardShimsExist(homeDir = os.homedir()) {
92
+ return fs.existsSync(pointerPath(homeDir)) && GUARD_NAMES.every((name) => fs.existsSync(shimPath(name, homeDir)));
93
+ }
94
+
95
+ /**
96
+ * Given an absolute script path a hook command resolved to, determine
97
+ * whether it's actually a guard SHIM (its directory carries a sibling
98
+ * `app-root.json`) rather than a direct app-root script. A direct script has
99
+ * no pointer and is trivially "real" once `fs.existsSync` is true — the
100
+ * caller doesn't need this function for that case.
101
+ *
102
+ * For a shim, existence of the shim FILE is not enough: the shim's own
103
+ * require() target is `ptr.appRoot/scripts/hooks/<name>.cjs`, resolved only
104
+ * at hook-invocation time. If the pointer is missing, unparseable, or now
105
+ * names a script that's gone, the shim silently guards nothing the next time
106
+ * a hook actually fires — decay one hop downstream of the hook command
107
+ * itself. Returns `null` when `scriptAbsPath` is not a shim (no sibling
108
+ * pointer) — the caller's plain existence check already covers that case.
109
+ * Throws when it IS a shim and the pointer has decayed.
110
+ */
111
+ function resolveShimTarget(scriptAbsPath) {
112
+ const dir = path.dirname(scriptAbsPath);
113
+ const ptrFile = path.join(dir, 'app-root.json');
114
+ if (!fs.existsSync(ptrFile)) return null;
115
+
116
+ const guardName = path.basename(scriptAbsPath);
117
+ let appRoot;
118
+ try {
119
+ appRoot = JSON.parse(fs.readFileSync(ptrFile, 'utf8'))?.appRoot;
120
+ } catch (err) {
121
+ throw new Error(`guard shim pointer ${ptrFile} is unparseable: ${err?.message ?? err}`);
122
+ }
123
+ if (typeof appRoot !== 'string' || !appRoot) {
124
+ throw new Error(`guard shim pointer ${ptrFile} has no appRoot`);
125
+ }
126
+ const real = path.join(appRoot, 'scripts', 'hooks', guardName);
127
+ if (!fs.existsSync(real)) {
128
+ throw new Error(`guard shim pointer ${ptrFile} resolves to a script that no longer exists: ${real}`);
129
+ }
130
+ return real;
131
+ }
132
+
133
+ /**
134
+ * `writeGuardShims({ homeDir })`, but returns `null` on success and an
135
+ * install-function-shaped error object (`{ ok:false, action:'error', error }`)
136
+ * on failure — the one call every `install*Guard` in delegationReadiness.cjs
137
+ * needs before it can safely write a hook command pointing at the shim.
138
+ */
139
+ async function ensureGuardShimsOrError(homeDir) {
140
+ const result = await writeGuardShims({ homeDir });
141
+ if (result.ok) return null;
142
+ return { ok: false, action: 'error', error: `could not write the stable guard shim under ${hooksDir(homeDir)}: ${result.error}` };
143
+ }
144
+
145
+ module.exports = {
146
+ APP_ROOT,
147
+ GUARD_NAMES,
148
+ hooksDir,
149
+ pointerPath,
150
+ shimPath,
151
+ shimBody,
152
+ writeGuardShims,
153
+ guardShimsExist,
154
+ resolveShimTarget,
155
+ ensureGuardShimsOrError,
156
+ };
@@ -26,10 +26,9 @@
26
26
  'use strict';
27
27
 
28
28
  const path = require('node:path');
29
- const os = require('node:os');
30
29
  const config = require('../config.cjs');
31
30
 
32
- const ROLLUP_PATH = path.join(os.homedir(), '.claude', 'session-manager', 'history-rollup.jsonl');
31
+ const { historyRollupPath } = require('./schedulerPaths.cjs');
33
32
  const COMPACT_THRESHOLD_BYTES = 5 * 1024 * 1024;
34
33
 
35
34
  const TOTALS_MODEL_ID = '';
@@ -75,7 +74,7 @@ function mergeRollupLines(lines) {
75
74
  * only want data lines should filter on modelId/projectDir.
76
75
  */
77
76
  async function readRollup(fromDate, toDate) {
78
- const { exists, text } = await config.readText(ROLLUP_PATH);
77
+ const { exists, text } = await config.readText(historyRollupPath());
79
78
  if (!exists || !text) return new Map();
80
79
  const merged = mergeRollupLines(text.split('\n'));
81
80
  const filtered = new Map();
@@ -108,7 +107,7 @@ function isDateFinalized(mergedMap, date) {
108
107
  async function appendRollupDays(entries) {
109
108
  if (!entries || entries.length === 0) return;
110
109
 
111
- const real = config.validatePath(ROLLUP_PATH);
110
+ const real = config.validatePath(historyRollupPath());
112
111
  config.validateWrite(real);
113
112
  const fsp = require('node:fs/promises');
114
113
  await fsp.mkdir(path.dirname(real), { recursive: true });
@@ -125,15 +124,15 @@ async function appendRollupDays(entries) {
125
124
 
126
125
  /** Dedupe the rollup file down to its last-write-wins merged lines. */
127
126
  async function compact() {
128
- const { exists, text } = await config.readText(ROLLUP_PATH);
127
+ const { exists, text } = await config.readText(historyRollupPath());
129
128
  if (!exists || !text) return;
130
129
  const merged = mergeRollupLines(text.split('\n'));
131
130
  const rewritten = Array.from(merged.values()).map((b) => JSON.stringify(b)).join('\n') + '\n';
132
- await config.writeTextAtomic(ROLLUP_PATH, rewritten);
131
+ await config.writeTextAtomic(historyRollupPath(), rewritten);
133
132
  }
134
133
 
135
134
  module.exports = {
136
- ROLLUP_PATH,
135
+ historyRollupPath,
137
136
  COMPACT_THRESHOLD_BYTES,
138
137
  TOTALS_MODEL_ID,
139
138
  FINALIZED_MODEL_ID,
@@ -26,14 +26,16 @@
26
26
  'use strict';
27
27
 
28
28
  const fs = require('node:fs');
29
- const os = require('node:os');
30
29
  const path = require('node:path');
30
+ const { identity, isDifferentProcess } = require('./procIdentity.cjs');
31
+ const { instanceLockPath } = require('./schedulerPaths.cjs');
32
+ const { appendAuditEvent } = require('./auditLog.cjs');
31
33
 
32
34
  // Env override is for unit tests only (isolates the lock from the real
33
35
  // ~/.claude of the machine running the suite).
34
36
  function lockPath() {
35
37
  return process.env.SM_SCHEDULER_LOCK_PATH
36
- || path.join(os.homedir(), '.claude', 'session-manager', 'scheduler-owner.lock');
38
+ || instanceLockPath();
37
39
  }
38
40
 
39
41
  function pidAlive(pid) {
@@ -58,7 +60,11 @@ function readLock() {
58
60
  }
59
61
 
60
62
  function writeLockExclusive() {
61
- const body = JSON.stringify({ pid: process.pid, startedAt: new Date().toISOString() });
63
+ const body = JSON.stringify({
64
+ pid: process.pid,
65
+ startedAt: new Date().toISOString(),
66
+ identity: identity(process.pid),
67
+ });
62
68
  fs.mkdirSync(path.dirname(lockPath()), { recursive: true });
63
69
  // 'wx' = atomic create-or-fail — two racing instances cannot both win.
64
70
  fs.writeFileSync(lockPath(), body, { flag: 'wx', mode: 0o600 });
@@ -67,14 +73,35 @@ function writeLockExclusive() {
67
73
  /**
68
74
  * Try to become the machine's scheduler owner.
69
75
  * Returns { owner: true } or { owner: false, holderPid } — never throws.
76
+ *
77
+ * A holder pid that is alive is normally respected — but if the lock body
78
+ * carries a COMPLETE recorded identity that provably differs from the live
79
+ * process now holding that pid (procIdentity.isDifferentProcess), the pid
80
+ * was recycled after a hard crash: today's bare pidAlive() check would pin
81
+ * ownership on that unrelated live process forever. That case is treated as
82
+ * stale and the lock is broken (audited as `stale_lock_broken`). A missing
83
+ * identity (legacy lock body, or an unreadable /proc on either side) is
84
+ * fail-closed to "not different" — i.e. today's pid-only behaviour.
70
85
  */
71
86
  function acquireSchedulerOwnership() {
72
87
  for (let attempt = 0; attempt < 2; attempt++) {
73
88
  const existing = readLock();
74
89
  if (existing && existing.pid !== process.pid && pidAlive(existing.pid)) {
75
- return { owner: false, holderPid: existing.pid };
90
+ const liveIdentity = identity(existing.pid);
91
+ if (!isDifferentProcess(existing.identity, liveIdentity)) {
92
+ return { owner: false, holderPid: existing.pid };
93
+ }
94
+ appendAuditEvent('stale_lock_broken', {
95
+ path: lockPath(),
96
+ recordedPid: existing.pid,
97
+ recordedStartedAt: existing.startedAt ?? null,
98
+ recordedIdentity: existing.identity ?? null,
99
+ liveIdentity,
100
+ });
76
101
  }
77
- // Missing, unreadable, our own, or stale (holder pid dead) — take it.
102
+ // Missing, unreadable, our own, stale (holder pid dead), or a recorded
103
+ // identity proven different from the live process at that recycled pid
104
+ // — take it.
78
105
  try {
79
106
  if (existing || fs.existsSync(lockPath())) fs.unlinkSync(lockPath());
80
107
  } catch { /* raced with another breaker — retry loop below settles it */ }
@@ -27,8 +27,13 @@
27
27
  * must still park.
28
28
  */
29
29
 
30
- /** The one app-owned root. Matches at any depth, and only as a path segment. */
31
- const OPS_ROOT_SEGMENT = 'session-manager-operations';
30
+ /**
31
+ * The one app-owned root. Matches at any depth, and only as a path segment.
32
+ * Sourced from opsOwnership.cjs (the single-writer law's own root name)
33
+ * rather than a second hardcoded literal — this file matches git-porcelain
34
+ * RELATIVE paths, never builds an fs path, so opsPath() itself doesn't apply.
35
+ */
36
+ const { OPS_ROOT_DIR: OPS_ROOT_SEGMENT } = require('./opsOwnership.cjs');
32
37
 
33
38
  /** True when `p` lives under an app-owned operations root. Pure. */
34
39
  function isAppOwnedChurn(p) {