claude-code-session-manager 0.86.0 → 0.88.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (335) hide show
  1. package/README.md +26 -53
  2. package/dist/assets/AgentLibrary-BZ6IY2g1.js +3 -0
  3. package/dist/assets/{DataModel-Q4jhl24R.js → DataModel-dAoGAODX.js} +1 -1
  4. package/dist/assets/{History-Cj2FejEo.js → History-CF157HyX.js} +2 -2
  5. package/dist/assets/{Hooks-CaelQI6t.js → Hooks-CCPPipBk.js} +3 -3
  6. package/dist/assets/{HostBilko--v7cMR8I.js → HostBilko-Dlmz2m06.js} +1 -1
  7. package/dist/assets/{Library-DgI9oCCZ.js → Library-BaXcboWX.js} +1 -1
  8. package/dist/assets/{ListDetail-DYUZN-x-.js → ListDetail-CoUCzG5S.js} +1 -1
  9. package/dist/assets/MarkdownEditor-DTg3JlOT.js +1 -0
  10. package/dist/assets/{McpServers-ypCYURh3.js → McpServers-DdjY1sA3.js} +2 -2
  11. package/dist/assets/{Memory-C2qYp-3M.js → Memory-BJ3Jlw0C.js} +4 -4
  12. package/dist/assets/{Panel-Cj2kw-Zv.js → Panel-ygRSgc41.js} +1 -1
  13. package/dist/assets/Permissions-m0KAnccF.js +3 -0
  14. package/dist/assets/{Plugins-C1Vj8_dU.js → Plugins-CJRl6B3Q.js} +2 -2
  15. package/dist/assets/{ProvenanceBadge-DczPNM5U.js → ProvenanceBadge-w8cnlVvW.js} +1 -1
  16. package/dist/assets/{SaveBar-Cd_7U6Gb.js → SaveBar-BpyPcUIk.js} +1 -1
  17. package/dist/assets/Scheduler-D2ppxSfy.js +14 -0
  18. package/dist/assets/{ScopeSwitcher-DVSyI44-.js → ScopeSwitcher-EyuIwt-j.js} +1 -1
  19. package/dist/assets/Settings-BHSGGFEc.js +3 -0
  20. package/dist/assets/{SkillReferenceGraph-CUv1_Q2c.js → SkillReferenceGraph-BNaZ2JCF.js} +1 -1
  21. package/dist/assets/{Skills-C_YHkAy-.js → Skills-Dl3q75QV.js} +2 -2
  22. package/dist/assets/{SystemPrompt-B8R7T9xn.js → SystemPrompt-Bg6F-Cyp.js} +1 -1
  23. package/dist/assets/{TagLibrary-dj9YHWyy.js → TagLibrary-BSutflCy.js} +1 -1
  24. package/dist/assets/{TiptapBody-DnSBUjHE.js → TiptapBody-CXih4xrc.js} +1 -1
  25. package/dist/assets/{Toggle-CjV_BJn6.js → Toggle-2qtUHhA9.js} +1 -1
  26. package/dist/assets/{index-CXFQIPhO.js → index-B3emy2oI.js} +583 -583
  27. package/dist/assets/{index-CDo9xBR9.css → index-BHTX4OTc.css} +1 -1
  28. package/dist/assets/settingsSchema-CdzxaqwM.js +3 -0
  29. package/dist/assets/{whisperWorker-Dbia1OpC.js → whisperWorker-C7ZGQwKg.js} +7 -7
  30. package/dist/index.html +2 -2
  31. package/dist/vad/ort-wasm-simd-threaded.asyncify.mjs +106 -110
  32. package/dist/vad/ort-wasm-simd-threaded.asyncify.wasm +0 -0
  33. package/dist/vad/ort-wasm-simd-threaded.jsep.mjs +98 -98
  34. package/dist/vad/ort-wasm-simd-threaded.jsep.wasm +0 -0
  35. package/dist/vad/ort-wasm-simd-threaded.jspi.mjs +99 -102
  36. package/dist/vad/ort-wasm-simd-threaded.jspi.wasm +0 -0
  37. package/dist/vad/ort-wasm-simd-threaded.mjs +46 -46
  38. package/dist/vad/ort-wasm-simd-threaded.wasm +0 -0
  39. package/package.json +14 -17
  40. package/plugins/CLAUDE.md +61 -0
  41. package/plugins/session-manager-dev/.claude-plugin/plugin.json +1 -1
  42. package/plugins/session-manager-dev/skills/builder/4-manual/SKILL.md +1 -1
  43. package/plugins/session-manager-dev/skills/ops-sweep/SKILL.md +1 -1
  44. package/scripts/README.md +59 -0
  45. package/scripts/audit-ops-hygiene.cjs +350 -0
  46. package/scripts/hooks/guard-destructive-git.cjs +10 -34
  47. package/scripts/hooks/guard-inline-implementation.cjs +14 -8
  48. package/scripts/hooks/guard-prd-writes.cjs +7 -36
  49. package/scripts/hooks/guard-self-schedule.cjs +175 -0
  50. package/scripts/ops-sweep.cjs +355 -0
  51. package/scripts/scheduler-mcp-server.cjs +35 -71
  52. package/src/main/__tests__/agentModelResolve.test.cjs +100 -9
  53. package/src/main/__tests__/bilkoHost-integration.test.cjs +3 -3
  54. package/src/main/__tests__/broadcastCoalescer.test.cjs +18 -0
  55. package/src/main/__tests__/chat-cancel-terminal.test.cjs +6 -9
  56. package/src/main/__tests__/chat-exit-close-race.test.cjs +3 -3
  57. package/src/main/__tests__/chat-mcp-consent-notice.test.cjs +4 -5
  58. package/src/main/__tests__/chat-queue.test.cjs +2 -2
  59. package/src/main/__tests__/chat-stop-signal.test.cjs +2 -2
  60. package/src/main/__tests__/dep-orphan-archive-health.test.cjs +77 -0
  61. package/src/main/__tests__/dod-batchkey.test.cjs +2 -2
  62. package/src/main/__tests__/dod-drain-hook.test.cjs +2 -2
  63. package/src/main/__tests__/dod-report.test.cjs +2 -2
  64. package/src/main/__tests__/dod-reverify.test.cjs +2 -2
  65. package/src/main/__tests__/epicMint.test.cjs +4 -4
  66. package/src/main/__tests__/exchanges.test.cjs +2 -2
  67. package/src/main/__tests__/extractJson.test.cjs +2 -2
  68. package/src/main/__tests__/files-reject-credentials.test.cjs +1 -1
  69. package/src/main/__tests__/fixtures/1218-fo-01-move-scripts-lib-into-src-main-lib.log +556 -0
  70. package/src/main/__tests__/health-build-freshness.test.cjs +39 -0
  71. package/src/main/__tests__/health-delegation-chain.test.cjs +17 -2
  72. package/src/main/__tests__/health-queue-dispatch.test.cjs +58 -7
  73. package/src/main/__tests__/health-tick-liveness.test.cjs +11 -3
  74. package/src/main/__tests__/health-usage-poller.test.cjs +70 -23
  75. package/src/main/__tests__/historyRollup.test.cjs +2 -2
  76. package/src/main/__tests__/kg-augment.test.cjs +2 -2
  77. package/src/main/__tests__/mcpStatus.test.cjs +2 -2
  78. package/src/main/__tests__/memoryAggregate.test.cjs +1 -1
  79. package/src/main/__tests__/memoryStale.test.cjs +1 -1
  80. package/src/main/__tests__/needsReviewLedger.test.cjs +162 -0
  81. package/src/main/__tests__/opsErrorLogTelemetryTap.test.cjs +28 -4
  82. package/src/main/__tests__/pollLoop-dispatch-on-failure.test.cjs +48 -4
  83. package/src/main/__tests__/prd-group-allocator.test.cjs +2 -2
  84. package/src/main/__tests__/prdAdminRouteParity.test.cjs +2 -0
  85. package/src/main/__tests__/prdAdminRoutes.test.cjs +14 -2
  86. package/src/main/__tests__/prdAuthoringSeed.test.cjs +39 -0
  87. package/src/main/__tests__/prdCreateDisposition.test.cjs +201 -0
  88. package/src/main/__tests__/prdFrontmatterDisposition.test.cjs +125 -0
  89. package/src/main/__tests__/prdLocations.test.cjs +100 -2
  90. package/src/main/__tests__/prdLocationsArchived.test.cjs +50 -1
  91. package/src/main/__tests__/prdSetDisposition.test.cjs +222 -0
  92. package/src/main/__tests__/proc-role-env.test.cjs +125 -0
  93. package/src/main/__tests__/procname-claude-spawn-sites.test.cjs +304 -0
  94. package/src/main/__tests__/procname-sm-processes.test.cjs +127 -0
  95. package/src/main/__tests__/projectHomeAdminRoutes.test.cjs +81 -401
  96. package/src/main/__tests__/projectPages.test.cjs +63 -149
  97. package/src/main/__tests__/queue-health-verdict.test.cjs +170 -0
  98. package/src/main/__tests__/queue-starvation-dispatch-driver.test.cjs +117 -20
  99. package/src/main/__tests__/queue-starvation-per-project.test.cjs +14 -2
  100. package/src/main/__tests__/queueHistory.test.cjs +65 -2
  101. package/src/main/__tests__/rateLimitPollerStreak.test.cjs +38 -3
  102. package/src/main/__tests__/reconcileTiming.test.cjs +135 -0
  103. package/src/main/__tests__/runVerify-landed-commit-outranks.test.cjs +181 -0
  104. package/src/main/__tests__/runVerify.test.cjs +5 -5
  105. package/src/main/__tests__/scheduleJobStatusDrift.test.cjs +3 -3
  106. package/src/main/__tests__/scheduleJobTransitions.test.cjs +103 -3
  107. package/src/main/__tests__/scheduler-adopted-run-supervision.test.cjs +143 -0
  108. package/src/main/__tests__/scheduler-autofix-select.test.cjs +2 -2
  109. package/src/main/__tests__/scheduler-autopromote.test.cjs +2 -2
  110. package/src/main/__tests__/scheduler-bash-timeout-env.test.cjs +3 -5
  111. package/src/main/__tests__/scheduler-boot-orphans.test.cjs +79 -98
  112. package/src/main/__tests__/scheduler-broadcast-reconcile.test.cjs +121 -0
  113. package/src/main/__tests__/scheduler-cross-project-batch.test.cjs +43 -0
  114. package/src/main/__tests__/scheduler-default-eligible-heal.test.cjs +161 -0
  115. package/src/main/__tests__/scheduler-dispatch-loop.test.cjs +58 -0
  116. package/src/main/__tests__/scheduler-epic-digest.test.cjs +3 -5
  117. package/src/main/__tests__/scheduler-force-tick-outcome.test.cjs +2 -2
  118. package/src/main/__tests__/scheduler-gate-shadow.test.cjs +119 -0
  119. package/src/main/__tests__/scheduler-guard-verdict-autoresolve.test.cjs +390 -0
  120. package/src/main/__tests__/scheduler-heartbeat-payload.test.cjs +80 -0
  121. package/src/main/__tests__/scheduler-inplace-salvage.test.cjs +25 -18
  122. package/src/main/__tests__/scheduler-investigation-prompt.test.cjs +4 -4
  123. package/src/main/__tests__/scheduler-job-budget.test.cjs +172 -0
  124. package/src/main/__tests__/scheduler-launch-failure.test.cjs +3 -5
  125. package/src/main/__tests__/scheduler-looks-done.test.cjs +119 -10
  126. package/src/main/__tests__/scheduler-manual-pause.test.cjs +118 -0
  127. package/src/main/__tests__/scheduler-meta-code-sha.test.cjs +26 -3
  128. package/src/main/__tests__/scheduler-porcelain-rename.test.cjs +164 -0
  129. package/src/main/__tests__/scheduler-prd-missing-skip.test.cjs +17 -2
  130. package/src/main/__tests__/scheduler-prd-persona-spawn.test.cjs +3 -5
  131. package/src/main/__tests__/scheduler-quiet-machine-lease.test.cjs +41 -6
  132. package/src/main/__tests__/scheduler-rate-limit-pause.test.cjs +62 -6
  133. package/src/main/__tests__/scheduler-rate-limit-spin-guard.test.cjs +3 -5
  134. package/src/main/__tests__/scheduler-reap-dead-running-jobs.test.cjs +182 -3
  135. package/src/main/__tests__/scheduler-reaper-helpers-basics.test.cjs +87 -0
  136. package/src/main/__tests__/scheduler-reconcile-cwd-preserve.test.cjs +100 -0
  137. package/src/main/__tests__/scheduler-reconcile-invalid-repair.test.cjs +3 -3
  138. package/src/main/__tests__/scheduler-reconcile-quarantine.test.cjs +4 -4
  139. package/src/main/__tests__/scheduler-shard-quarantine.test.cjs +110 -0
  140. package/src/main/__tests__/scheduler-shared-tree-guard.test.cjs +168 -0
  141. package/src/main/__tests__/scheduler-starve-escalation.test.cjs +18 -8
  142. package/src/main/__tests__/scheduler-stranded-autofix-park.test.cjs +245 -0
  143. package/src/main/__tests__/scheduler-supervisor-record.test.cjs +81 -0
  144. package/src/main/__tests__/scheduler-tick-cancel-token.test.cjs +2 -2
  145. package/src/main/__tests__/scheduler-tick-wedge.test.cjs +172 -0
  146. package/src/main/__tests__/scheduler-transient-failure.test.cjs +2 -2
  147. package/src/main/__tests__/scheduler-worktree-cap-defer.test.cjs +51 -11
  148. package/src/main/__tests__/scheduler-worktree-exec-cwd.test.cjs +3 -5
  149. package/src/main/__tests__/usageSingleFlight.test.cjs +157 -0
  150. package/src/main/__tests__/workTypeLibrary.test.cjs +1 -1
  151. package/src/main/bilkoHost.cjs +16 -7
  152. package/src/main/build-info.json +8 -0
  153. package/src/main/chatRunner.cjs +12 -7
  154. package/src/main/config.cjs +2 -3
  155. package/src/main/docEdit.cjs +4 -2
  156. package/src/main/health.cjs +268 -52
  157. package/src/main/heapSnapshot.cjs +2 -2
  158. package/src/main/historyAggregator.cjs +6 -1
  159. package/src/main/index.cjs +152 -50
  160. package/src/main/ipcSchemas.cjs +53 -17
  161. package/src/main/lib/__tests__/active-sessions.test.cjs +251 -0
  162. package/src/main/lib/__tests__/auditLog.test.cjs +38 -0
  163. package/src/main/lib/__tests__/bootSelfHeal.test.cjs +107 -0
  164. package/src/main/lib/__tests__/buildIdentity.test.cjs +121 -0
  165. package/src/main/lib/__tests__/cwdClassify.test.cjs +111 -0
  166. package/src/main/lib/__tests__/definitionOfDoneSequence.test.cjs +95 -0
  167. package/src/main/lib/__tests__/delegationReadiness.test.cjs +323 -44
  168. package/src/main/lib/__tests__/dispatchLoop.test.cjs +63 -0
  169. package/src/main/lib/__tests__/effectiveModelInfo.test.cjs +239 -0
  170. package/src/main/lib/__tests__/gateFixtures.json +20 -0
  171. package/src/main/lib/__tests__/gitWorktree.test.cjs +97 -1
  172. package/src/main/lib/__tests__/guardShims.test.cjs +151 -0
  173. package/src/main/lib/__tests__/instanceLock.test.cjs +93 -7
  174. package/src/main/lib/__tests__/jobSupervisorRecord.test.cjs +78 -0
  175. package/src/main/lib/__tests__/jobWorktreeBootLive.test.cjs +11 -0
  176. package/src/main/lib/__tests__/localAdminHttp.test.cjs +1 -1
  177. package/src/main/lib/__tests__/mcpToolCatalog.test.cjs +6 -1
  178. package/src/main/lib/__tests__/opsRootAbsoluteCwd.test.cjs +5 -5
  179. package/src/main/lib/__tests__/prdDisposition.test.cjs +224 -0
  180. package/src/main/lib/__tests__/procIdentity.test.cjs +119 -0
  181. package/src/main/lib/__tests__/procName.test.cjs +92 -0
  182. package/src/main/lib/__tests__/queueStoreMachineStateRecovery.test.cjs +67 -0
  183. package/src/main/lib/__tests__/reaperHelpers.test.cjs +179 -1
  184. package/src/main/lib/__tests__/schedulerMcpServerHelp.test.cjs +6 -7
  185. package/src/main/lib/__tests__/schedulerMcpServerProjectHome.test.cjs +37 -204
  186. package/src/main/lib/__tests__/schedulerPaths.test.cjs +226 -0
  187. package/src/main/lib/__tests__/schedulerPathsWorktree.test.cjs +94 -0
  188. package/src/main/lib/__tests__/schedulerRuntimeState.test.cjs +56 -0
  189. package/src/main/lib/__tests__/sessionSlots.test.cjs +45 -0
  190. package/src/main/lib/__tests__/telemetryBacklog.test.cjs +1 -1
  191. package/src/main/lib/__tests__/upgradeDrain.test.cjs +130 -0
  192. package/src/main/lib/__tests__/usageCircuit.test.cjs +285 -0
  193. package/src/main/lib/__tests__/watchdog-helpers.test.cjs +375 -0
  194. package/src/main/lib/__tests__/watchdog-relaunch.test.cjs +266 -0
  195. package/src/main/lib/activeIndexRebuild.cjs +1 -3
  196. package/{scripts → src/main}/lib/activeSessions.cjs +67 -89
  197. package/src/main/lib/adoptedRunSupervisor.cjs +136 -0
  198. package/src/main/lib/agentModelResolve.cjs +94 -24
  199. package/src/main/lib/agentPersonaSchema.cjs +2 -2
  200. package/src/main/lib/auditLog.cjs +30 -5
  201. package/src/main/lib/bootSelfHeal.cjs +88 -0
  202. package/src/main/lib/buildIdentity.cjs +113 -0
  203. package/src/main/lib/classifyPromptTicket.cjs +3 -2
  204. package/src/main/lib/claudeBin.cjs +37 -2
  205. package/src/main/lib/cleanEnv.cjs +27 -1
  206. package/src/main/lib/credentials.cjs +4 -2
  207. package/src/main/lib/cwdClassify.cjs +185 -0
  208. package/src/main/lib/definitionOfDone.cjs +296 -52
  209. package/src/main/lib/delegationReadiness.cjs +290 -225
  210. package/src/main/lib/dispatchLoop.cjs +40 -0
  211. package/src/main/lib/effectiveModelInfo.cjs +332 -0
  212. package/src/main/lib/ephemeralCwd.cjs +7 -28
  213. package/src/main/lib/epicMint.cjs +3 -3
  214. package/src/main/lib/gitWorktree.cjs +136 -19
  215. package/src/main/lib/guardShims.cjs +156 -0
  216. package/src/main/lib/historyRollup.cjs +6 -7
  217. package/src/main/lib/instanceLock.cjs +32 -5
  218. package/src/main/lib/jobDirtFilter.cjs +7 -2
  219. package/src/main/lib/jobSupervisorRecord.cjs +147 -0
  220. package/src/main/lib/jobWorktreeBootLive.cjs +9 -5
  221. package/src/main/lib/launchFailure.cjs +2 -1
  222. package/src/main/lib/localAdminHttp.cjs +9 -27
  223. package/src/main/lib/mcpToolCatalog.cjs +33 -78
  224. package/src/main/lib/needsReviewLedger.cjs +205 -0
  225. package/src/main/lib/opsErrorLog.cjs +1 -1
  226. package/src/main/lib/opsOwnership.cjs +27 -19
  227. package/src/main/lib/prdAuthoringSeed.cjs +36 -0
  228. package/src/main/lib/prdCreate.cjs +56 -1
  229. package/src/main/lib/prdDisposition.cjs +199 -0
  230. package/src/main/lib/prdFrontmatter.cjs +8 -2
  231. package/src/main/lib/prdLocations.cjs +214 -45
  232. package/src/main/lib/procIdentity.cjs +126 -0
  233. package/src/main/lib/procName.cjs +98 -0
  234. package/src/main/lib/projectHomeAdminRoutes.cjs +56 -327
  235. package/src/main/lib/projectRootResolve.cjs +1 -1
  236. package/src/main/lib/queueHistory.cjs +27 -8
  237. package/src/main/lib/queueStore.cjs +63 -38
  238. package/src/main/lib/quietMachineLease.cjs +19 -2
  239. package/src/main/lib/reaperHelpers.cjs +181 -15
  240. package/src/main/lib/reservationExpiry.cjs +30 -0
  241. package/src/main/lib/runClaudeP.cjs +4 -2
  242. package/src/main/lib/runLogRetention.cjs +3 -2
  243. package/src/main/lib/scheduleJobSchema.cjs +10 -2
  244. package/src/main/lib/scheduleJobTransitions.cjs +60 -0
  245. package/src/main/lib/schedulerBatch.cjs +27 -2
  246. package/src/main/lib/schedulerConfig.cjs +24 -0
  247. package/src/main/lib/schedulerPaths.cjs +175 -0
  248. package/src/main/lib/schedulerRuntimeState.cjs +59 -0
  249. package/src/main/lib/sessionSlots.cjs +47 -9
  250. package/src/main/lib/smProcNames.cjs +51 -0
  251. package/src/main/lib/upgradeDrain.cjs +188 -0
  252. package/src/main/lib/usageCircuit.cjs +206 -0
  253. package/{scripts → src/main}/lib/watchdogHelpers.cjs +71 -38
  254. package/src/main/lib/withTimeout.cjs +33 -0
  255. package/src/main/mcpStatus.cjs +4 -2
  256. package/src/main/pluginInstall.cjs +6 -2
  257. package/src/main/projectPages.cjs +41 -145
  258. package/src/main/pty.cjs +8 -1
  259. package/src/main/queueOps.cjs +10 -10
  260. package/src/main/runVerify.cjs +69 -3
  261. package/src/main/scheduler/prdParser.cjs +13 -0
  262. package/src/main/scheduler.cjs +2841 -482
  263. package/src/main/seedAgentPersonas.cjs +1 -1
  264. package/src/main/seedDevPlugin.cjs +1 -1
  265. package/src/main/seedSchedulerMcp.cjs +7 -6
  266. package/src/main/seedStatus.cjs +1 -1
  267. package/src/main/supervisor.cjs +5 -3
  268. package/src/main/templates/PRD_AUTHORING.md +50 -0
  269. package/src/main/usage.cjs +140 -58
  270. package/src/preload/api.d.ts +110 -65
  271. package/src/preload/index.cjs +12 -4
  272. package/src/seed/agents/project-home-builder.md +31 -48
  273. package/dist/assets/AgentLibrary-DTFL7y8G.js +0 -3
  274. package/dist/assets/MarkdownEditor-DCIubYWf.js +0 -1
  275. package/dist/assets/Permissions-BiYZNGYW.js +0 -3
  276. package/dist/assets/Scheduler-DcLBiJBq.js +0 -14
  277. package/dist/assets/Settings-Cv-pRyms.js +0 -3
  278. package/dist/assets/settingsSchema-BJVciriw.js +0 -3
  279. package/screenshots/.gitkeep +0 -0
  280. package/screenshots/README-screenshots.md +0 -13
  281. package/scripts/project-pages-logic/dist/logic.cjs +0 -4709
  282. package/scripts/render-project-pages/dist/renderer.cjs +0 -18900
  283. package/scripts/render-project-pages.cjs +0 -70
  284. package/scripts/validate-project-pages-summary.cjs +0 -62
  285. package/src/main/lib/projectPageSummarySchema.cjs +0 -181
  286. package/src/main/teams.cjs +0 -95
  287. package/src/main/templates/project-pages-catalog.json +0 -741
  288. package/src/main/templates/project-pages-default-home.html +0 -123
  289. package/src/main/templates/project-pages-pipeline.md +0 -417
  290. package/src/seed/prompts/code-review/ac-coverage-check.md +0 -8
  291. package/src/seed/prompts/code-review/correctness-only.md +0 -8
  292. package/src/seed/prompts/code-review/full-spectrum-high.md +0 -8
  293. package/src/seed/prompts/code-review/hallucination-check.md +0 -8
  294. package/src/seed/prompts/code-review/public-api-compat.md +0 -8
  295. package/src/seed/prompts/code-review/readability-naming.md +0 -8
  296. package/src/seed/prompts/debugging/bug-as-failing-test.md +0 -8
  297. package/src/seed/prompts/debugging/git-bisect-regression.md +0 -8
  298. package/src/seed/prompts/debugging/instrument-intermittent-bug.md +0 -8
  299. package/src/seed/prompts/debugging/localize-pipeline-failure.md +0 -8
  300. package/src/seed/prompts/debugging/reproduce-then-diagnose.md +0 -8
  301. package/src/seed/prompts/documentation/adr-from-change.md +0 -8
  302. package/src/seed/prompts/documentation/module-readme.md +0 -8
  303. package/src/seed/prompts/documentation/onboarding-plan.md +0 -8
  304. package/src/seed/prompts/documentation/refresh-claude-md.md +0 -8
  305. package/src/seed/prompts/documentation/tsdoc-public-exports.md +0 -8
  306. package/src/seed/prompts/git-pr/conventional-commit.md +0 -8
  307. package/src/seed/prompts/git-pr/draft-pr-title-body.md +0 -8
  308. package/src/seed/prompts/git-pr/pre-commit-safety-sweep.md +0 -8
  309. package/src/seed/prompts/git-pr/release-notes-block.md +0 -8
  310. package/src/seed/prompts/git-pr/split-large-pr.md +0 -8
  311. package/src/seed/prompts/performance/bundle-startup-audit.md +0 -8
  312. package/src/seed/prompts/performance/complexity-audit.md +0 -8
  313. package/src/seed/prompts/performance/cpu-profile-hot-path.md +0 -8
  314. package/src/seed/prompts/performance/db-query-plan-review.md +0 -8
  315. package/src/seed/prompts/performance/memory-leak-hunt.md +0 -8
  316. package/src/seed/prompts/qa/api-contract-tests.md +0 -8
  317. package/src/seed/prompts/qa/e2e-critical-path.md +0 -8
  318. package/src/seed/prompts/qa/failing-test-for-bug.md +0 -8
  319. package/src/seed/prompts/qa/find-missing-test-coverage.md +0 -8
  320. package/src/seed/prompts/qa/stabilize-flaky-test.md +0 -8
  321. package/src/seed/prompts/qa/tdd-red-first.md +0 -8
  322. package/src/seed/prompts/qa/visual-regression-review.md +0 -8
  323. package/src/seed/prompts/qa/wcag-axe-scan.md +0 -8
  324. package/src/seed/prompts/refactoring/dead-code-sweep.md +0 -8
  325. package/src/seed/prompts/refactoring/extract-duplicated-pattern.md +0 -8
  326. package/src/seed/prompts/refactoring/modernize-legacy-file.md +0 -8
  327. package/src/seed/prompts/refactoring/reduce-cyclomatic-complexity.md +0 -8
  328. package/src/seed/prompts/refactoring/tighten-module-boundaries.md +0 -8
  329. package/src/seed/prompts/security/authz-audit.md +0 -8
  330. package/src/seed/prompts/security/crypto-correctness.md +0 -8
  331. package/src/seed/prompts/security/cwe-top-25-hunt.md +0 -8
  332. package/src/seed/prompts/security/dependency-audit.md +0 -8
  333. package/src/seed/prompts/security/ipc-boundary-hardening.md +0 -8
  334. package/src/seed/prompts/security/owasp-top-10-staged-diff.md +0 -8
  335. package/src/seed/prompts/security/secret-credential-scan.md +0 -8
@@ -12,12 +12,22 @@
12
12
  * live. Active-project discovery reuses activeSessions.cjs's
13
13
  * activeProjectCwds (the same discovery watchdogHelpers.cjs's sweep() uses)
14
14
  * rather than re-implementing transcript scanning here.
15
+ *
16
+ * CACHING NOTE (PRD adding mtime-keyed memoization to this module): what's
17
+ * cached here is only WHICH DIRECTORIES EXIST, never their CONTENTS. A new
18
+ * PRD .md file dropped into an already-known `prds/` dir does not change
19
+ * that dir's own existence, so it needs no cache invalidation here at all —
20
+ * `schedule:rescan` (scheduler.cjs's `ipcMain.handle('schedule:rescan', …)`)
21
+ * still reports it because the actual file-listing happens one layer down,
22
+ * in scheduler/prdParser.cjs's `listPrdFiles(dir)`, which has its OWN
23
+ * separate mtime cache keyed on that specific dir's mtime (bumped by the new
24
+ * file) and is unaffected by anything cached in this module.
15
25
  */
16
26
  'use strict';
17
27
 
18
28
  const fs = require('node:fs');
19
29
  const path = require('node:path');
20
- const { activeProjectCwds, allProjectCwds } = require('../../../scripts/lib/activeSessions.cjs');
30
+ const { activeProjectCwds, allProjectCwds } = require('./activeSessions.cjs');
21
31
 
22
32
  const { opsPath, OPS_ROOT_DIR } = require('./opsOwnership.cjs');
23
33
 
@@ -53,18 +63,49 @@ function resolveEpicPrdWriteDir(cwd, epicId) {
53
63
  return path.join(resolveEpicsRoot(cwd), epicId, 'prds');
54
64
  }
55
65
 
66
+ // listEpicPrdDirs' readdir-the-Epics-root-plus-existsSync-per-subdir walk is
67
+ // the expensive half of prdLocations' per-reconcile()-pass cost (159/166
68
+ // dirs, ~430ms/~400ms measured). A new Epic's prds/ dir is always created in
69
+ // the SAME recursive mkdirSync call that creates `epics/<newId>/` itself
70
+ // (epicMint.cjs's ensureEpic, both the mint branch and the join branch's
71
+ // belt-and-suspenders mkdirSync) — so a brand-new `epics/<id>/prds` dir
72
+ // first coming into existence always bumps the Epics root's OWN mtime (a
73
+ // new directory entry under it), never just an existing subdir's mtime.
74
+ // Cache keyed on that mtime, same dir-mtime idiom as
75
+ // scheduler/prdParser.cjs's dirCache — no TTL, so the very next call after a
76
+ // mint sees the new dir with no wait.
77
+ const epicPrdDirsCache = new Map(); // epicsRootPath -> { mtimeMs, result }
78
+
56
79
  /** Enumerate every existing `epics/<id>/prds` dir under one project cwd. */
57
80
  function listEpicPrdDirs(cwd) {
58
81
  let root;
59
82
  try { root = resolveEpicsRoot(cwd); } catch { return []; }
83
+ let mtimeMs;
84
+ try {
85
+ mtimeMs = fs.statSync(root).mtimeMs;
86
+ } catch {
87
+ // Missing/unreadable root: nothing to scan, and any prior cache entry
88
+ // must not survive to be handed back once the dir later appears.
89
+ epicPrdDirsCache.delete(root);
90
+ return [];
91
+ }
92
+ const cached = epicPrdDirsCache.get(root);
93
+ if (cached && cached.mtimeMs === mtimeMs) return cached.result;
94
+
60
95
  let entries;
61
- try { entries = fs.readdirSync(root, { withFileTypes: true }); } catch { return []; }
96
+ try {
97
+ entries = fs.readdirSync(root, { withFileTypes: true });
98
+ } catch {
99
+ epicPrdDirsCache.delete(root);
100
+ return [];
101
+ }
62
102
  const dirs = [];
63
103
  for (const ent of entries) {
64
104
  if (!ent.isDirectory()) continue;
65
105
  const prdDir = path.join(root, ent.name, 'prds');
66
106
  if (fs.existsSync(prdDir)) dirs.push(prdDir);
67
107
  }
108
+ epicPrdDirsCache.set(root, { mtimeMs, result: dirs });
68
109
  return dirs;
69
110
  }
70
111
 
@@ -101,6 +142,85 @@ function resolvePrdWriteDir(cwd) {
101
142
  return opsPath(cwd, ...PRD_SUBPATH);
102
143
  }
103
144
 
145
+ // resolvePrdsDirs/resolveArchivedPrdsDirs each redo a fs.existsSync(dir) +
146
+ // listEpicPrdDirs(cwd) walk across EVERY project on every call, and
147
+ // reconcile() calls each of them once per pass. Cache the assembled result
148
+ // keyed on the caller's own (maxAgeMin, opts) tuple — a call with an
149
+ // explicit `{ projectsDir }` opts object is a DIFFERENT key from the
150
+ // no-opts default, so the two never read or write each other's entry — PLUS
151
+ // a freshness key built from every visited project's Epics-root mtimeMs,
152
+ // same composite-mtime-key idiom as queueHistory.cjs's historyCacheKey. The
153
+ // freshness key is cheap to rebuild every call (one statSync per project,
154
+ // far cheaper than the readdir+existsSync-per-Epic walk it guards), so a
155
+ // cache hit still costs O(projects) stats but skips the expensive walk
156
+ // entirely — "one filesystem walk" per reconcile() pass, not the full
157
+ // listEpicPrdDirs cost N times over.
158
+ //
159
+ // The Epics-root mtime alone has a real blind spot: `prds-archived/` is
160
+ // created (archiveCompletedPrd, scheduler.cjs) as a NEW entry INSIDE an
161
+ // ALREADY-EXISTING `epics/<id>/` dir — unlike a brand-new Epic's `prds/`,
162
+ // which is always created in the SAME mkdirSync call as `epics/<id>/`
163
+ // itself (see listEpicPrdDirs's header), that does not bump the Epics
164
+ // root's own mtime. Re-stating every Epic subdirectory's own mtime on every
165
+ // call to close that gap would cost about what the walk itself costs,
166
+ // defeating the cache — so this cache pairs the mtime key (instant
167
+ // invalidation for the case the non-negotiable freshness test covers: a
168
+ // brand-new Epic's `prds/` dir) with the SAME short TTL backstop
169
+ // src/main/lib/activeSessions.cjs's cwdScanCache already established for
170
+ // this exact module chain, bounding the archived-dir blind spot to one
171
+ // TTL window instead of leaving it stale indefinitely. Nothing reads
172
+ // resolveArchivedPrdsDirs off the dispatch-correctness path — the
173
+ // archived-twin stale-queue-row guard (scheduler.cjs's archivedTwinExists)
174
+ // calls listArchivedPrdDirs directly, uncached — so this is purely a
175
+ // reporting-freshness bound (schedule:list-prds's per-Epic PRD/run counts).
176
+ // Above the 60 s dispatch-loop interval (scheduler POLL_INTERVAL_MS).
177
+ const ASSEMBLED_DIRS_CACHE_TTL_MS = 120_000;
178
+ const assembledPrdsDirsCache = new Map(); // callerKey -> { freshnessKey, cachedAt, result }
179
+ const assembledArchivedPrdsDirsCache = new Map();
180
+
181
+ // Sorted-keys JSON.stringify: a plain JSON.stringify(opts) would key
182
+ // { a: 1, b: 2 } and { b: 2, a: 1 } as two different cache entries — a real
183
+ // (if currently latent, since every caller today passes at most one opts
184
+ // key) correctness trap for a cache key.
185
+ function stableStringify(obj) {
186
+ return JSON.stringify(obj, Object.keys(obj).sort());
187
+ }
188
+
189
+ function callerOptsKey(maxAgeMin, opts) {
190
+ return `${maxAgeMin}::${opts ? stableStringify(opts) : ''}`;
191
+ }
192
+
193
+ // mtimeMs of each visited project's Epics root, joined into one string. Any
194
+ // project whose root doesn't exist/isn't readable contributes a stable
195
+ // sentinel so the key still changes the moment that root is created.
196
+ function epicsRootsFreshnessKey(cwds) {
197
+ return cwds
198
+ .map((cwd) => {
199
+ let root;
200
+ try { root = resolveEpicsRoot(cwd); } catch { return `${cwd}:noroot`; }
201
+ try { return `${root}:${fs.statSync(root).mtimeMs}`; } catch { return `${root}:-1`; }
202
+ })
203
+ .join('|');
204
+ }
205
+
206
+ function memoizedAssembledDirs(cache, maxAgeMin, opts, compute) {
207
+ const callerKey = callerOptsKey(maxAgeMin, opts);
208
+ const allCwds = allProjectCwds(opts);
209
+ const activeCwds = activeProjectCwds(maxAgeMin, opts);
210
+ const cwdUnion = [...new Set([...allCwds, ...activeCwds])];
211
+ const freshnessKey = `${allCwds.join(',')}#${activeCwds.join(',')}#${epicsRootsFreshnessKey(cwdUnion)}`;
212
+
213
+ const cached = cache.get(callerKey);
214
+ if (cached && cached.freshnessKey === freshnessKey && Date.now() - cached.cachedAt < ASSEMBLED_DIRS_CACHE_TTL_MS) {
215
+ return cached.result;
216
+ }
217
+
218
+ const result = compute(allCwds, activeCwds);
219
+ if (Array.isArray(result)) Object.freeze(result);
220
+ cache.set(callerKey, { freshnessKey, cachedAt: Date.now(), result });
221
+ return result;
222
+ }
223
+
104
224
  /**
105
225
  * resolvePrdsDirs(maxAgeMin?, opts?) → string[]
106
226
  *
@@ -122,34 +242,36 @@ function resolvePrdWriteDir(cwd) {
122
242
  * underlying scan (e.g. `projectsDir` override for tests).
123
243
  */
124
244
  function resolvePrdsDirs(maxAgeMin, opts) {
125
- const dirs = [];
126
- const seen = new Set();
127
- const add = (dir) => {
128
- if (seen.has(dir)) return;
129
- seen.add(dir);
130
- dirs.push(dir);
131
- };
132
-
133
- // Every historical project that has a PRD dir on disk — the set that
134
- // matters for discovery, regardless of when it was last touched.
135
- for (const cwd of allProjectCwds(opts)) {
136
- let dir;
137
- try { dir = resolvePrdWriteDir(cwd); } catch { continue; }
138
- if (fs.existsSync(dir)) add(dir);
139
- // Epic-scoped dirs (the write layout for all new PRDs) are first-class
140
- // scan sources alongside the legacy flat dir.
141
- for (const epicDir of listEpicPrdDirs(cwd)) add(epicDir);
142
- }
245
+ return memoizedAssembledDirs(assembledPrdsDirsCache, maxAgeMin, opts, (allCwds, activeCwds) => {
246
+ const dirs = [];
247
+ const seen = new Set();
248
+ const add = (dir) => {
249
+ if (seen.has(dir)) return;
250
+ seen.add(dir);
251
+ dirs.push(dir);
252
+ };
143
253
 
144
- // Active projects are added unconditionally: a brand-new project has no
145
- // prds/ dir yet, and callers that resolve a write destination must still
146
- // find it. Scans over a non-existent dir are a harmless ENOENT no-op.
147
- for (const cwd of activeProjectCwds(maxAgeMin, opts)) {
148
- try { add(resolvePrdWriteDir(cwd)); } catch { /* unusable cwd */ }
149
- for (const epicDir of listEpicPrdDirs(cwd)) add(epicDir);
150
- }
254
+ // Every historical project that has a PRD dir on disk — the set that
255
+ // matters for discovery, regardless of when it was last touched.
256
+ for (const cwd of allCwds) {
257
+ let dir;
258
+ try { dir = resolvePrdWriteDir(cwd); } catch { continue; }
259
+ if (fs.existsSync(dir)) add(dir);
260
+ // Epic-scoped dirs (the write layout for all new PRDs) are first-class
261
+ // scan sources alongside the legacy flat dir.
262
+ for (const epicDir of listEpicPrdDirs(cwd)) add(epicDir);
263
+ }
151
264
 
152
- return dirs;
265
+ // Active projects are added unconditionally: a brand-new project has no
266
+ // prds/ dir yet, and callers that resolve a write destination must still
267
+ // find it. Scans over a non-existent dir are a harmless ENOENT no-op.
268
+ for (const cwd of activeCwds) {
269
+ try { add(resolvePrdWriteDir(cwd)); } catch { /* unusable cwd */ }
270
+ for (const epicDir of listEpicPrdDirs(cwd)) add(epicDir);
271
+ }
272
+
273
+ return dirs;
274
+ });
153
275
  }
154
276
 
155
277
  /**
@@ -163,26 +285,28 @@ function resolvePrdsDirs(maxAgeMin, opts) {
163
285
  * history, not something list-prds should stop counting.
164
286
  */
165
287
  function resolveArchivedPrdsDirs(maxAgeMin, opts) {
166
- const dirs = [];
167
- const seen = new Set();
168
- const add = (dir) => {
169
- if (seen.has(dir)) return;
170
- seen.add(dir);
171
- dirs.push(dir);
172
- };
173
-
174
- for (const cwd of allProjectCwds(opts)) {
175
- for (const dir of listArchivedPrdDirs(cwd)) {
176
- if (fs.existsSync(dir)) add(dir);
288
+ return memoizedAssembledDirs(assembledArchivedPrdsDirsCache, maxAgeMin, opts, (allCwds, activeCwds) => {
289
+ const dirs = [];
290
+ const seen = new Set();
291
+ const add = (dir) => {
292
+ if (seen.has(dir)) return;
293
+ seen.add(dir);
294
+ dirs.push(dir);
295
+ };
296
+
297
+ for (const cwd of allCwds) {
298
+ for (const dir of listArchivedPrdDirs(cwd)) {
299
+ if (fs.existsSync(dir)) add(dir);
300
+ }
177
301
  }
178
- }
179
- for (const cwd of activeProjectCwds(maxAgeMin, opts)) {
180
- for (const dir of listArchivedPrdDirs(cwd)) {
181
- if (fs.existsSync(dir)) add(dir);
302
+ for (const cwd of activeCwds) {
303
+ for (const dir of listArchivedPrdDirs(cwd)) {
304
+ if (fs.existsSync(dir)) add(dir);
305
+ }
182
306
  }
183
- }
184
307
 
185
- return dirs;
308
+ return dirs;
309
+ });
186
310
  }
187
311
 
188
312
  /**
@@ -216,6 +340,50 @@ function deriveEpicIdFromPrdPath(filePath) {
216
340
  return epicId;
217
341
  }
218
342
 
343
+ /**
344
+ * deriveProjectCwdFromPrdPath(filePath) → projectCwd | null
345
+ *
346
+ * The inverse of resolvePrdWriteDir/resolveEpicPrdWriteDir: given an absolute
347
+ * PRD file path (flat `prds/`, Epic-scoped `epics/<id>/prds/`, or either's
348
+ * `prds-archived/` sibling), returns the project root it was discovered
349
+ * under — by finding the `session-manager-operations` segment and validating
350
+ * the path round-trips back through the same resolver it walked up from.
351
+ * Returns null for anything that doesn't match one of those shapes (never
352
+ * guesses). Used by reconcile()'s fresh-discovery path so a PRD file with no
353
+ * `cwd:` frontmatter still gets a real project cwd instead of null (which
354
+ * would otherwise fall through to schedulerBatch.js's DEFAULT_PROJECT_CWD
355
+ * and relocate the row into the wrong project's queue.json shard).
356
+ */
357
+ function deriveProjectCwdFromPrdPath(filePath) {
358
+ if (!filePath || typeof filePath !== 'string' || !path.isAbsolute(filePath)) return null;
359
+ const segments = filePath.split(path.sep);
360
+ const opsIdx = segments.lastIndexOf(OPS_ROOT_DIR);
361
+ if (opsIdx <= 0) return null;
362
+ const projectCwd = segments.slice(0, opsIdx).join(path.sep);
363
+ const prdsDir = path.dirname(filePath);
364
+ const dirName = path.basename(prdsDir);
365
+ if (dirName !== 'prds' && dirName !== 'prds-archived') return null;
366
+ if (dirName === 'prds') {
367
+ try {
368
+ if (resolvePrdWriteDir(projectCwd) === prdsDir) return projectCwd;
369
+ } catch { /* fall through to the Epic-scoped check below */ }
370
+ }
371
+ if (dirName === 'prds-archived') {
372
+ // Flat legacy archive sibling of resolvePrdWriteDir's flat `prds/` (see
373
+ // listArchivedPrdDirs' first entry: opsPath(cwd, ...PRD_SUBPATH, '..',
374
+ // 'prds-archived')) — round-trip it the same way the flat `prds` branch
375
+ // above does, before falling through to the Epic-scoped check (which
376
+ // can never match a flat path: deriveEpicIdFromPrdPath requires an
377
+ // `epics/<id>/` segment a flat archive dir doesn't have).
378
+ try {
379
+ if (opsPath(projectCwd, ...PRD_SUBPATH, '..', 'prds-archived') === prdsDir) return projectCwd;
380
+ } catch { /* fall through to the Epic-scoped check below */ }
381
+ }
382
+ const epicId = deriveEpicIdFromPrdPath(filePath);
383
+ if (epicId) return projectCwd;
384
+ return null;
385
+ }
386
+
219
387
  module.exports = {
220
388
  resolvePrdWriteDir,
221
389
  resolvePrdsDirs,
@@ -225,6 +393,7 @@ module.exports = {
225
393
  listEpicPrdDirs,
226
394
  listArchivedPrdDirs,
227
395
  deriveEpicIdFromPrdPath,
396
+ deriveProjectCwdFromPrdPath,
228
397
  PRD_SUBPATH,
229
398
  EPICS_SUBPATH,
230
399
  };
@@ -0,0 +1,126 @@
1
+ /**
2
+ * procIdentity.cjs — fail-closed process identity, so a recycled pid can
3
+ * never be mistaken for the process that originally held it.
4
+ *
5
+ * identity(pid) reads /proc/<pid>/stat field 22 (starttime) — located AFTER
6
+ * the last ')', because `comm` (field 2) can itself contain spaces or
7
+ * parens — plus /proc/<pid>/cmdline. On a platform with no /proc (macOS),
8
+ * falls back to `ps -o lstart=,command=` (lstart has only second
9
+ * granularity, which is the known limit of that path).
10
+ *
11
+ * isDifferentProcess(recorded, live) is the ONLY decision function callers
12
+ * should use, and it is deliberately fail-closed in the SAFE direction: it
13
+ * returns true — "safe to treat the recorded holder as gone" — ONLY when
14
+ * BOTH sides are a complete identity AND they differ. An unreadable /proc
15
+ * entry, a missing live probe, or a legacy record with no identity fields at
16
+ * all all read as "not different" — i.e. today's pid-only behaviour, never a
17
+ * false positive that could wrongly break a live lock or veto a real kill.
18
+ *
19
+ * SM_PROC_ROOT overrides the /proc root — tests only, so stat/cmdline
20
+ * fixtures can be written under a temp dir instead of touching real /proc.
21
+ */
22
+ 'use strict';
23
+
24
+ const fs = require('node:fs');
25
+ const path = require('node:path');
26
+ const { execFileSync } = require('node:child_process');
27
+
28
+ const MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
29
+
30
+ function procRoot() {
31
+ return process.env.SM_PROC_ROOT || '/proc';
32
+ }
33
+
34
+ function hasProc() {
35
+ try {
36
+ return fs.existsSync(procRoot());
37
+ } catch {
38
+ return false;
39
+ }
40
+ }
41
+
42
+ /** Field 22 (starttime) sits AFTER the last ')' — comm can hold spaces/parens. */
43
+ function parseStatStartTicks(raw) {
44
+ const lastParen = raw.lastIndexOf(')');
45
+ if (lastParen === -1) return null;
46
+ const rest = raw.slice(lastParen + 1).trim();
47
+ if (!rest) return null;
48
+ const fields = rest.split(/\s+/);
49
+ // `rest[0]` is field 3 (state); field N sits at index (N - 3), so field 22
50
+ // (starttime) is index 19.
51
+ const startTicksStr = fields[19];
52
+ if (startTicksStr === undefined) return null;
53
+ const startTicks = Number(startTicksStr);
54
+ return Number.isFinite(startTicks) ? startTicks : null;
55
+ }
56
+
57
+ function readCmdlineFile(pid) {
58
+ const raw = fs.readFileSync(path.join(procRoot(), String(pid), 'cmdline'), 'utf8');
59
+ const cmd = raw.replace(/\0+$/, '').split('\0').join(' ').trim();
60
+ return cmd.length > 0 ? cmd : null;
61
+ }
62
+
63
+ function identityFromProc(pid) {
64
+ let startTicks = null;
65
+ let cmdline = null;
66
+ try {
67
+ const raw = fs.readFileSync(path.join(procRoot(), String(pid), 'stat'), 'utf8');
68
+ startTicks = parseStatStartTicks(raw);
69
+ } catch { /* unreadable stat (dead pid, hidepid, race) — leave null */ }
70
+ try {
71
+ cmdline = readCmdlineFile(pid);
72
+ } catch { /* unreadable cmdline — leave null */ }
73
+ return { pid, startTicks, cmdline, complete: startTicks !== null && cmdline !== null };
74
+ }
75
+
76
+ function parseLstart(lstart) {
77
+ const m = /^\w{3}\s+(\w{3})\s+(\d{1,2})\s+(\d{2}):(\d{2}):(\d{2})\s+(\d{4})$/.exec(lstart.trim());
78
+ if (!m) return null;
79
+ const monthIdx = MONTHS.indexOf(m[1]);
80
+ if (monthIdx === -1) return null;
81
+ const ms = Date.UTC(Number(m[6]), monthIdx, Number(m[2]), Number(m[3]), Number(m[4]), Number(m[5]));
82
+ return Number.isFinite(ms) ? ms : null;
83
+ }
84
+
85
+ function identityFromPs(pid) {
86
+ let out;
87
+ try {
88
+ out = execFileSync('ps', ['-p', String(pid), '-o', 'lstart=,command='], { encoding: 'utf8' });
89
+ } catch {
90
+ return { pid, startTicks: null, cmdline: null, complete: false };
91
+ }
92
+ const trimmed = out.replace(/\n$/, '');
93
+ const m = /^(\w{3}\s+\w{3}\s+\d{1,2}\s+\d{2}:\d{2}:\d{2}\s+\d{4})\s(.*)$/.exec(trimmed);
94
+ if (!m) return { pid, startTicks: null, cmdline: null, complete: false };
95
+ const startTicks = parseLstart(m[1]);
96
+ const cmdline = m[2].trim() || null;
97
+ return { pid, startTicks, cmdline, complete: startTicks !== null && cmdline !== null };
98
+ }
99
+
100
+ /** identity(pid) → {pid, startTicks, cmdline, complete}. Never throws. */
101
+ function identity(pid) {
102
+ if (!Number.isInteger(pid) || pid <= 0) {
103
+ return { pid, startTicks: null, cmdline: null, complete: false };
104
+ }
105
+ try {
106
+ return hasProc() ? identityFromProc(pid) : identityFromPs(pid);
107
+ } catch {
108
+ return { pid, startTicks: null, cmdline: null, complete: false };
109
+ }
110
+ }
111
+
112
+ /**
113
+ * isDifferentProcess(recorded, live) — true ONLY when both sides are a
114
+ * COMPLETE identity and differ. Every other case (missing side, incomplete
115
+ * side, a legacy record with no identity fields) is "not different", i.e.
116
+ * today's pid-only behaviour — the fail-closed default.
117
+ */
118
+ function isDifferentProcess(recorded, live) {
119
+ if (!recorded || !live) return false;
120
+ if (!recorded.complete || !live.complete) return false;
121
+ return recorded.pid !== live.pid
122
+ || recorded.startTicks !== live.startTicks
123
+ || recorded.cmdline !== live.cmdline;
124
+ }
125
+
126
+ module.exports = { identity, isDifferentProcess };
@@ -0,0 +1,98 @@
1
+ /**
2
+ * procName.cjs — make Session Manager's spawned processes distinguishable in
3
+ * GNOME System Monitor / ps / htop.
4
+ *
5
+ * - aliasBinFor(realBin, alias): the kernel sets `comm` (the "Process Name"
6
+ * column) to the basename of the path handed to execve, capped at 15 chars
7
+ * (TASK_COMM_LEN 16 incl. NUL). argv0 does NOT affect it, so we exec a
8
+ * symlink named `alias` that points at the real binary.
9
+ * - smArgv0(role, detail): a bounded argv0 label for /proc/pid/cmdline. It
10
+ * ALWAYS contains `claude` as a word — /\bclaude\b/ gates killOrphanClaudePid
11
+ * (scheduler.cjs), claudePidAlive and findLiveProcessForJob (reaperHelpers.cjs).
12
+ *
13
+ * Fail-open is absolute: any error returns the ORIGINAL bin. Naming must never
14
+ * stop a job from launching.
15
+ *
16
+ * DELIBERATE exceptions to CLAUDE.md laws (do not "fix"):
17
+ * - The tmp+rename symlink primitive below is NOT config.cjs writeJson /
18
+ * writeTextAtomic: those write file contents and cannot create symlinks.
19
+ * - procnamesRoot() is intentionally NOT registered with config.cjs validateWrite /
20
+ * addAllowedRoot: that containment governs IPC-reachable writes; this is a
21
+ * main-process-internal write.
22
+ *
23
+ * Not to be confused with procIdentity.cjs (pid-recycling fingerprint).
24
+ */
25
+ 'use strict';
26
+
27
+ const fs = require('node:fs');
28
+ const path = require('node:path');
29
+
30
+ const { procnamesRoot } = require('./schedulerPaths.cjs');
31
+ const MAX_ALIAS_LEN = 15; // Linux TASK_COMM_LEN (16) minus NUL
32
+ const MAX_LABEL_LEN = 120;
33
+
34
+ let tmpSeq = 0;
35
+
36
+ function resolveBare(bin) {
37
+ if (path.isAbsolute(bin)) return bin;
38
+ try {
39
+ // Lazy: openExternalApp requires 'electron', unavailable outside the app.
40
+ const { findCommand } = require('./openExternalApp.cjs');
41
+ const found = findCommand(bin);
42
+ return found && path.isAbsolute(found) ? found : null;
43
+ } catch { return null; }
44
+ }
45
+
46
+ /** O(1) fs ops. Returns alias path on success, else `realBin` unchanged. */
47
+ function aliasBinFor(realBin, alias) {
48
+ if (typeof alias !== 'string' || !alias || alias.length > MAX_ALIAS_LEN || /[\\/\0]/.test(alias) || alias === '.' || alias === '..') {
49
+ throw new Error(`procName: alias "${alias}" must be 1-${MAX_ALIAS_LEN} chars (Linux TASK_COMM_LEN) with no path separators`);
50
+ }
51
+ try {
52
+ if (typeof realBin !== 'string' || !realBin) return realBin;
53
+ const target = resolveBare(realBin);
54
+ if (!target) return realBin;
55
+ const aliasPath = path.join(procnamesRoot(), alias);
56
+ let current = null;
57
+ try { current = fs.readlinkSync(aliasPath); } catch { /* absent or not a link */ }
58
+ if (current !== target) {
59
+ fs.mkdirSync(procnamesRoot(), { recursive: true });
60
+ const tmp = path.join(procnamesRoot(), `.${alias}.${process.pid}.${Date.now()}.${tmpSeq++}.tmp`);
61
+ try {
62
+ fs.symlinkSync(target, tmp);
63
+ fs.renameSync(tmp, aliasPath); // atomic replace; never unlink-then-symlink
64
+ } catch (err) {
65
+ try { fs.unlinkSync(tmp); } catch { /* nothing to clean */ }
66
+ if (err && err.code !== 'EEXIST') throw err; // losing racer: swallow
67
+ }
68
+ }
69
+ fs.accessSync(aliasPath, fs.constants.X_OK); // noexec / dangling probe
70
+ return aliasPath;
71
+ } catch {
72
+ return realBin;
73
+ }
74
+ }
75
+
76
+ /** `sm-claude-<role>:<detail>`, sanitised to [A-Za-z0-9._-], <= 120 chars. Always contains word `claude`. */
77
+ function smArgv0(role, detail) {
78
+ const clean = (v) => String(v == null ? '' : v).replace(/[^A-Za-z0-9._-]/g, '');
79
+ const r = clean(role) || 'proc';
80
+ const d = clean(detail);
81
+ return `sm-claude-${r}${d ? `:${d}` : ''}`.slice(0, MAX_LABEL_LEN);
82
+ }
83
+
84
+ /** Remove alias entries whose target no longer resolves to an executable. Never throws. O(entries). */
85
+ function pruneStaleAliases() {
86
+ try {
87
+ for (const name of fs.readdirSync(procnamesRoot())) {
88
+ const p = path.join(procnamesRoot(), name);
89
+ try {
90
+ fs.accessSync(p, fs.constants.X_OK);
91
+ } catch {
92
+ try { fs.unlinkSync(p); } catch { /* best effort */ }
93
+ }
94
+ }
95
+ } catch { /* root absent/unreadable */ }
96
+ }
97
+
98
+ module.exports = { aliasBinFor, smArgv0, procnamesRoot, pruneStaleAliases };