claude-code-session-manager 0.87.0 → 0.88.1

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 (291) hide show
  1. package/README.md +26 -53
  2. package/dist/assets/{AgentLibrary-DyLWzZDf.js → AgentLibrary-kQF8_wAw.js} +1 -1
  3. package/dist/assets/{DataModel--mISIJ6h.js → DataModel-CY_mMnsr.js} +1 -1
  4. package/dist/assets/{History-C2ahUXTg.js → History-Sji2brb0.js} +1 -1
  5. package/dist/assets/{Hooks-BiC6oyR2.js → Hooks-CtqpO-p5.js} +1 -1
  6. package/dist/assets/{HostBilko-BPleEOld.js → HostBilko-DN0ydOCr.js} +1 -1
  7. package/dist/assets/{Library-Dc8Qst1R.js → Library-CoiLZTKh.js} +1 -1
  8. package/dist/assets/{ListDetail-DIXh-OLX.js → ListDetail-CmqiQ_io.js} +1 -1
  9. package/dist/assets/{MarkdownEditor-C90bkLXK.js → MarkdownEditor-HPoNeKhT.js} +1 -1
  10. package/dist/assets/{McpServers-DqcbLOLZ.js → McpServers-Bj1X3_NT.js} +1 -1
  11. package/dist/assets/{Memory-CW62MXlh.js → Memory-1PKjsM3b.js} +1 -1
  12. package/dist/assets/{Panel-Bw1FhRuF.js → Panel-I1s6ghPo.js} +1 -1
  13. package/dist/assets/{Permissions-BcUC-5y8.js → Permissions-BhiiZjHQ.js} +1 -1
  14. package/dist/assets/{Plugins-BnKx9flD.js → Plugins-gv3_fkPX.js} +2 -2
  15. package/dist/assets/{ProvenanceBadge-Bw5vNVPT.js → ProvenanceBadge-BGcgBd8V.js} +1 -1
  16. package/dist/assets/{SaveBar-CWr0O_w-.js → SaveBar-BGr9eZ4g.js} +1 -1
  17. package/dist/assets/{Scheduler-DYdLuUqq.js → Scheduler-Bdo_awPq.js} +7 -7
  18. package/dist/assets/{ScopeSwitcher-CrBLbg8s.js → ScopeSwitcher-ukl0qWaA.js} +1 -1
  19. package/dist/assets/{Settings-DluB-vN1.js → Settings-DJkFj22G.js} +1 -1
  20. package/dist/assets/{SkillReferenceGraph-CHLSseay.js → SkillReferenceGraph-0t1Qblp6.js} +1 -1
  21. package/dist/assets/{Skills-gNdo_HNK.js → Skills-CeVdBGE-.js} +1 -1
  22. package/dist/assets/{SystemPrompt-Cru05-Ia.js → SystemPrompt-CfKvNHi3.js} +1 -1
  23. package/dist/assets/{TagLibrary-DNHY0xou.js → TagLibrary-DAzNaUPd.js} +1 -1
  24. package/dist/assets/{TiptapBody-I4lmbCgP.js → TiptapBody--S_rGXoA.js} +1 -1
  25. package/dist/assets/{Toggle-bWMHjmRh.js → Toggle-Dszm0xnp.js} +1 -1
  26. package/dist/assets/{index-DV3PorRY.css → index-BHTX4OTc.css} +1 -1
  27. package/dist/assets/{index-fc_JjdxL.js → index-Dp4Rc--q.js} +356 -356
  28. package/dist/assets/{settingsSchema-BfhtZnGD.js → settingsSchema-BLbssMYo.js} +1 -1
  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 +10 -13
  40. package/scripts/README.md +59 -0
  41. package/scripts/audit-ops-hygiene.cjs +350 -0
  42. package/scripts/hooks/guard-destructive-git.cjs +10 -34
  43. package/scripts/hooks/guard-inline-implementation.cjs +14 -8
  44. package/scripts/hooks/guard-prd-writes.cjs +7 -36
  45. package/scripts/hooks/guard-self-schedule.cjs +175 -0
  46. package/scripts/ops-sweep.cjs +355 -0
  47. package/scripts/scheduler-mcp-server.cjs +28 -71
  48. package/src/main/__tests__/bilkoHost-integration.test.cjs +3 -3
  49. package/src/main/__tests__/chat-cancel-terminal.test.cjs +6 -9
  50. package/src/main/__tests__/chat-exit-close-race.test.cjs +3 -3
  51. package/src/main/__tests__/chat-mcp-consent-notice.test.cjs +4 -5
  52. package/src/main/__tests__/chat-queue.test.cjs +2 -2
  53. package/src/main/__tests__/chat-stop-signal.test.cjs +2 -2
  54. package/src/main/__tests__/dep-orphan-archive-health.test.cjs +77 -0
  55. package/src/main/__tests__/dod-batchkey.test.cjs +2 -2
  56. package/src/main/__tests__/dod-drain-hook.test.cjs +2 -2
  57. package/src/main/__tests__/dod-report.test.cjs +2 -2
  58. package/src/main/__tests__/dod-reverify.test.cjs +2 -2
  59. package/src/main/__tests__/epicMint.test.cjs +2 -2
  60. package/src/main/__tests__/exchanges.test.cjs +2 -2
  61. package/src/main/__tests__/extractJson.test.cjs +2 -2
  62. package/src/main/__tests__/files-reject-credentials.test.cjs +1 -1
  63. package/src/main/__tests__/fixtures/1218-fo-01-move-scripts-lib-into-src-main-lib.log +556 -0
  64. package/src/main/__tests__/health-build-freshness.test.cjs +39 -0
  65. package/src/main/__tests__/health-delegation-chain.test.cjs +15 -1
  66. package/src/main/__tests__/health-queue-dispatch.test.cjs +58 -7
  67. package/src/main/__tests__/health-tick-liveness.test.cjs +27 -3
  68. package/src/main/__tests__/health-usage-poller.test.cjs +70 -23
  69. package/src/main/__tests__/historyRollup.test.cjs +2 -2
  70. package/src/main/__tests__/kg-augment.test.cjs +2 -2
  71. package/src/main/__tests__/mcpStatus.test.cjs +2 -2
  72. package/src/main/__tests__/memoryAggregate.test.cjs +1 -1
  73. package/src/main/__tests__/memoryStale.test.cjs +1 -1
  74. package/src/main/__tests__/opsErrorLogTelemetryTap.test.cjs +25 -1
  75. package/src/main/__tests__/pollLoop-dispatch-on-failure.test.cjs +33 -3
  76. package/src/main/__tests__/prd-group-allocator.test.cjs +2 -2
  77. package/src/main/__tests__/prdAdminRouteParity.test.cjs +2 -0
  78. package/src/main/__tests__/prdAdminRoutes.test.cjs +14 -2
  79. package/src/main/__tests__/prdAuthoringSeed.test.cjs +39 -0
  80. package/src/main/__tests__/prdLocationsArchived.test.cjs +20 -13
  81. package/src/main/__tests__/proc-role-env.test.cjs +125 -0
  82. package/src/main/__tests__/procname-claude-spawn-sites.test.cjs +304 -0
  83. package/src/main/__tests__/procname-sm-processes.test.cjs +127 -0
  84. package/src/main/__tests__/projectHomeAdminRoutes.test.cjs +81 -401
  85. package/src/main/__tests__/projectPages.test.cjs +63 -149
  86. package/src/main/__tests__/queue-health-verdict.test.cjs +9 -9
  87. package/src/main/__tests__/queue-starvation-dispatch-driver.test.cjs +117 -20
  88. package/src/main/__tests__/queueHistory.test.cjs +2 -2
  89. package/src/main/__tests__/rateLimitPollerStreak.test.cjs +38 -3
  90. package/src/main/__tests__/runVerify-landed-commit-outranks.test.cjs +181 -0
  91. package/src/main/__tests__/runVerify.test.cjs +5 -5
  92. package/src/main/__tests__/scheduleJobStatusDrift.test.cjs +3 -3
  93. package/src/main/__tests__/scheduleJobTransitions.test.cjs +2 -2
  94. package/src/main/__tests__/scheduler-adopted-run-supervision.test.cjs +143 -0
  95. package/src/main/__tests__/scheduler-autofix-select.test.cjs +2 -2
  96. package/src/main/__tests__/scheduler-autopromote.test.cjs +2 -2
  97. package/src/main/__tests__/scheduler-bash-timeout-env.test.cjs +3 -5
  98. package/src/main/__tests__/scheduler-boot-orphans.test.cjs +78 -97
  99. package/src/main/__tests__/scheduler-default-eligible-heal.test.cjs +161 -0
  100. package/src/main/__tests__/scheduler-dispatch-loop.test.cjs +58 -0
  101. package/src/main/__tests__/scheduler-epic-digest.test.cjs +3 -5
  102. package/src/main/__tests__/scheduler-force-tick-outcome.test.cjs +2 -2
  103. package/src/main/__tests__/scheduler-gate-shadow.test.cjs +119 -0
  104. package/src/main/__tests__/scheduler-guard-verdict-autoresolve.test.cjs +46 -0
  105. package/src/main/__tests__/scheduler-heartbeat-payload.test.cjs +80 -0
  106. package/src/main/__tests__/scheduler-inplace-salvage.test.cjs +25 -18
  107. package/src/main/__tests__/scheduler-investigation-prompt.test.cjs +4 -4
  108. package/src/main/__tests__/scheduler-launch-failure.test.cjs +3 -5
  109. package/src/main/__tests__/scheduler-looks-done.test.cjs +26 -7
  110. package/src/main/__tests__/scheduler-manual-pause.test.cjs +118 -0
  111. package/src/main/__tests__/scheduler-meta-code-sha.test.cjs +26 -3
  112. package/src/main/__tests__/scheduler-prd-missing-skip.test.cjs +17 -2
  113. package/src/main/__tests__/scheduler-prd-persona-spawn.test.cjs +3 -5
  114. package/src/main/__tests__/scheduler-quiet-machine-lease.test.cjs +41 -6
  115. package/src/main/__tests__/scheduler-rate-limit-pause.test.cjs +62 -6
  116. package/src/main/__tests__/scheduler-rate-limit-spin-guard.test.cjs +3 -5
  117. package/src/main/__tests__/scheduler-reap-dead-running-jobs.test.cjs +23 -4
  118. package/src/main/__tests__/scheduler-reconcile-cwd-preserve.test.cjs +100 -0
  119. package/src/main/__tests__/scheduler-reconcile-invalid-repair.test.cjs +3 -3
  120. package/src/main/__tests__/scheduler-reconcile-quarantine.test.cjs +4 -4
  121. package/src/main/__tests__/scheduler-shard-quarantine.test.cjs +110 -0
  122. package/src/main/__tests__/scheduler-shared-tree-guard.test.cjs +81 -1
  123. package/src/main/__tests__/scheduler-stall-per-project.test.cjs +14 -0
  124. package/src/main/__tests__/scheduler-starve-escalation.test.cjs +4 -4
  125. package/src/main/__tests__/scheduler-stranded-autofix-park.test.cjs +245 -0
  126. package/src/main/__tests__/scheduler-supervisor-record.test.cjs +81 -0
  127. package/src/main/__tests__/scheduler-tick-cancel-token.test.cjs +2 -2
  128. package/src/main/__tests__/scheduler-tick-wedge.test.cjs +172 -0
  129. package/src/main/__tests__/scheduler-transient-failure.test.cjs +2 -2
  130. package/src/main/__tests__/scheduler-worktree-cap-defer.test.cjs +37 -11
  131. package/src/main/__tests__/scheduler-worktree-exec-cwd.test.cjs +3 -5
  132. package/src/main/__tests__/usageSingleFlight.test.cjs +157 -0
  133. package/src/main/__tests__/workTypeLibrary.test.cjs +1 -1
  134. package/src/main/bilkoHost.cjs +16 -7
  135. package/src/main/build-info.json +8 -0
  136. package/src/main/chatRunner.cjs +4 -2
  137. package/src/main/config.cjs +2 -3
  138. package/src/main/docEdit.cjs +4 -2
  139. package/src/main/health.cjs +276 -52
  140. package/src/main/heapSnapshot.cjs +2 -2
  141. package/src/main/historyAggregator.cjs +1 -1
  142. package/src/main/index.cjs +61 -10
  143. package/src/main/ipcSchemas.cjs +6 -17
  144. package/src/main/lib/__tests__/auditLog.test.cjs +38 -0
  145. package/src/main/lib/__tests__/buildIdentity.test.cjs +121 -0
  146. package/src/main/lib/__tests__/cwdClassify.test.cjs +111 -0
  147. package/src/main/lib/__tests__/definitionOfDoneSequence.test.cjs +95 -0
  148. package/src/main/lib/__tests__/delegationReadiness.test.cjs +1 -1
  149. package/src/main/lib/__tests__/dispatchLoop.test.cjs +63 -0
  150. package/src/main/lib/__tests__/gateFixtures.json +20 -0
  151. package/src/main/lib/__tests__/gitWorktree.test.cjs +8 -1
  152. package/src/main/lib/__tests__/instanceLock.test.cjs +93 -7
  153. package/src/main/lib/__tests__/jobSupervisorRecord.test.cjs +78 -0
  154. package/src/main/lib/__tests__/jobWorktreeBootLive.test.cjs +11 -0
  155. package/src/main/lib/__tests__/localAdminHttp.test.cjs +1 -1
  156. package/src/main/lib/__tests__/mcpToolCatalog.test.cjs +6 -1
  157. package/src/main/lib/__tests__/procIdentity.test.cjs +119 -0
  158. package/src/main/lib/__tests__/procName.test.cjs +92 -0
  159. package/src/main/lib/__tests__/queueStoreMachineStateRecovery.test.cjs +67 -0
  160. package/src/main/lib/__tests__/schedulerMcpServerHelp.test.cjs +6 -7
  161. package/src/main/lib/__tests__/schedulerMcpServerProjectHome.test.cjs +37 -204
  162. package/src/main/lib/__tests__/schedulerPaths.test.cjs +226 -0
  163. package/src/main/lib/__tests__/schedulerPathsWorktree.test.cjs +94 -0
  164. package/src/main/lib/__tests__/schedulerRuntimeState.test.cjs +56 -0
  165. package/src/main/lib/__tests__/sessionSlots.test.cjs +45 -0
  166. package/src/main/lib/__tests__/telemetryBacklog.test.cjs +1 -1
  167. package/src/main/lib/__tests__/upgradeDrain.test.cjs +130 -0
  168. package/src/main/lib/__tests__/usageCircuit.test.cjs +61 -0
  169. package/src/main/lib/__tests__/watchdog-helpers.test.cjs +63 -0
  170. package/src/main/lib/__tests__/watchdog-relaunch.test.cjs +73 -0
  171. package/src/main/lib/activeIndexRebuild.cjs +1 -3
  172. package/src/main/lib/activeSessions.cjs +20 -88
  173. package/src/main/lib/adoptedRunSupervisor.cjs +136 -0
  174. package/src/main/lib/agentModelResolve.cjs +33 -1
  175. package/src/main/lib/agentPersonaSchema.cjs +2 -2
  176. package/src/main/lib/auditLog.cjs +30 -5
  177. package/src/main/lib/buildIdentity.cjs +113 -0
  178. package/src/main/lib/classifyPromptTicket.cjs +3 -2
  179. package/src/main/lib/claudeBin.cjs +37 -2
  180. package/src/main/lib/cleanEnv.cjs +27 -1
  181. package/src/main/lib/credentials.cjs +4 -2
  182. package/src/main/lib/cwdClassify.cjs +185 -0
  183. package/src/main/lib/definitionOfDone.cjs +296 -52
  184. package/src/main/lib/dispatchLoop.cjs +40 -0
  185. package/src/main/lib/effectiveModelInfo.cjs +9 -10
  186. package/src/main/lib/ephemeralCwd.cjs +7 -28
  187. package/src/main/lib/gitWorktree.cjs +95 -8
  188. package/src/main/lib/guardShims.cjs +3 -3
  189. package/src/main/lib/historyRollup.cjs +6 -7
  190. package/src/main/lib/instanceLock.cjs +32 -5
  191. package/src/main/lib/jobSupervisorRecord.cjs +147 -0
  192. package/src/main/lib/jobWorktreeBootLive.cjs +9 -5
  193. package/src/main/lib/localAdminHttp.cjs +9 -27
  194. package/src/main/lib/mcpToolCatalog.cjs +29 -77
  195. package/src/main/lib/opsOwnership.cjs +27 -19
  196. package/src/main/lib/prdAuthoringSeed.cjs +36 -0
  197. package/src/main/lib/prdLocations.cjs +48 -1
  198. package/src/main/lib/procIdentity.cjs +126 -0
  199. package/src/main/lib/procName.cjs +98 -0
  200. package/src/main/lib/projectHomeAdminRoutes.cjs +56 -327
  201. package/src/main/lib/queueHistory.cjs +8 -7
  202. package/src/main/lib/queueStore.cjs +57 -37
  203. package/src/main/lib/quietMachineLease.cjs +19 -2
  204. package/src/main/lib/reservationExpiry.cjs +30 -0
  205. package/src/main/lib/runClaudeP.cjs +4 -2
  206. package/src/main/lib/runLogRetention.cjs +3 -2
  207. package/src/main/lib/scheduleJobSchema.cjs +2 -2
  208. package/src/main/lib/scheduleJobTransitions.cjs +29 -2
  209. package/src/main/lib/schedulerBatch.cjs +27 -2
  210. package/src/main/lib/schedulerPaths.cjs +175 -0
  211. package/src/main/lib/schedulerRuntimeState.cjs +59 -0
  212. package/src/main/lib/sessionSlots.cjs +47 -9
  213. package/src/main/lib/smProcNames.cjs +51 -0
  214. package/src/main/lib/upgradeDrain.cjs +188 -0
  215. package/src/main/lib/usageCircuit.cjs +53 -6
  216. package/src/main/lib/watchdogHelpers.cjs +70 -37
  217. package/src/main/lib/withTimeout.cjs +33 -0
  218. package/src/main/mcpStatus.cjs +4 -2
  219. package/src/main/pluginInstall.cjs +6 -2
  220. package/src/main/projectPages.cjs +41 -145
  221. package/src/main/pty.cjs +8 -1
  222. package/src/main/queueOps.cjs +10 -10
  223. package/src/main/runVerify.cjs +69 -3
  224. package/src/main/scheduler.cjs +1607 -383
  225. package/src/main/seedAgentPersonas.cjs +1 -1
  226. package/src/main/seedDevPlugin.cjs +1 -1
  227. package/src/main/seedSchedulerMcp.cjs +7 -6
  228. package/src/main/seedStatus.cjs +1 -1
  229. package/src/main/supervisor.cjs +5 -3
  230. package/src/main/usage.cjs +126 -62
  231. package/src/preload/api.d.ts +18 -64
  232. package/src/preload/index.cjs +2 -4
  233. package/src/seed/agents/project-home-builder.md +31 -48
  234. package/screenshots/.gitkeep +0 -0
  235. package/screenshots/README-screenshots.md +0 -13
  236. package/src/main/lib/projectPageSummarySchema.cjs +0 -181
  237. package/src/main/teams.cjs +0 -95
  238. package/src/main/templates/project-pages-catalog.json +0 -741
  239. package/src/main/templates/project-pages-default-home.html +0 -123
  240. package/src/main/templates/project-pages-pipeline.md +0 -417
  241. package/src/seed/prompts/code-review/ac-coverage-check.md +0 -8
  242. package/src/seed/prompts/code-review/correctness-only.md +0 -8
  243. package/src/seed/prompts/code-review/full-spectrum-high.md +0 -8
  244. package/src/seed/prompts/code-review/hallucination-check.md +0 -8
  245. package/src/seed/prompts/code-review/public-api-compat.md +0 -8
  246. package/src/seed/prompts/code-review/readability-naming.md +0 -8
  247. package/src/seed/prompts/debugging/bug-as-failing-test.md +0 -8
  248. package/src/seed/prompts/debugging/git-bisect-regression.md +0 -8
  249. package/src/seed/prompts/debugging/instrument-intermittent-bug.md +0 -8
  250. package/src/seed/prompts/debugging/localize-pipeline-failure.md +0 -8
  251. package/src/seed/prompts/debugging/reproduce-then-diagnose.md +0 -8
  252. package/src/seed/prompts/documentation/adr-from-change.md +0 -8
  253. package/src/seed/prompts/documentation/module-readme.md +0 -8
  254. package/src/seed/prompts/documentation/onboarding-plan.md +0 -8
  255. package/src/seed/prompts/documentation/refresh-claude-md.md +0 -8
  256. package/src/seed/prompts/documentation/tsdoc-public-exports.md +0 -8
  257. package/src/seed/prompts/git-pr/conventional-commit.md +0 -8
  258. package/src/seed/prompts/git-pr/draft-pr-title-body.md +0 -8
  259. package/src/seed/prompts/git-pr/pre-commit-safety-sweep.md +0 -8
  260. package/src/seed/prompts/git-pr/release-notes-block.md +0 -8
  261. package/src/seed/prompts/git-pr/split-large-pr.md +0 -8
  262. package/src/seed/prompts/performance/bundle-startup-audit.md +0 -8
  263. package/src/seed/prompts/performance/complexity-audit.md +0 -8
  264. package/src/seed/prompts/performance/cpu-profile-hot-path.md +0 -8
  265. package/src/seed/prompts/performance/db-query-plan-review.md +0 -8
  266. package/src/seed/prompts/performance/memory-leak-hunt.md +0 -8
  267. package/src/seed/prompts/qa/api-contract-tests.md +0 -8
  268. package/src/seed/prompts/qa/e2e-critical-path.md +0 -8
  269. package/src/seed/prompts/qa/failing-test-for-bug.md +0 -8
  270. package/src/seed/prompts/qa/find-missing-test-coverage.md +0 -8
  271. package/src/seed/prompts/qa/stabilize-flaky-test.md +0 -8
  272. package/src/seed/prompts/qa/tdd-red-first.md +0 -8
  273. package/src/seed/prompts/qa/visual-regression-review.md +0 -8
  274. package/src/seed/prompts/qa/wcag-axe-scan.md +0 -8
  275. package/src/seed/prompts/refactoring/dead-code-sweep.md +0 -8
  276. package/src/seed/prompts/refactoring/extract-duplicated-pattern.md +0 -8
  277. package/src/seed/prompts/refactoring/modernize-legacy-file.md +0 -8
  278. package/src/seed/prompts/refactoring/reduce-cyclomatic-complexity.md +0 -8
  279. package/src/seed/prompts/refactoring/tighten-module-boundaries.md +0 -8
  280. package/src/seed/prompts/security/authz-audit.md +0 -8
  281. package/src/seed/prompts/security/crypto-correctness.md +0 -8
  282. package/src/seed/prompts/security/cwe-top-25-hunt.md +0 -8
  283. package/src/seed/prompts/security/dependency-audit.md +0 -8
  284. package/src/seed/prompts/security/ipc-boundary-hardening.md +0 -8
  285. package/src/seed/prompts/security/owasp-top-10-staged-diff.md +0 -8
  286. package/src/seed/prompts/security/secret-credential-scan.md +0 -8
  287. package/web/README.md +0 -41
  288. package/web/project-pages/logic/dist/logic.cjs +0 -4709
  289. package/web/project-pages/render.cjs +0 -70
  290. package/web/project-pages/renderer/dist/renderer.cjs +0 -18900
  291. package/web/project-pages/validate-summary.cjs +0 -62
@@ -47,6 +47,24 @@ const MCP_TOOL_CATALOG = [
47
47
  exampleArgs: { slug: 'add-mcp-tool-catalog', force: false },
48
48
  notes: 'Refuses a job whose status is already "completed" unless force:true is passed — resetting a completed job re-executes already-shipped work.',
49
49
  },
50
+ {
51
+ name: 'scheduler_pause',
52
+ group: 'scheduler',
53
+ purpose: "Pause the scheduler (user-initiated) via the session-manager app's admin API.",
54
+ whenToUse: 'Use to stop NEW job dispatch on request (e.g. before a risky change). Running jobs are never killed.',
55
+ whenNotToUse: 'Do not use to cancel a running job (use scheduler_cancel_job) or as a workaround for a stuck queue.',
56
+ exampleArgs: {},
57
+ notes: 'A manual pause survives app restart and is never auto-cleared by the rate-limit timer or network recovery — only scheduler_resume clears it.',
58
+ },
59
+ {
60
+ name: 'scheduler_resume',
61
+ group: 'scheduler',
62
+ purpose: "Clear any scheduler pause (manual or automatic) via the session-manager app's admin API.",
63
+ whenToUse: 'Use to restart dispatch after scheduler_pause, or to lift an auto-pause you have judged stale.',
64
+ whenNotToUse: 'Do not use to retry a parked needs_review job — that is scheduler_reset_job.',
65
+ exampleArgs: {},
66
+ notes: null,
67
+ },
50
68
  {
51
69
  name: 'scheduler_list_jobs',
52
70
  group: 'scheduler',
@@ -224,65 +242,18 @@ const MCP_TOOL_CATALOG = [
224
242
  + 'delivered.',
225
243
  },
226
244
  {
227
- name: 'project_home_get_contract',
228
- group: 'project-home',
229
- purpose: "Fetch the self-sufficient contract for generating this project's Project Home pages via the "
230
- + "session-manager app's admin API — the protocol steps, the ProjectPageSummary/ProjectPagePicks JSON "
231
- + 'schemas, the component catalog (all 5 lenses: home, marketing, feature, architecture, brief), the '
232
- + 'absolute output paths, and the full pipeline spec text. Nothing in the response requires reading '
233
- + "session-manager's own repo — this is what makes generation work on a machine with only the npm "
234
- + 'package installed.',
235
- whenToUse: 'Call this FIRST, before composing any ProjectPageSummary content or picking variants, whenever '
236
- + 'a project-home-builder session starts generating Project Home pages for a project.',
237
- whenNotToUse: 'Do not begin composing a ProjectPageSummary or choosing variants before calling this — it '
238
- + 'must be the FIRST call a project-home-builder session makes; every other project_home_* tool assumes '
239
- + "you already have this response's schema/catalog/paths in hand.",
240
- exampleArgs: { cwd: '/home/bilko/Projects/session-manager' },
241
- notes: "cwd is optional — defaults to the calling session's own project root (SM_PROJECT_ROOT or process.cwd()) when omitted.",
242
- },
243
- {
244
- name: 'project_home_validate_summary',
245
- group: 'project-home',
246
- purpose: 'Validate a composed ProjectPageSummary against the schema from project_home_get_contract via the '
247
- + "session-manager app's admin API, without writing anything to disk.",
248
- whenToUse: 'Use after composing (or editing) a ProjectPageSummary and before calling project_home_render — '
249
- + 'fix every returned {field, message} error and re-validate until valid:true.',
250
- whenNotToUse: 'Do not skip straight to project_home_render on an unvalidated summary — render does '
251
- + 're-validate server-side and rejects with no writes on failure, but catching errors here first saves a '
252
- + 'round trip and gives per-field detail sooner.',
253
- exampleArgs: { cwd: '/home/bilko/Projects/session-manager', summary: { title: 'Example Project' } },
254
- notes: "cwd is optional — defaults to the calling session's own project root (SM_PROJECT_ROOT or process.cwd()) when omitted.",
255
- },
256
- {
257
- name: 'project_home_render',
258
- group: 'project-home',
259
- purpose: 'Render and write all 5 Project Home lenses (home, marketing, feature, architecture, brief) plus '
260
- + "summary.json/picks.json/manifest.json via the session-manager app's admin API — the only write path "
261
- + 'for Project Home generation.',
262
- whenToUse: 'Use once project_home_validate_summary reports valid:true and a variant has been picked for '
263
- + "every lens/slot in the contract's catalog.",
264
- whenNotToUse: 'Must NOT be used to fabricate summary content — every field of the summary passed here must '
265
- + 'trace to something concrete about the real project (an Epic goal, a source file/dir, a convention, a '
266
- + 'git log entry). This tool re-validates the summary server-side and rejects with no writes on schema '
267
- + 'failure, but it only checks shape, never truthfulness.',
268
- exampleArgs: {
269
- cwd: '/home/bilko/Projects/session-manager',
270
- summary: { title: 'Example Project' },
271
- picks: { home: { hero: 'variant-a' } },
272
- },
273
- notes: "cwd is optional — defaults to the calling session's own project root (SM_PROJECT_ROOT or process.cwd()) when omitted.",
274
- },
275
- {
276
- name: 'project_home_status',
245
+ name: 'project_home_write',
277
246
  group: 'project-home',
278
- purpose: "Report what already exists for this project's Project Home generation via the session-manager "
279
- + "app's admin API — summary.json/picks.json/each lens file's existence + mtime, and manifest.json's "
280
- + 'generatedAt.',
281
- whenToUse: 'Use before starting generation to see prior state, and again after project_home_render to '
282
- + 'confirm the new files landed.',
283
- whenNotToUse: 'Do not treat an all-absent result as an error — a project that has never generated a '
284
- + 'Project Home page yet still returns a well-formed status with every exists:false rather than failing.',
285
- exampleArgs: { cwd: '/home/bilko/Projects/session-manager' },
247
+ purpose: "Write this project's Project Home page — ONE self-contained HTML document — to "
248
+ + "session-manager-operations/project-pages/home.html via the session-manager app's admin API. "
249
+ + 'The only write path for Project Home.',
250
+ whenToUse: 'Use when asked to generate or refresh the project home page. Compose the whole page yourself '
251
+ + '(inline <style>/<script>, data: URIs for assets) from what is really true about the project, then '
252
+ + 'pass the full document as `html`.',
253
+ whenNotToUse: 'Never fabricate content — every claim must trace to something concrete in the project. The '
254
+ + 'document must be self-contained: the call is rejected for empty input, more than 1MB, any '
255
+ + '<script src>, <link href="http…">, @import, or url(http…) — no network egress.',
256
+ exampleArgs: { cwd: '/home/bilko/Projects/session-manager', html: '<!DOCTYPE html><html><body><h1>Example</h1></body></html>' },
286
257
  notes: "cwd is optional — defaults to the calling session's own project root (SM_PROJECT_ROOT or process.cwd()) when omitted.",
287
258
  },
288
259
  {
@@ -335,25 +306,6 @@ const MCP_RECIPES = [
335
306
  'The call only delivers a PROPOSED session in the receiving project — nothing runs until a human there presses Approve & start; there is no reply channel.',
336
307
  ],
337
308
  },
338
- {
339
- id: 'generate-project-home',
340
- title: "Generate a project's Project Home pages",
341
- steps: [
342
- "Call project_home_get_contract to get the protocol, ProjectPageSummary/ProjectPagePicks schemas, "
343
- + 'component catalog, absolute output paths, and pipeline spec — the FIRST call any '
344
- + 'project-home-builder session makes.',
345
- 'Author a ProjectPageSummary matching the contract\'s summarySchema — every field must trace to '
346
- + 'something concrete about the real project; never fabricate content.',
347
- 'Call project_home_validate_summary with { cwd, summary } and fix every {field, message} error it '
348
- + 'returns, re-validating until valid:true.',
349
- "For each lens and slot in the contract's catalog, choose exactly one variant id by judging the "
350
- + 'candidate variants against the summary you composed, assembling a ProjectPagePicks object '
351
- + '(lensId -> slotId -> variantId).',
352
- 'Call project_home_render with { cwd, summary, picks } to render all 5 lenses and write '
353
- + 'summary.json/picks.json/output files.',
354
- "Call project_home_status to confirm the new files landed and read manifest.json's generatedAt.",
355
- ],
356
- },
357
309
  ];
358
310
 
359
311
  for (const entry of MCP_TOOL_CATALOG) {
@@ -48,17 +48,10 @@ const OWNERS = Object.freeze({
48
48
  'scheduler': 'scheduler',
49
49
  // Project Home owns the synthesized Brief (generate + hand-edit).
50
50
  'project-brief': 'project-home',
51
- // Project Home also owns the app-side render path for Project Pages
52
- // (summary.json/picks.json/output/* written via the
53
- // /admin/project-home/render route, config.cjs's writeJson). This is a
54
- // NARROWER claim than "project-home owns everything under this folder":
55
- // the SAME folder is also written directly by a project-home-builder
56
- // Epic's own Write tool, which never goes through config.cjs and so is
57
- // NOT governed by this table at all (see
58
- // session-manager-operations/project-pages/README.md). Declaring the
59
- // namespace here only closes the fail-closed gap for the app's own write
60
- // path — it does not claim exclusivity over the agent's Write-tool calls,
61
- // which remain outside this law by construction.
51
+ // Project Home owns project-pages/home.html, written only via the
52
+ // project_home_write MCP tool -> /admin/project-home/write route
53
+ // (config.cjs's writeTextAtomic). See
54
+ // session-manager-operations/project-pages/README.md.
62
55
  'project-pages': 'project-home',
63
56
  // Structured per-tab error log lines (JSONL), tagged for tracing/analysis.
64
57
  'logs': 'logs',
@@ -177,11 +170,13 @@ function assertOpsWrite(absPath, writer) {
177
170
  // worktree-cwd hazard (PRD 1082; incidents 2026-08-30, 2026-09-01).
178
171
  const { inOps } = parseOpsPath(absPath);
179
172
  if (inOps) {
180
- const segs = absPath.split(path.sep);
181
- const idx = segs.lastIndexOf(OPS_ROOT_DIR);
182
- const projectRoot = segs.slice(0, idx).join(path.sep) || path.sep;
183
- const { isEphemeralCwd } = require('./ephemeralCwd.cjs');
184
- if (isEphemeralCwd(projectRoot)) {
173
+ // innermostOpsRoot (lastIndexOf) is the fact THIS gate keys on; activeSessions
174
+ // truncates at the outermost instead — cwdClassify returns both, unified in
175
+ // neither direction.
176
+ const { classifyCwd } = require('./cwdClassify.cjs');
177
+ const projectRoot = path.dirname(classifyCwd(absPath).innermostOpsRoot);
178
+ const { kind, reason } = classifyCwd(projectRoot);
179
+ if (kind === 'ephemeral' || kind === 'worktree') {
185
180
  const err = new Error(
186
181
  `refusing to write ${OPS_ROOT_DIR}/ state under an ephemeral project root `
187
182
  + `(tmpdir or linked git worktree): "${projectRoot}" — see lib/opsOwnership.cjs resolveProjectRoot`,
@@ -189,6 +184,17 @@ function assertOpsWrite(absPath, writer) {
189
184
  err.ephemeral = true;
190
185
  throw err;
191
186
  }
187
+ if (kind === 'unknown') {
188
+ // Fail closed on the unprovable case (worktree .git file whose admin
189
+ // back-reference is gone, garbled .git file): the exact shape behind the
190
+ // lost-Epics incidents. WRITES only — reads and dispatch stay open.
191
+ const err = new Error(
192
+ `refusing to write ${OPS_ROOT_DIR}/ state under "${projectRoot}": project identity `
193
+ + `cannot be proven (${reason}) — see lib/cwdClassify.cjs`,
194
+ );
195
+ err.unknownCwd = true;
196
+ throw err;
197
+ }
192
198
  }
193
199
  }
194
200
 
@@ -236,10 +242,12 @@ function resolveProjectRoot(cwd, { opsInternal = 'normalize' } = {}) {
236
242
  `resolveProjectRoot: cwd must be a project root, not a path inside ${OPS_ROOT_DIR}/, got "${cwd}"`,
237
243
  );
238
244
  }
239
- // Lazy: activeSessions → gitWorktree → (lazily) config.cjs → this module.
240
- const { projectRootOf } = require('./activeSessions.cjs');
245
+ // Lazy: cwdClassify → schedulerPaths; ephemeralCwd → cwdClassify.
246
+ const { classifyCwd } = require('./cwdClassify.cjs');
241
247
  const { isEphemeralCwd } = require('./ephemeralCwd.cjs');
242
- const root = projectRootOf(cwd);
248
+ // `unknown` deliberately does NOT throw here: reads and dispatch must keep
249
+ // working; assertOpsWrite is the choke point that refuses the WRITE.
250
+ const root = classifyCwd(cwd).projectRoot;
243
251
  if (isEphemeralCwd(root)) {
244
252
  const err = new Error(
245
253
  `resolveProjectRoot: refusing ephemeral cwd (tmpdir or linked git worktree), got "${cwd}"`,
@@ -0,0 +1,36 @@
1
+ 'use strict';
2
+ // Seed-when-the-stamp-differs for the bundled PRD_AUTHORING.md guide.
3
+ // Pure fs + injected writer so it is testable without an Electron app.
4
+
5
+ const fs = require('fs');
6
+
7
+ const STAMP_RE = /^<!--\s*PRD_AUTHORING\.md\s+(v\d+)\s*-->/;
8
+
9
+ /** Version stamp from line 1 of `text`, or null when absent. */
10
+ function readStamp(text) {
11
+ const first = String(text).split(/\r?\n/, 1)[0];
12
+ const m = STAMP_RE.exec(first);
13
+ return m ? m[1] : null;
14
+ }
15
+
16
+ /** True when dest should be (re)written from src: missing/unstamped/differing stamp. */
17
+ function shouldSeed(srcText, destText) {
18
+ if (destText == null) return true;
19
+ const destStamp = readStamp(destText);
20
+ return destStamp === null || destStamp !== readStamp(srcText);
21
+ }
22
+
23
+ /**
24
+ * Overwrite `dest` with `src` when shouldSeed. `write(abs, text)` is the atomic
25
+ * writer (config.writeTextAtomic). Resolves true when it wrote, false otherwise.
26
+ */
27
+ async function seedAuthoringGuide({ src, dest, write }) {
28
+ if (!fs.existsSync(src)) return false;
29
+ const srcText = fs.readFileSync(src, 'utf8');
30
+ const destText = fs.existsSync(dest) ? fs.readFileSync(dest, 'utf8') : null;
31
+ if (!shouldSeed(srcText, destText)) return false;
32
+ await write(dest, srcText);
33
+ return true;
34
+ }
35
+
36
+ module.exports = { readStamp, shouldSeed, seedAuthoringGuide };
@@ -173,7 +173,8 @@ function resolvePrdWriteDir(cwd) {
173
173
  // archived-twin stale-queue-row guard (scheduler.cjs's archivedTwinExists)
174
174
  // calls listArchivedPrdDirs directly, uncached — so this is purely a
175
175
  // reporting-freshness bound (schedule:list-prds's per-Epic PRD/run counts).
176
- const ASSEMBLED_DIRS_CACHE_TTL_MS = 30_000;
176
+ // Above the 60 s dispatch-loop interval (scheduler POLL_INTERVAL_MS).
177
+ const ASSEMBLED_DIRS_CACHE_TTL_MS = 120_000;
177
178
  const assembledPrdsDirsCache = new Map(); // callerKey -> { freshnessKey, cachedAt, result }
178
179
  const assembledArchivedPrdsDirsCache = new Map();
179
180
 
@@ -215,6 +216,7 @@ function memoizedAssembledDirs(cache, maxAgeMin, opts, compute) {
215
216
  }
216
217
 
217
218
  const result = compute(allCwds, activeCwds);
219
+ if (Array.isArray(result)) Object.freeze(result);
218
220
  cache.set(callerKey, { freshnessKey, cachedAt: Date.now(), result });
219
221
  return result;
220
222
  }
@@ -338,6 +340,50 @@ function deriveEpicIdFromPrdPath(filePath) {
338
340
  return epicId;
339
341
  }
340
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
+
341
387
  module.exports = {
342
388
  resolvePrdWriteDir,
343
389
  resolvePrdsDirs,
@@ -347,6 +393,7 @@ module.exports = {
347
393
  listEpicPrdDirs,
348
394
  listArchivedPrdDirs,
349
395
  deriveEpicIdFromPrdPath,
396
+ deriveProjectCwdFromPrdPath,
350
397
  PRD_SUBPATH,
351
398
  EPICS_SUBPATH,
352
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 };