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
@@ -1,3 +1,8 @@
1
+ const { PROC_NAMES, setProcessTitle, inhibitHolderShell } = require('./lib/smProcNames.cjs');
2
+ // Self-describing `comm` in System Monitor (pn-03). process.argv is a JS copy
3
+ // made at startup, so the argv-memory overwrite below never affects the
4
+ // `process.argv.includes('--simple')` read further down.
5
+ setProcessTitle(PROC_NAMES.main);
1
6
  const { app, BrowserWindow, ipcMain, dialog, Menu, session, systemPreferences, globalShortcut, shell, clipboard, powerSaveBlocker, protocol } = require('electron');
2
7
  const { spawn, execFile, execFileSync } = require('node:child_process');
3
8
  const path = require('node:path');
@@ -39,8 +44,9 @@ const epicWorktreeProjectConfig = require('./lib/epicWorktreeProjectConfig.cjs')
39
44
  const agentLibrary = require('./agentLibrary.cjs');
40
45
  const agentModelResolve = require('./lib/agentModelResolve.cjs');
41
46
  const { resolveEffectiveModelInfo } = require('./lib/effectiveModelInfo.cjs');
42
- const { checkDelegationReadiness, ensureGuardsInstalled, installPrdWriteGuard, installDestructiveGitGuard, installInlineImplementationGuard } = require('./lib/delegationReadiness.cjs');
47
+ const { checkDelegationReadiness, ensureGuardsInstalled, installPrdWriteGuard, installDestructiveGitGuard, installInlineImplementationGuard, installSelfScheduleGuard } = require('./lib/delegationReadiness.cjs');
43
48
  const { resolveProjectContext } = require('./lib/projectRootResolve.cjs');
49
+ const upgradeDrain = require('./lib/upgradeDrain.cjs');
44
50
  const { writeGuardShims } = require('./lib/guardShims.cjs');
45
51
  const { MCP_TOOL_CATALOG, MCP_RECIPES } = require('./lib/mcpToolCatalog.cjs');
46
52
  const { sendIfAlive } = require('./lib/sendToRenderer.cjs');
@@ -58,7 +64,6 @@ chatRunner.registerAdminRoute(adminHttp);
58
64
  crossProjectFeedback.registerAdminRoute(adminHttp);
59
65
  const supervisor = require('./supervisor.cjs');
60
66
  const watchers = require('./watchers.cjs');
61
- const teams = require('./teams.cjs');
62
67
  const queueOps = require('./queueOps.cjs');
63
68
  const pluginInstall = require('./pluginInstall.cjs');
64
69
  const { seedDevPlugin } = require('./seedDevPlugin.cjs');
@@ -135,7 +140,10 @@ function startSystemdInhibit() {
135
140
  '--who=Claude Session Manager',
136
141
  '--why=Scheduler polling and claude -p jobs must survive idle',
137
142
  '--mode=block',
138
- 'sh', '-c', `while kill -0 ${process.pid} 2>/dev/null; do sleep 5; done`,
143
+ // sm-inhibit-hold: aliased shell so the permanent holder is attributable
144
+ // in System Monitor (its `sleep 5` child stays a bare `sleep`, parented
145
+ // under it). Falls back to plain /bin/sh if the alias can't be made.
146
+ inhibitHolderShell(), '-c', `while kill -0 ${process.pid} 2>/dev/null; do sleep 5; done`,
139
147
  ], { stdio: 'ignore', detached: false });
140
148
  child.on('error', (e) => {
141
149
  logs.writeLine({ scope: 'main', level: 'warn', message: 'systemd-inhibit spawn failed', meta: { error: e?.message } });
@@ -308,7 +316,26 @@ function relaunchViaNpx() {
308
316
  return child;
309
317
  }
310
318
 
311
- async function rebootApp() {
319
+ /**
320
+ * rebootApp({force}) — restart is a REQUEST that drains first and never kills
321
+ * (lib/upgradeDrain.cjs): dispatch pauses, running jobs finish, then the
322
+ * scheduler's heartbeat-driven drain calls performReboot(). `force: true`
323
+ * keeps the old immediate behaviour (kills running executors) for humans who
324
+ * accept the cost.
325
+ */
326
+ async function rebootApp({ force = false } = {}) {
327
+ if (force) {
328
+ logReboot('force reboot requested — immediate, not draining');
329
+ return performReboot();
330
+ }
331
+ const request = upgradeDrain.requestRestart({ reason: 'user-reboot', requestedBy: 'user' });
332
+ logReboot(`restart requested (drain first): ${request.requestedAt}`);
333
+ }
334
+
335
+ // Registered as the scheduler's drain-complete exit path (zero running jobs).
336
+ scheduler.setRestartHandler(() => performReboot());
337
+
338
+ async function performReboot() {
312
339
  ptyManager.killAll();
313
340
  configMgr.closeAllWatchers();
314
341
  closeAllProjectPagesWatchers();
@@ -343,6 +370,9 @@ async function rebootApp() {
343
370
  return;
344
371
  }
345
372
 
373
+ // Bounded-lifetime marker: the watchdog must not relaunch a second instance
374
+ // during this deliberate exit window (single-instance-lock race).
375
+ upgradeDrain.markRestarting();
346
376
  let childOk = false;
347
377
  try {
348
378
  const child = relaunchViaNpx();
@@ -588,12 +618,22 @@ function logGuardInstallAttempt(guard, cwd, outcome) {
588
618
  }
589
619
 
590
620
  async function handleGuardInstall(guard, installFn, payload) {
621
+ // Normalized the SAME way the sibling app:delegation-readiness handler
622
+ // above normalizes cwd, before either installing or logging — without
623
+ // this, a worktree/ops-internal cwd installs the guard into an ephemeral
624
+ // `.claude/settings.json` that vanishes with the worktree (this repo's
625
+ // "Worktree cwd ops-root hazard" incident class), diverging from the
626
+ // main-tree root the readiness banner (and its own auto-heal path,
627
+ // ensureGuardsInstalled) actually reads.
628
+ const resolved = resolveProjectContext({ cwd: payload.cwd });
629
+ const cwd = resolved.cwd || payload.cwd;
630
+ const normalizedPayload = { ...payload, cwd };
591
631
  try {
592
- const result = await installFn(payload);
593
- logGuardInstallAttempt(guard, payload.cwd, result);
632
+ const result = await installFn(normalizedPayload);
633
+ logGuardInstallAttempt(guard, cwd, result);
594
634
  return result;
595
635
  } catch (err) {
596
- logGuardInstallAttempt(guard, payload.cwd, { ok: false, action: 'error', error: err?.message ?? String(err) });
636
+ logGuardInstallAttempt(guard, cwd, { ok: false, action: 'error', error: err?.message ?? String(err) });
597
637
  throw err;
598
638
  }
599
639
  }
@@ -615,6 +655,9 @@ ipcMain.handle('app:install-destructive-git-guard', validated(schemas.delegation
615
655
  ipcMain.handle('app:install-inline-implementation-guard', validated(schemas.delegationReadinessCwd, (payload) =>
616
656
  handleGuardInstall('inline-implementation-guard', installInlineImplementationGuard, payload)
617
657
  ));
658
+ ipcMain.handle('app:install-self-schedule-guard', validated(schemas.delegationReadinessCwd, (payload) =>
659
+ handleGuardInstall('self-schedule-guard', installSelfScheduleGuard, payload)
660
+ ));
618
661
 
619
662
  ipcMain.handle('app:engage-rules-path', () => process.env.SESSION_MANAGER_ENGAGE_RULES || null);
620
663
 
@@ -641,7 +684,7 @@ ipcMain.handle('app:pick-directory', async () => {
641
684
  return result.filePaths[0];
642
685
  });
643
686
 
644
- ipcMain.on('app:reboot-app', () => rebootApp());
687
+ ipcMain.on('app:reboot-app', (_e, opts) => { rebootApp({ force: opts?.force === true }).catch((e) => logReboot(`reboot failed: ${e?.message}`)); });
645
688
 
646
689
  // Image paste — Ctrl+V in the Terminal pane. Reads the OS clipboard via
647
690
  // Electron's native API (renderer's navigator.clipboard.read() doesn't expose
@@ -916,7 +959,6 @@ voiceHotkey.registerHotkeyHandlers();
916
959
  voiceWizard.registerWizardHandlers();
917
960
  scheduler.registerScheduleHandlers();
918
961
  watchers.registerWatcherHandlers();
919
- teams.registerTeamsHandlers();
920
962
  queueOps.registerQueueOpsHandlers({ retireCompletedSlugs: scheduler.retireCompletedSlugs });
921
963
  registerHistoryAggregatorHandlers();
922
964
  registerHistoryDashboardHandlers();
@@ -1235,6 +1277,7 @@ app.whenReady().then(async () => {
1235
1277
 
1236
1278
  const heapSnapshotMenuItem = heapSnapshot.buildMenuItem(() => mainWindow);
1237
1279
 
1280
+ // Application menu rules + rationale: session-manager-operations/architecture/application-menu.md
1238
1281
  const template = [
1239
1282
  {
1240
1283
  label: 'Session Manager',
@@ -1251,7 +1294,7 @@ app.whenReady().then(async () => {
1251
1294
  {
1252
1295
  label: 'Restart Session Manager App',
1253
1296
  accelerator: 'CmdOrCtrl+Shift+R',
1254
- click: rebootApp,
1297
+ click: () => { rebootApp().catch((e) => logReboot(`reboot failed: ${e?.message}`)); },
1255
1298
  },
1256
1299
  {
1257
1300
  label: 'Restart Terminal in Active Tab',
@@ -1443,6 +1486,14 @@ function runShutdownCleanup() {
1443
1486
  // Mark a clean exit so the next boot can distinguish a graceful quit from an
1444
1487
  // OOM-kill / native crash (which leaves the sentinel `open`).
1445
1488
  crashDiagnostics.markCleanShutdown();
1489
+ // Release scheduler ownership on every teardown path, not just before-quit
1490
+ // — the app.exit() sites (in-app reboot, dist-missing abort) bypass
1491
+ // before-quit entirely and previously left a stale lock behind (fail-closed
1492
+ // proc-identity recovery covers a hard crash, but a clean app.exit() should
1493
+ // never need it). Pid-scoped and idempotent, so a redundant call from a
1494
+ // later before-quit/will-quit firing is a safe no-op.
1495
+ releaseSchedulerOwnership();
1496
+ try { scheduler.stop(); } catch { /* */ }
1446
1497
  // PRD F1 v2 §IPC plumbing: must unregisterAll on will-quit.
1447
1498
  try { globalShortcut.unregisterAll(); } catch { /* */ }
1448
1499
  voiceHotkey.disposeOnQuit();
@@ -674,21 +674,12 @@ const projectPagesCwd = z.object({
674
674
  cwd: z.string().min(1).max(4096),
675
675
  }).strict();
676
676
 
677
- // ──────────────────────────────────────────── Project Home admin routes
678
- // (PRD: project-home-admin-routes). Same validation split as
679
- // projectPagesCwd above: real path validation is projectRootResolve.cjs +
680
- // config.cjs's validatePath at first fs access.
681
- const projectHomeAdminCwdQuery = z.object({
677
+ // ──────────────────────────────────────────── Project Home admin route
678
+ // Real path validation is projectRootResolve.cjs + config.cjs's validatePath;
679
+ // html content limits live in projectHomeAdminRoutes.cjs's validateHomeHtml.
680
+ const projectHomeAdminWriteBody = z.object({
682
681
  cwd: z.string().min(1).max(4096),
683
- }).strict();
684
- const projectHomeAdminValidateSummaryBody = z.object({
685
- cwd: z.string().min(1).max(4096),
686
- summary: z.unknown(),
687
- }).strict();
688
- const projectHomeAdminRenderBody = z.object({
689
- cwd: z.string().min(1).max(4096),
690
- summary: z.unknown(),
691
- picks: z.unknown(),
682
+ html: z.string(),
692
683
  }).strict();
693
684
 
694
685
  // ──────────────────────────────────────────── Host on Bilko.run
@@ -1115,9 +1106,7 @@ module.exports = {
1115
1106
  memoryStale,
1116
1107
  projectBriefCwd,
1117
1108
  projectPagesCwd,
1118
- projectHomeAdminCwdQuery,
1119
- projectHomeAdminValidateSummaryBody,
1120
- projectHomeAdminRenderBody,
1109
+ projectHomeAdminWriteBody,
1121
1110
  bilkoHostCwd,
1122
1111
  bilkoHostPrepareBundle,
1123
1112
  bilkoHostAddDocument,
@@ -0,0 +1,38 @@
1
+ /**
2
+ * auditLog.test.cjs — readTail returns a bounded reverse tail of complete lines.
3
+ *
4
+ * Run: timeout 120 npx vitest run src/main/lib/__tests__/auditLog.test.cjs
5
+ */
6
+ 'use strict';
7
+
8
+ import { test, expect, beforeAll, afterAll } from 'vitest';
9
+ const fs = require('node:fs');
10
+ const os = require('node:os');
11
+ const path = require('node:path');
12
+ const { readTail } = require('../auditLog.cjs');
13
+
14
+ let dir;
15
+ beforeAll(() => { dir = fs.mkdtempSync(path.join(os.tmpdir(), 'auditlog-tail-')); });
16
+ afterAll(() => { fs.rmSync(dir, { recursive: true, force: true }); });
17
+
18
+ test('missing file → []', () => {
19
+ expect(readTail(1024, path.join(dir, 'nope.jsonl'))).toEqual([]);
20
+ });
21
+
22
+ test('window larger than file → every line', () => {
23
+ const f = path.join(dir, 'small.jsonl');
24
+ fs.writeFileSync(f, '{"n":1}\n{"n":2}\n');
25
+ expect(readTail(1_000_000, f)).toEqual(['{"n":1}', '{"n":2}']);
26
+ });
27
+
28
+ test('bounded window drops the partial leading line and keeps the newest', () => {
29
+ const f = path.join(dir, 'big.jsonl');
30
+ const lines = Array.from({ length: 1000 }, (_, i) => JSON.stringify({ n: i, pad: 'x'.repeat(50) }));
31
+ fs.writeFileSync(f, lines.join('\n') + '\n');
32
+ const tail = readTail(500, f);
33
+ expect(tail.length).toBeGreaterThan(0);
34
+ expect(tail.length).toBeLessThan(20);
35
+ expect(tail[tail.length - 1]).toBe(lines[999]);
36
+ for (const l of tail) expect(() => JSON.parse(l)).not.toThrow();
37
+ expect(readTail(0, f)).toEqual([]);
38
+ });
@@ -0,0 +1,121 @@
1
+ /**
2
+ * buildIdentity.test.cjs — resolveBuildIdentity() resolution order:
3
+ * build-info.json first, git fallback only when a real .git sits AT the
4
+ * package root (never walking up), else null.
5
+ *
6
+ * Each test injects a distinct fixture `packageRoot` (see buildIdentity.cjs's
7
+ * `opts.packageRoot`), which also keeps memoization scoped per-root so the
8
+ * three cases don't clobber each other's cached result in one process.
9
+ *
10
+ * Run: timeout 120 npx vitest run src/main/lib/__tests__/buildIdentity.test.cjs
11
+ */
12
+
13
+ 'use strict';
14
+
15
+ import { test, expect, afterEach } from 'vitest';
16
+ const fs = require('node:fs');
17
+ const os = require('node:os');
18
+ const path = require('node:path');
19
+ const { execFileSync } = require('node:child_process');
20
+ const { resolveBuildIdentity } = require('../buildIdentity.cjs');
21
+
22
+ const cleanupDirs = [];
23
+
24
+ function mkFixtureRoot(prefix) {
25
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), prefix));
26
+ cleanupDirs.push(dir);
27
+ fs.mkdirSync(path.join(dir, 'src', 'main'), { recursive: true });
28
+ return dir;
29
+ }
30
+
31
+ function writePackageJson(root, version) {
32
+ fs.writeFileSync(path.join(root, 'package.json'), JSON.stringify({ name: 'fixture-pkg', version }));
33
+ }
34
+
35
+ function mkRealGitRepo(root) {
36
+ execFileSync('git', ['-C', root, 'init', '-q']);
37
+ execFileSync('git', ['-C', root, 'config', 'user.email', 'test@test.com']);
38
+ execFileSync('git', ['-C', root, 'config', 'user.name', 'Test']);
39
+ fs.writeFileSync(path.join(root, 'README.md'), 'fixture\n');
40
+ execFileSync('git', ['-C', root, 'add', 'README.md']);
41
+ execFileSync('git', ['-C', root, 'commit', '-q', '-m', 'init']);
42
+ return execFileSync('git', ['-C', root, 'rev-parse', '--short', 'HEAD'], { encoding: 'utf8' }).trim();
43
+ }
44
+
45
+ afterEach(() => {
46
+ while (cleanupDirs.length) {
47
+ const dir = cleanupDirs.pop();
48
+ fs.rmSync(dir, { recursive: true, force: true });
49
+ }
50
+ });
51
+
52
+ test('build-info.json present: its fields win over both git and package.json', () => {
53
+ const root = mkFixtureRoot('sm-buildid-present-');
54
+ writePackageJson(root, '0.1.0');
55
+ fs.writeFileSync(path.join(root, 'src', 'main', 'build-info.json'), JSON.stringify({
56
+ version: '9.9.9',
57
+ gitSha: 'deadbeefdeadbeefdeadbeefdeadbeefdeadbeef',
58
+ gitShortSha: 'deadbee',
59
+ gitBranch: 'main',
60
+ builtAt: '2026-01-01T00:00:00.000Z',
61
+ dirty: false,
62
+ }));
63
+
64
+ const identity = resolveBuildIdentity({ packageRoot: root, bootedAt: '2026-02-02T00:00:00.000Z' });
65
+
66
+ expect(identity.version).toBe('9.9.9');
67
+ expect(identity.codeSha).toBe('deadbee');
68
+ expect(identity.builtAt).toBe('2026-01-01T00:00:00.000Z');
69
+ expect(identity.appPath).toBe(root);
70
+ expect(identity.bootedAt).toBe('2026-02-02T00:00:00.000Z');
71
+ });
72
+
73
+ test('build-info.json absent, real .git at the package root: codeSha falls back to git, builtAt stays null', () => {
74
+ const root = mkFixtureRoot('sm-buildid-git-');
75
+ writePackageJson(root, '1.2.3');
76
+ const sha = mkRealGitRepo(root);
77
+
78
+ const identity = resolveBuildIdentity({ packageRoot: root });
79
+
80
+ expect(identity.version).toBe('1.2.3');
81
+ expect(identity.codeSha).toBe(sha);
82
+ expect(identity.builtAt).toBeNull();
83
+ });
84
+
85
+ test('build-info.json absent, no .git at the package root: codeSha is null, never walks up to an ancestor .git', () => {
86
+ const ancestor = mkFixtureRoot('sm-buildid-ancestor-');
87
+ mkRealGitRepo(ancestor); // a real repo one level UP from the fixture root — must never be found
88
+ const root = path.join(ancestor, 'nested-pkg');
89
+ fs.mkdirSync(path.join(root, 'src', 'main'), { recursive: true });
90
+ writePackageJson(root, '2.0.0');
91
+
92
+ const identity = resolveBuildIdentity({ packageRoot: root });
93
+
94
+ expect(identity.version).toBe('2.0.0');
95
+ expect(identity.codeSha).toBeNull();
96
+ expect(identity.builtAt).toBeNull();
97
+ });
98
+
99
+ test('bootedAt is echoed back verbatim, never re-captured, and defaults to null', () => {
100
+ const root = mkFixtureRoot('sm-buildid-bootedat-');
101
+ writePackageJson(root, '1.0.0');
102
+
103
+ expect(resolveBuildIdentity({ packageRoot: root }).bootedAt).toBeNull();
104
+ expect(resolveBuildIdentity({ packageRoot: root, bootedAt: 'fixed-value' }).bootedAt).toBe('fixed-value');
105
+ });
106
+
107
+ test('installChannel is reused from machineProfile.resolveInstallChannel, keyed off appPath', () => {
108
+ const root = mkFixtureRoot('sm-buildid-channel-');
109
+ writePackageJson(root, '1.0.0');
110
+ const prevSmDev = process.env.SM_DEV;
111
+ delete process.env.SM_DEV;
112
+ try {
113
+ const identity = resolveBuildIdentity({ packageRoot: root });
114
+ // fixture root lives under os.tmpdir(), not an .npm/_npx cache path, and
115
+ // SM_DEV is unset here, so resolveInstallChannel falls through to 'unknown'.
116
+ expect(identity.installChannel).toBe('unknown');
117
+ } finally {
118
+ if (prevSmDev === undefined) delete process.env.SM_DEV;
119
+ else process.env.SM_DEV = prevSmDev;
120
+ }
121
+ });
@@ -0,0 +1,111 @@
1
+ // cwdClassify.cjs — one classifier for "what project is this path", failing
2
+ // CLOSED on the unprovable worktree case. Fixtures live under a per-run
3
+ // mkdtemp sandbox, never the live roots.
4
+ const assert = require('node:assert/strict');
5
+ const fs = require('node:fs');
6
+ const os = require('node:os');
7
+ const path = require('node:path');
8
+ const { classifyCwd, nearestGitEntry, worktreeMainRootOf } = require('../cwdClassify.cjs');
9
+ const { assertOpsWrite, resolveOpsRoot } = require('../opsOwnership.cjs');
10
+ const { isEphemeralCwd } = require('../ephemeralCwd.cjs');
11
+ const { projectRootOf } = require('../activeSessions.cjs');
12
+
13
+ const OPS = 'session-manager-operations';
14
+ let sandbox;
15
+ const real = (p) => fs.realpathSync(p);
16
+
17
+ beforeEach(() => { sandbox = real(fs.mkdtempSync(path.join(os.tmpdir(), 'sm-cwdclassify-'))); });
18
+ afterEach(() => { fs.rmSync(sandbox, { recursive: true, force: true }); });
19
+
20
+ function mkWorktree({ backRef = true } = {}) {
21
+ const main = path.join(sandbox, 'main');
22
+ const wt = path.join(sandbox, 'wt');
23
+ const admin = path.join(main, '.git', 'worktrees', 'wt1');
24
+ fs.mkdirSync(admin, { recursive: true });
25
+ fs.mkdirSync(wt, { recursive: true });
26
+ fs.writeFileSync(path.join(wt, '.git'), `gitdir: ${admin}\n`);
27
+ if (backRef) fs.writeFileSync(path.join(admin, 'gitdir'), `${path.join(wt, '.git')}\n`);
28
+ return { main, wt };
29
+ }
30
+
31
+ test('healthy worktree → worktree, projectRoot is the main tree', () => {
32
+ const { main, wt } = mkWorktree();
33
+ const c = classifyCwd(wt);
34
+ assert.equal(c.kind, 'worktree');
35
+ assert.equal(c.projectRoot, main);
36
+ assert.equal(projectRootOf(wt), main);
37
+ assert.equal(isEphemeralCwd(wt), true);
38
+ assert.equal(worktreeMainRootOf(wt), main);
39
+ });
40
+
41
+ test('broken back-reference → unknown; ops writes refused, resolution and dispatch stay open', () => {
42
+ const { wt } = mkWorktree({ backRef: false });
43
+ const c = classifyCwd(wt);
44
+ assert.equal(c.kind, 'unknown');
45
+ assert.equal(c.projectRoot, wt);
46
+ assert.match(c.reason, /back-reference/);
47
+ assert.equal(projectRootOf(wt), wt); // total — never throws
48
+ assert.equal(resolveOpsRoot(wt), path.join(wt, OPS)); // reads/dispatch unaffected
49
+ assert.throws(
50
+ () => assertOpsWrite(path.join(wt, OPS, 'scheduler', 'state', 'queue.json'), 'scheduler'),
51
+ (e) => e.unknownCwd === true && /back-reference/.test(e.message),
52
+ );
53
+ });
54
+
55
+ test('garbled .git file → unknown', () => {
56
+ const dir = path.join(sandbox, 'garbled');
57
+ fs.mkdirSync(dir);
58
+ fs.writeFileSync(path.join(dir, '.git'), 'not a pointer\n');
59
+ assert.equal(classifyCwd(dir).kind, 'unknown');
60
+ });
61
+
62
+ test('separate-git-dir → project (gitdir not under worktrees/)', () => {
63
+ const dir = path.join(sandbox, 'sep');
64
+ fs.mkdirSync(dir);
65
+ fs.writeFileSync(path.join(dir, '.git'), `gitdir: ${path.join(sandbox, 'elsewhere.git')}\n`);
66
+ const c = classifyCwd(dir);
67
+ assert.equal(c.kind, 'project');
68
+ assert.equal(c.projectRoot, dir);
69
+ assert.doesNotThrow(() => assertOpsWrite(path.join(dir, OPS, 'scheduler', 'state', 'queue.json'), 'scheduler'));
70
+ });
71
+
72
+ test('submodule (relative gitdir) → project rooted at the submodule', () => {
73
+ const sup = path.join(sandbox, 'super');
74
+ const sub = path.join(sup, 'libs', 'sub');
75
+ fs.mkdirSync(path.join(sup, '.git', 'modules', 'sub'), { recursive: true });
76
+ fs.mkdirSync(path.join(sub, 'src'), { recursive: true });
77
+ fs.writeFileSync(path.join(sub, '.git'), 'gitdir: ../../.git/modules/sub\n');
78
+ const c = classifyCwd(path.join(sub, 'src'));
79
+ assert.equal(c.kind, 'project');
80
+ assert.equal(c.projectRoot, sub);
81
+ });
82
+
83
+ test('plain project (.git dir, and no git at all) → project', () => {
84
+ const proj = path.join(sandbox, 'proj');
85
+ fs.mkdirSync(path.join(proj, '.git'), { recursive: true });
86
+ fs.mkdirSync(path.join(proj, 'src'));
87
+ assert.deepEqual(
88
+ { k: classifyCwd(path.join(proj, 'src')).kind, r: classifyCwd(path.join(proj, 'src')).projectRoot },
89
+ { k: 'project', r: proj },
90
+ );
91
+ const bare = path.join(sandbox, 'nogit');
92
+ fs.mkdirSync(bare);
93
+ assert.equal(classifyCwd(bare).projectRoot, bare);
94
+ assert.equal(nearestGitEntry(bare), null);
95
+ });
96
+
97
+ test('nested ops root: outermost (indexOf) and innermost (lastIndexOf) are both reported, neither unified', () => {
98
+ const proj = path.join(sandbox, 'proj');
99
+ fs.mkdirSync(path.join(proj, '.git'), { recursive: true });
100
+ const nested = path.join(proj, OPS, 'scheduler', 'prds', OPS, 'scheduler', 'state');
101
+ const c = classifyCwd(nested);
102
+ assert.equal(c.projectRoot, proj);
103
+ assert.equal(c.outermostOpsRoot, path.join(proj, OPS));
104
+ assert.equal(c.innermostOpsRoot, path.join(proj, OPS, 'scheduler', 'prds', OPS));
105
+ assert.equal(classifyCwd(proj).outermostOpsRoot, null);
106
+ });
107
+
108
+ test('ephemeral: os.tmpdir() itself', () => {
109
+ assert.equal(classifyCwd(path.resolve(os.tmpdir())).kind, 'ephemeral');
110
+ assert.equal(isEphemeralCwd(os.tmpdir()), true);
111
+ });
@@ -0,0 +1,95 @@
1
+ /**
2
+ * definitionOfDoneSequence.test.cjs — extractAcSequence / resolveGate parsing
3
+ * matrix plus real gate lines lifted from archived PRDs (gateFixtures.json).
4
+ *
5
+ * Run: timeout 120 npx vitest run src/main/lib/__tests__/definitionOfDoneSequence.test.cjs
6
+ */
7
+ 'use strict';
8
+
9
+ const assert = require('node:assert/strict');
10
+ const { extractAcSequence, extractAcCommand, resolveGate } = require('../definitionOfDone.cjs');
11
+ const fixtures = require('./gateFixtures.json');
12
+
13
+ const ac = (...lines) => ['# Acceptance criteria', '', ...lines, '', '# Out of scope', ''].join('\n');
14
+ const argvs = (seq) => seq.map((c) => c.argv);
15
+
16
+ test('splits a && chain, honouring a leading timeout N per command', () => {
17
+ const seq = extractAcSequence(ac('- [ ] Gate: `timeout 300 npm run typecheck && timeout 60 npm run lint`'));
18
+ assert.deepEqual(argvs(seq), [['npm', 'run', 'typecheck'], ['npm', 'run', 'lint']]);
19
+ assert.deepEqual(seq.map((c) => c.timeoutMs), [300_000, 60_000]);
20
+ });
21
+
22
+ test('a command with no timeout token gets the default timeout', () => {
23
+ const seq = extractAcSequence(ac('- [ ] `timeout 5 npm run a && npm run b`'));
24
+ assert.equal(seq[0].timeoutMs, 5_000);
25
+ assert.equal(seq[1].timeoutMs, 300_000);
26
+ });
27
+
28
+ test('&& inside quotes is never a split point', () => {
29
+ const seq = extractAcSequence(ac('- [ ] `timeout 30 node -e "a && b" && timeout 30 echo \'x && y\'`'));
30
+ assert.deepEqual(argvs(seq), [['node', '-e', 'a && b'], ['echo', 'x && y']]);
31
+ });
32
+
33
+ test('a leading TMPDIR=$(mktemp -d) is dropped, other literal env is kept', () => {
34
+ const seq = extractAcSequence(ac('- [ ] `TMPDIR=$(mktemp -d) timeout 900 npm run test:unit && FOO=bar timeout 10 npm run x`'));
35
+ assert.deepEqual(argvs(seq), [['npm', 'run', 'test:unit'], ['npm', 'run', 'x']]);
36
+ assert.deepEqual(seq[0].env, {});
37
+ assert.deepEqual(seq[1].env, { FOO: 'bar' });
38
+ });
39
+
40
+ test('any surviving shell metacharacter makes the whole gate unparseable (no shell)', () => {
41
+ for (const bad of [
42
+ '`timeout 60 npm run a | tail -5`', '`timeout 60 npm run a > out.txt`', '`timeout 60 npm run a; rm x`',
43
+ '`timeout 60 npm run a & echo`', '`timeout 60 echo $(whoami)`', '`timeout 60 npm run a && timeout 60 npm run b || true`',
44
+ ]) {
45
+ assert.deepEqual(extractAcSequence(ac(`- [ ] ${bad}`)), [], bad);
46
+ }
47
+ });
48
+
49
+ test('an empty segment (dangling &&) is unparseable', () => {
50
+ assert.deepEqual(extractAcSequence(ac('- [ ] `timeout 60 npm run a &&`')), []);
51
+ });
52
+
53
+ test('frontmatter gate: list takes priority over the AC line', () => {
54
+ const text = '---\ntitle: t\ngate:\n - timeout 10 npm run one\n - "timeout 20 npm run two && timeout 30 npm run three"\n---\n' +
55
+ ac('- [ ] `timeout 999 npm run ignored`');
56
+ const g = resolveGate(text);
57
+ assert.equal(g.source, 'explicit');
58
+ assert.deepEqual(argvs(g.sequence), [['npm', 'run', 'one'], ['npm', 'run', 'two'], ['npm', 'run', 'three']]);
59
+ });
60
+
61
+ test('frontmatter inline gate list', () => {
62
+ const g = resolveGate('---\ngate: [timeout 10 npm run one, timeout 20 npm run two]\n---\nbody');
63
+ assert.deepEqual(argvs(g.sequence), [['npm', 'run', 'one'], ['npm', 'run', 'two']]);
64
+ });
65
+
66
+ test('a fenced ```gate block takes priority', () => {
67
+ const text = ac('- [ ] `timeout 999 npm run ignored`', '', '```gate', 'timeout 10 npm run a && timeout 20 npm run b', '```');
68
+ const g = resolveGate(text);
69
+ assert.equal(g.source, 'explicit');
70
+ assert.deepEqual(argvs(g.sequence), [['npm', 'run', 'a'], ['npm', 'run', 'b']]);
71
+ });
72
+
73
+ test('gate: none is an explicit opt-out, distinct from absent', () => {
74
+ const none = resolveGate('---\ngate: none\n---\n' + ac('- [ ] `timeout 60 npm test`'));
75
+ assert.equal(none.source, 'none');
76
+ assert.deepEqual(none.sequence, []);
77
+ assert.equal(resolveGate(ac('- [ ] prose only')).source, 'absent');
78
+ });
79
+
80
+ test('extractAcCommand delegates: single command → original string, chain → null', () => {
81
+ assert.equal(extractAcCommand(ac('- [ ] `timeout 60 npm test`')), 'timeout 60 npm test');
82
+ assert.equal(extractAcCommand(ac('- [ ] `timeout 60 npm a && timeout 60 npm b`')), null);
83
+ });
84
+
85
+ test('real archived gate lines: count how many now parse', () => {
86
+ const parsed = fixtures.filter((line) => extractAcSequence(ac(line)).length > 0);
87
+ console.log(`[gate-fixtures] ${parsed.length}/${fixtures.length} real gate lines now parse`);
88
+ const chains = fixtures.filter((l) => l.includes('&&') && /^- \[ \] (Gate|Locally)/.test(l));
89
+ for (const line of chains) assert.ok(extractAcSequence(ac(line)).length >= 2, line);
90
+ // The three deliberately un-runnable fixtures (shell loop, pipe, prose) stay empty.
91
+ const unparseable = fixtures.filter((l) => /for i in|\| tail|Prose only/.test(l));
92
+ assert.equal(unparseable.length, 3);
93
+ for (const line of unparseable) assert.deepEqual(extractAcSequence(ac(line)), [], line);
94
+ assert.equal(parsed.length, fixtures.length - 3);
95
+ });
@@ -694,7 +694,7 @@ test('the installed destructive-git command actually DENIES a shared-tree stash
694
694
  const denied = run('git stash', cwd);
695
695
  expect(denied.hookSpecificOutput?.permissionDecision).toBe('deny');
696
696
 
697
- const worktreeCwd = path.join(os2.tmpdir(), 'session-manager-job-worktrees', 'somehash', 'some-slug');
697
+ const worktreeCwd = path.join(process.env.SM_WORKTREE_ROOT || os2.tmpdir(), 'session-manager-job-worktrees', 'somehash', 'some-slug');
698
698
  const allowed = run('git stash', worktreeCwd);
699
699
  expect(allowed.hookSpecificOutput?.permissionDecision).not.toBe('deny');
700
700
  });
@@ -0,0 +1,63 @@
1
+ /**
2
+ * dispatchLoop.test.cjs — interval clamping, no overlap, throw isolation, stop(), kill switch.
3
+ * Run: timeout 120 npx vitest run src/main/lib/__tests__/dispatchLoop.test.cjs
4
+ */
5
+ 'use strict';
6
+
7
+ import { test, expect, vi, beforeEach, afterEach } from 'vitest';
8
+ const { startDispatchLoop, resolveIntervalMs } = require('../dispatchLoop.cjs');
9
+
10
+ beforeEach(() => { vi.useFakeTimers(); delete process.env.SM_DISPATCH_LOOP_DISABLE; delete process.env.SM_DISPATCH_LOOP_INTERVAL_MS; });
11
+ afterEach(() => { vi.useRealTimers(); delete process.env.SM_DISPATCH_LOOP_DISABLE; delete process.env.SM_DISPATCH_LOOP_INTERVAL_MS; });
12
+
13
+ test('interval defaults to 30s and clamps to [10s, 300s]', () => {
14
+ expect(resolveIntervalMs(undefined, {})).toBe(30_000);
15
+ expect(resolveIntervalMs(undefined, { SM_DISPATCH_LOOP_INTERVAL_MS: '1' })).toBe(10_000);
16
+ expect(resolveIntervalMs(undefined, { SM_DISPATCH_LOOP_INTERVAL_MS: '9999999' })).toBe(300_000);
17
+ expect(resolveIntervalMs(undefined, { SM_DISPATCH_LOOP_INTERVAL_MS: 'abc' })).toBe(30_000);
18
+ expect(resolveIntervalMs(45_000, {})).toBe(45_000);
19
+ });
20
+
21
+ test('ticks on the interval and stop() clears the timer', async () => {
22
+ const tick = vi.fn(async () => {});
23
+ const h = startDispatchLoop({ tick });
24
+ await vi.advanceTimersByTimeAsync(90_000);
25
+ expect(tick).toHaveBeenCalledTimes(3);
26
+ h.stop();
27
+ expect(vi.getTimerCount()).toBe(0);
28
+ await vi.advanceTimersByTimeAsync(90_000);
29
+ expect(tick).toHaveBeenCalledTimes(3);
30
+ });
31
+
32
+ test('skips a pass while the previous one is still running', async () => {
33
+ let release;
34
+ const tick = vi.fn(() => new Promise((r) => { release = r; }));
35
+ const h = startDispatchLoop({ tick, intervalMs: 10_000 });
36
+ await vi.advanceTimersByTimeAsync(50_000);
37
+ expect(tick).toHaveBeenCalledTimes(1);
38
+ release();
39
+ await vi.advanceTimersByTimeAsync(10_000);
40
+ expect(tick).toHaveBeenCalledTimes(2);
41
+ h.stop();
42
+ });
43
+
44
+ test('a throwing/rejecting tick is isolated, reported, and the loop keeps running', async () => {
45
+ const onError = vi.fn();
46
+ let n = 0;
47
+ const tick = vi.fn(async () => { n++; if (n === 1) throw new Error('boom'); if (n === 2) throw new Error('boom2'); });
48
+ const h = startDispatchLoop({ tick, intervalMs: 10_000, onError });
49
+ await vi.advanceTimersByTimeAsync(40_000);
50
+ expect(tick).toHaveBeenCalledTimes(4);
51
+ expect(onError).toHaveBeenCalledTimes(2);
52
+ h.stop();
53
+ });
54
+
55
+ test('SM_DISPATCH_LOOP_DISABLE=1 never arms the loop', async () => {
56
+ process.env.SM_DISPATCH_LOOP_DISABLE = '1';
57
+ const tick = vi.fn();
58
+ const h = startDispatchLoop({ tick });
59
+ expect(vi.getTimerCount()).toBe(0);
60
+ await vi.advanceTimersByTimeAsync(600_000);
61
+ expect(tick).not.toHaveBeenCalled();
62
+ h.stop();
63
+ });
@@ -0,0 +1,20 @@
1
+ [
2
+ "- [ ] Gate (last thing the run does, must be green): `timeout 300 npm run typecheck && timeout 300 npm run lint && timeout 600 npm run test:unit`.",
3
+ "- [ ] Gate (last thing the run does, must be green): `timeout 300 npm run typecheck && timeout 300 npm run lint && TMPDIR=$(mktemp -d) timeout 900 npm run test:unit`.",
4
+ "- [ ] Gate (last thing the run does, must be green): `timeout 300 npm run typecheck && timeout 300 npm run lint && TMPDIR=$(mktemp -d) timeout 900 npm run test:unit` — run the unit suite with a scratch TMPDIR so gitWorktree.test.cjs sweeps a throwaway root, never the live job-worktree root.",
5
+ "- [ ] Gate (last thing the run does, must be green): `timeout 300 npm run typecheck && timeout 300 npm run lint && TMPDIR=$(mktemp -d) timeout 900 npm run test:unit` — scratch TMPDIR so gitWorktree.test.cjs sweeps a throwaway root, never the live job-worktree root.",
6
+ "- [ ] No zustand selector returns a freshly-built value; `timeout 300 npm run lint:selectors` passes.",
7
+ "- [ ] No hook is declared below a top-level early return — `timeout 300 npm run lint:hooks` exits 0.",
8
+ "- [ ] Tests: `timeout 300 npm run typecheck` and `timeout 300 npm run lint` pass.",
9
+ "- [ ] Tests: `TMPDIR=$(mktemp -d) timeout 300 npx vitest run src/main/__tests__/activeIndexMerge.test.cjs` passes.",
10
+ "- [ ] Both tests pass: `timeout 900 npm run test:unit` and `TMPDIR=$(mktemp -d) timeout 900 npm run test:unit`.",
11
+ "- [ ] All 16 tests in `timeout 120 npx vitest run tests/unit/chatRunner.spec.ts` pass.",
12
+ "- [ ] `SM_CHAT_CONCURRENCY=3 timeout 120 npx vitest run tests/unit/chatRunner.spec.ts` passes",
13
+ "- [ ] Each handled basename is removed from the ALLOWLIST map; `timeout 120 node scripts/check-unregistered-tests.cjs` exits 0.",
14
+ "- [ ] `timeout 120 npm run lint:docs` exits 0 on the current tree.",
15
+ "- [ ] Locally, the same sequence passes: `timeout 300 npm run typecheck && timeout 300 npm run lint && timeout 600 npm run build:project-pages`.",
16
+ "- [ ] After the commit lands, `timeout 60 gh run list --branch main --limit 1` prints the newest run without error.",
17
+ "- [ ] `for i in 1 2 3; do TMPDIR=$(mktemp -d) timeout 180 npx vitest run src/main/__tests__/scheduler-tick-wedge.test.cjs; done` is green 3/3 (shell loop — not a no-shell gate).",
18
+ "- [ ] The gate is `timeout 300 npm run typecheck | tail -5` (pipe — needs a shell).",
19
+ "- [ ] Prose only, no command at all."
20
+ ]