claude-code-session-manager 0.86.0 → 0.88.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (335) hide show
  1. package/README.md +26 -53
  2. package/dist/assets/AgentLibrary-BZ6IY2g1.js +3 -0
  3. package/dist/assets/{DataModel-Q4jhl24R.js → DataModel-dAoGAODX.js} +1 -1
  4. package/dist/assets/{History-Cj2FejEo.js → History-CF157HyX.js} +2 -2
  5. package/dist/assets/{Hooks-CaelQI6t.js → Hooks-CCPPipBk.js} +3 -3
  6. package/dist/assets/{HostBilko--v7cMR8I.js → HostBilko-Dlmz2m06.js} +1 -1
  7. package/dist/assets/{Library-DgI9oCCZ.js → Library-BaXcboWX.js} +1 -1
  8. package/dist/assets/{ListDetail-DYUZN-x-.js → ListDetail-CoUCzG5S.js} +1 -1
  9. package/dist/assets/MarkdownEditor-DTg3JlOT.js +1 -0
  10. package/dist/assets/{McpServers-ypCYURh3.js → McpServers-DdjY1sA3.js} +2 -2
  11. package/dist/assets/{Memory-C2qYp-3M.js → Memory-BJ3Jlw0C.js} +4 -4
  12. package/dist/assets/{Panel-Cj2kw-Zv.js → Panel-ygRSgc41.js} +1 -1
  13. package/dist/assets/Permissions-m0KAnccF.js +3 -0
  14. package/dist/assets/{Plugins-C1Vj8_dU.js → Plugins-CJRl6B3Q.js} +2 -2
  15. package/dist/assets/{ProvenanceBadge-DczPNM5U.js → ProvenanceBadge-w8cnlVvW.js} +1 -1
  16. package/dist/assets/{SaveBar-Cd_7U6Gb.js → SaveBar-BpyPcUIk.js} +1 -1
  17. package/dist/assets/Scheduler-D2ppxSfy.js +14 -0
  18. package/dist/assets/{ScopeSwitcher-DVSyI44-.js → ScopeSwitcher-EyuIwt-j.js} +1 -1
  19. package/dist/assets/Settings-BHSGGFEc.js +3 -0
  20. package/dist/assets/{SkillReferenceGraph-CUv1_Q2c.js → SkillReferenceGraph-BNaZ2JCF.js} +1 -1
  21. package/dist/assets/{Skills-C_YHkAy-.js → Skills-Dl3q75QV.js} +2 -2
  22. package/dist/assets/{SystemPrompt-B8R7T9xn.js → SystemPrompt-Bg6F-Cyp.js} +1 -1
  23. package/dist/assets/{TagLibrary-dj9YHWyy.js → TagLibrary-BSutflCy.js} +1 -1
  24. package/dist/assets/{TiptapBody-DnSBUjHE.js → TiptapBody-CXih4xrc.js} +1 -1
  25. package/dist/assets/{Toggle-CjV_BJn6.js → Toggle-2qtUHhA9.js} +1 -1
  26. package/dist/assets/{index-CXFQIPhO.js → index-B3emy2oI.js} +583 -583
  27. package/dist/assets/{index-CDo9xBR9.css → index-BHTX4OTc.css} +1 -1
  28. package/dist/assets/settingsSchema-CdzxaqwM.js +3 -0
  29. package/dist/assets/{whisperWorker-Dbia1OpC.js → whisperWorker-C7ZGQwKg.js} +7 -7
  30. package/dist/index.html +2 -2
  31. package/dist/vad/ort-wasm-simd-threaded.asyncify.mjs +106 -110
  32. package/dist/vad/ort-wasm-simd-threaded.asyncify.wasm +0 -0
  33. package/dist/vad/ort-wasm-simd-threaded.jsep.mjs +98 -98
  34. package/dist/vad/ort-wasm-simd-threaded.jsep.wasm +0 -0
  35. package/dist/vad/ort-wasm-simd-threaded.jspi.mjs +99 -102
  36. package/dist/vad/ort-wasm-simd-threaded.jspi.wasm +0 -0
  37. package/dist/vad/ort-wasm-simd-threaded.mjs +46 -46
  38. package/dist/vad/ort-wasm-simd-threaded.wasm +0 -0
  39. package/package.json +14 -17
  40. package/plugins/CLAUDE.md +61 -0
  41. package/plugins/session-manager-dev/.claude-plugin/plugin.json +1 -1
  42. package/plugins/session-manager-dev/skills/builder/4-manual/SKILL.md +1 -1
  43. package/plugins/session-manager-dev/skills/ops-sweep/SKILL.md +1 -1
  44. package/scripts/README.md +59 -0
  45. package/scripts/audit-ops-hygiene.cjs +350 -0
  46. package/scripts/hooks/guard-destructive-git.cjs +10 -34
  47. package/scripts/hooks/guard-inline-implementation.cjs +14 -8
  48. package/scripts/hooks/guard-prd-writes.cjs +7 -36
  49. package/scripts/hooks/guard-self-schedule.cjs +175 -0
  50. package/scripts/ops-sweep.cjs +355 -0
  51. package/scripts/scheduler-mcp-server.cjs +35 -71
  52. package/src/main/__tests__/agentModelResolve.test.cjs +100 -9
  53. package/src/main/__tests__/bilkoHost-integration.test.cjs +3 -3
  54. package/src/main/__tests__/broadcastCoalescer.test.cjs +18 -0
  55. package/src/main/__tests__/chat-cancel-terminal.test.cjs +6 -9
  56. package/src/main/__tests__/chat-exit-close-race.test.cjs +3 -3
  57. package/src/main/__tests__/chat-mcp-consent-notice.test.cjs +4 -5
  58. package/src/main/__tests__/chat-queue.test.cjs +2 -2
  59. package/src/main/__tests__/chat-stop-signal.test.cjs +2 -2
  60. package/src/main/__tests__/dep-orphan-archive-health.test.cjs +77 -0
  61. package/src/main/__tests__/dod-batchkey.test.cjs +2 -2
  62. package/src/main/__tests__/dod-drain-hook.test.cjs +2 -2
  63. package/src/main/__tests__/dod-report.test.cjs +2 -2
  64. package/src/main/__tests__/dod-reverify.test.cjs +2 -2
  65. package/src/main/__tests__/epicMint.test.cjs +4 -4
  66. package/src/main/__tests__/exchanges.test.cjs +2 -2
  67. package/src/main/__tests__/extractJson.test.cjs +2 -2
  68. package/src/main/__tests__/files-reject-credentials.test.cjs +1 -1
  69. package/src/main/__tests__/fixtures/1218-fo-01-move-scripts-lib-into-src-main-lib.log +556 -0
  70. package/src/main/__tests__/health-build-freshness.test.cjs +39 -0
  71. package/src/main/__tests__/health-delegation-chain.test.cjs +17 -2
  72. package/src/main/__tests__/health-queue-dispatch.test.cjs +58 -7
  73. package/src/main/__tests__/health-tick-liveness.test.cjs +11 -3
  74. package/src/main/__tests__/health-usage-poller.test.cjs +70 -23
  75. package/src/main/__tests__/historyRollup.test.cjs +2 -2
  76. package/src/main/__tests__/kg-augment.test.cjs +2 -2
  77. package/src/main/__tests__/mcpStatus.test.cjs +2 -2
  78. package/src/main/__tests__/memoryAggregate.test.cjs +1 -1
  79. package/src/main/__tests__/memoryStale.test.cjs +1 -1
  80. package/src/main/__tests__/needsReviewLedger.test.cjs +162 -0
  81. package/src/main/__tests__/opsErrorLogTelemetryTap.test.cjs +28 -4
  82. package/src/main/__tests__/pollLoop-dispatch-on-failure.test.cjs +48 -4
  83. package/src/main/__tests__/prd-group-allocator.test.cjs +2 -2
  84. package/src/main/__tests__/prdAdminRouteParity.test.cjs +2 -0
  85. package/src/main/__tests__/prdAdminRoutes.test.cjs +14 -2
  86. package/src/main/__tests__/prdAuthoringSeed.test.cjs +39 -0
  87. package/src/main/__tests__/prdCreateDisposition.test.cjs +201 -0
  88. package/src/main/__tests__/prdFrontmatterDisposition.test.cjs +125 -0
  89. package/src/main/__tests__/prdLocations.test.cjs +100 -2
  90. package/src/main/__tests__/prdLocationsArchived.test.cjs +50 -1
  91. package/src/main/__tests__/prdSetDisposition.test.cjs +222 -0
  92. package/src/main/__tests__/proc-role-env.test.cjs +125 -0
  93. package/src/main/__tests__/procname-claude-spawn-sites.test.cjs +304 -0
  94. package/src/main/__tests__/procname-sm-processes.test.cjs +127 -0
  95. package/src/main/__tests__/projectHomeAdminRoutes.test.cjs +81 -401
  96. package/src/main/__tests__/projectPages.test.cjs +63 -149
  97. package/src/main/__tests__/queue-health-verdict.test.cjs +170 -0
  98. package/src/main/__tests__/queue-starvation-dispatch-driver.test.cjs +117 -20
  99. package/src/main/__tests__/queue-starvation-per-project.test.cjs +14 -2
  100. package/src/main/__tests__/queueHistory.test.cjs +65 -2
  101. package/src/main/__tests__/rateLimitPollerStreak.test.cjs +38 -3
  102. package/src/main/__tests__/reconcileTiming.test.cjs +135 -0
  103. package/src/main/__tests__/runVerify-landed-commit-outranks.test.cjs +181 -0
  104. package/src/main/__tests__/runVerify.test.cjs +5 -5
  105. package/src/main/__tests__/scheduleJobStatusDrift.test.cjs +3 -3
  106. package/src/main/__tests__/scheduleJobTransitions.test.cjs +103 -3
  107. package/src/main/__tests__/scheduler-adopted-run-supervision.test.cjs +143 -0
  108. package/src/main/__tests__/scheduler-autofix-select.test.cjs +2 -2
  109. package/src/main/__tests__/scheduler-autopromote.test.cjs +2 -2
  110. package/src/main/__tests__/scheduler-bash-timeout-env.test.cjs +3 -5
  111. package/src/main/__tests__/scheduler-boot-orphans.test.cjs +79 -98
  112. package/src/main/__tests__/scheduler-broadcast-reconcile.test.cjs +121 -0
  113. package/src/main/__tests__/scheduler-cross-project-batch.test.cjs +43 -0
  114. package/src/main/__tests__/scheduler-default-eligible-heal.test.cjs +161 -0
  115. package/src/main/__tests__/scheduler-dispatch-loop.test.cjs +58 -0
  116. package/src/main/__tests__/scheduler-epic-digest.test.cjs +3 -5
  117. package/src/main/__tests__/scheduler-force-tick-outcome.test.cjs +2 -2
  118. package/src/main/__tests__/scheduler-gate-shadow.test.cjs +119 -0
  119. package/src/main/__tests__/scheduler-guard-verdict-autoresolve.test.cjs +390 -0
  120. package/src/main/__tests__/scheduler-heartbeat-payload.test.cjs +80 -0
  121. package/src/main/__tests__/scheduler-inplace-salvage.test.cjs +25 -18
  122. package/src/main/__tests__/scheduler-investigation-prompt.test.cjs +4 -4
  123. package/src/main/__tests__/scheduler-job-budget.test.cjs +172 -0
  124. package/src/main/__tests__/scheduler-launch-failure.test.cjs +3 -5
  125. package/src/main/__tests__/scheduler-looks-done.test.cjs +119 -10
  126. package/src/main/__tests__/scheduler-manual-pause.test.cjs +118 -0
  127. package/src/main/__tests__/scheduler-meta-code-sha.test.cjs +26 -3
  128. package/src/main/__tests__/scheduler-porcelain-rename.test.cjs +164 -0
  129. package/src/main/__tests__/scheduler-prd-missing-skip.test.cjs +17 -2
  130. package/src/main/__tests__/scheduler-prd-persona-spawn.test.cjs +3 -5
  131. package/src/main/__tests__/scheduler-quiet-machine-lease.test.cjs +41 -6
  132. package/src/main/__tests__/scheduler-rate-limit-pause.test.cjs +62 -6
  133. package/src/main/__tests__/scheduler-rate-limit-spin-guard.test.cjs +3 -5
  134. package/src/main/__tests__/scheduler-reap-dead-running-jobs.test.cjs +182 -3
  135. package/src/main/__tests__/scheduler-reaper-helpers-basics.test.cjs +87 -0
  136. package/src/main/__tests__/scheduler-reconcile-cwd-preserve.test.cjs +100 -0
  137. package/src/main/__tests__/scheduler-reconcile-invalid-repair.test.cjs +3 -3
  138. package/src/main/__tests__/scheduler-reconcile-quarantine.test.cjs +4 -4
  139. package/src/main/__tests__/scheduler-shard-quarantine.test.cjs +110 -0
  140. package/src/main/__tests__/scheduler-shared-tree-guard.test.cjs +168 -0
  141. package/src/main/__tests__/scheduler-starve-escalation.test.cjs +18 -8
  142. package/src/main/__tests__/scheduler-stranded-autofix-park.test.cjs +245 -0
  143. package/src/main/__tests__/scheduler-supervisor-record.test.cjs +81 -0
  144. package/src/main/__tests__/scheduler-tick-cancel-token.test.cjs +2 -2
  145. package/src/main/__tests__/scheduler-tick-wedge.test.cjs +172 -0
  146. package/src/main/__tests__/scheduler-transient-failure.test.cjs +2 -2
  147. package/src/main/__tests__/scheduler-worktree-cap-defer.test.cjs +51 -11
  148. package/src/main/__tests__/scheduler-worktree-exec-cwd.test.cjs +3 -5
  149. package/src/main/__tests__/usageSingleFlight.test.cjs +157 -0
  150. package/src/main/__tests__/workTypeLibrary.test.cjs +1 -1
  151. package/src/main/bilkoHost.cjs +16 -7
  152. package/src/main/build-info.json +8 -0
  153. package/src/main/chatRunner.cjs +12 -7
  154. package/src/main/config.cjs +2 -3
  155. package/src/main/docEdit.cjs +4 -2
  156. package/src/main/health.cjs +268 -52
  157. package/src/main/heapSnapshot.cjs +2 -2
  158. package/src/main/historyAggregator.cjs +6 -1
  159. package/src/main/index.cjs +152 -50
  160. package/src/main/ipcSchemas.cjs +53 -17
  161. package/src/main/lib/__tests__/active-sessions.test.cjs +251 -0
  162. package/src/main/lib/__tests__/auditLog.test.cjs +38 -0
  163. package/src/main/lib/__tests__/bootSelfHeal.test.cjs +107 -0
  164. package/src/main/lib/__tests__/buildIdentity.test.cjs +121 -0
  165. package/src/main/lib/__tests__/cwdClassify.test.cjs +111 -0
  166. package/src/main/lib/__tests__/definitionOfDoneSequence.test.cjs +95 -0
  167. package/src/main/lib/__tests__/delegationReadiness.test.cjs +323 -44
  168. package/src/main/lib/__tests__/dispatchLoop.test.cjs +63 -0
  169. package/src/main/lib/__tests__/effectiveModelInfo.test.cjs +239 -0
  170. package/src/main/lib/__tests__/gateFixtures.json +20 -0
  171. package/src/main/lib/__tests__/gitWorktree.test.cjs +97 -1
  172. package/src/main/lib/__tests__/guardShims.test.cjs +151 -0
  173. package/src/main/lib/__tests__/instanceLock.test.cjs +93 -7
  174. package/src/main/lib/__tests__/jobSupervisorRecord.test.cjs +78 -0
  175. package/src/main/lib/__tests__/jobWorktreeBootLive.test.cjs +11 -0
  176. package/src/main/lib/__tests__/localAdminHttp.test.cjs +1 -1
  177. package/src/main/lib/__tests__/mcpToolCatalog.test.cjs +6 -1
  178. package/src/main/lib/__tests__/opsRootAbsoluteCwd.test.cjs +5 -5
  179. package/src/main/lib/__tests__/prdDisposition.test.cjs +224 -0
  180. package/src/main/lib/__tests__/procIdentity.test.cjs +119 -0
  181. package/src/main/lib/__tests__/procName.test.cjs +92 -0
  182. package/src/main/lib/__tests__/queueStoreMachineStateRecovery.test.cjs +67 -0
  183. package/src/main/lib/__tests__/reaperHelpers.test.cjs +179 -1
  184. package/src/main/lib/__tests__/schedulerMcpServerHelp.test.cjs +6 -7
  185. package/src/main/lib/__tests__/schedulerMcpServerProjectHome.test.cjs +37 -204
  186. package/src/main/lib/__tests__/schedulerPaths.test.cjs +226 -0
  187. package/src/main/lib/__tests__/schedulerPathsWorktree.test.cjs +94 -0
  188. package/src/main/lib/__tests__/schedulerRuntimeState.test.cjs +56 -0
  189. package/src/main/lib/__tests__/sessionSlots.test.cjs +45 -0
  190. package/src/main/lib/__tests__/telemetryBacklog.test.cjs +1 -1
  191. package/src/main/lib/__tests__/upgradeDrain.test.cjs +130 -0
  192. package/src/main/lib/__tests__/usageCircuit.test.cjs +285 -0
  193. package/src/main/lib/__tests__/watchdog-helpers.test.cjs +375 -0
  194. package/src/main/lib/__tests__/watchdog-relaunch.test.cjs +266 -0
  195. package/src/main/lib/activeIndexRebuild.cjs +1 -3
  196. package/{scripts → src/main}/lib/activeSessions.cjs +67 -89
  197. package/src/main/lib/adoptedRunSupervisor.cjs +136 -0
  198. package/src/main/lib/agentModelResolve.cjs +94 -24
  199. package/src/main/lib/agentPersonaSchema.cjs +2 -2
  200. package/src/main/lib/auditLog.cjs +30 -5
  201. package/src/main/lib/bootSelfHeal.cjs +88 -0
  202. package/src/main/lib/buildIdentity.cjs +113 -0
  203. package/src/main/lib/classifyPromptTicket.cjs +3 -2
  204. package/src/main/lib/claudeBin.cjs +37 -2
  205. package/src/main/lib/cleanEnv.cjs +27 -1
  206. package/src/main/lib/credentials.cjs +4 -2
  207. package/src/main/lib/cwdClassify.cjs +185 -0
  208. package/src/main/lib/definitionOfDone.cjs +296 -52
  209. package/src/main/lib/delegationReadiness.cjs +290 -225
  210. package/src/main/lib/dispatchLoop.cjs +40 -0
  211. package/src/main/lib/effectiveModelInfo.cjs +332 -0
  212. package/src/main/lib/ephemeralCwd.cjs +7 -28
  213. package/src/main/lib/epicMint.cjs +3 -3
  214. package/src/main/lib/gitWorktree.cjs +136 -19
  215. package/src/main/lib/guardShims.cjs +156 -0
  216. package/src/main/lib/historyRollup.cjs +6 -7
  217. package/src/main/lib/instanceLock.cjs +32 -5
  218. package/src/main/lib/jobDirtFilter.cjs +7 -2
  219. package/src/main/lib/jobSupervisorRecord.cjs +147 -0
  220. package/src/main/lib/jobWorktreeBootLive.cjs +9 -5
  221. package/src/main/lib/launchFailure.cjs +2 -1
  222. package/src/main/lib/localAdminHttp.cjs +9 -27
  223. package/src/main/lib/mcpToolCatalog.cjs +33 -78
  224. package/src/main/lib/needsReviewLedger.cjs +205 -0
  225. package/src/main/lib/opsErrorLog.cjs +1 -1
  226. package/src/main/lib/opsOwnership.cjs +27 -19
  227. package/src/main/lib/prdAuthoringSeed.cjs +36 -0
  228. package/src/main/lib/prdCreate.cjs +56 -1
  229. package/src/main/lib/prdDisposition.cjs +199 -0
  230. package/src/main/lib/prdFrontmatter.cjs +8 -2
  231. package/src/main/lib/prdLocations.cjs +214 -45
  232. package/src/main/lib/procIdentity.cjs +126 -0
  233. package/src/main/lib/procName.cjs +98 -0
  234. package/src/main/lib/projectHomeAdminRoutes.cjs +56 -327
  235. package/src/main/lib/projectRootResolve.cjs +1 -1
  236. package/src/main/lib/queueHistory.cjs +27 -8
  237. package/src/main/lib/queueStore.cjs +63 -38
  238. package/src/main/lib/quietMachineLease.cjs +19 -2
  239. package/src/main/lib/reaperHelpers.cjs +181 -15
  240. package/src/main/lib/reservationExpiry.cjs +30 -0
  241. package/src/main/lib/runClaudeP.cjs +4 -2
  242. package/src/main/lib/runLogRetention.cjs +3 -2
  243. package/src/main/lib/scheduleJobSchema.cjs +10 -2
  244. package/src/main/lib/scheduleJobTransitions.cjs +60 -0
  245. package/src/main/lib/schedulerBatch.cjs +27 -2
  246. package/src/main/lib/schedulerConfig.cjs +24 -0
  247. package/src/main/lib/schedulerPaths.cjs +175 -0
  248. package/src/main/lib/schedulerRuntimeState.cjs +59 -0
  249. package/src/main/lib/sessionSlots.cjs +47 -9
  250. package/src/main/lib/smProcNames.cjs +51 -0
  251. package/src/main/lib/upgradeDrain.cjs +188 -0
  252. package/src/main/lib/usageCircuit.cjs +206 -0
  253. package/{scripts → src/main}/lib/watchdogHelpers.cjs +71 -38
  254. package/src/main/lib/withTimeout.cjs +33 -0
  255. package/src/main/mcpStatus.cjs +4 -2
  256. package/src/main/pluginInstall.cjs +6 -2
  257. package/src/main/projectPages.cjs +41 -145
  258. package/src/main/pty.cjs +8 -1
  259. package/src/main/queueOps.cjs +10 -10
  260. package/src/main/runVerify.cjs +69 -3
  261. package/src/main/scheduler/prdParser.cjs +13 -0
  262. package/src/main/scheduler.cjs +2841 -482
  263. package/src/main/seedAgentPersonas.cjs +1 -1
  264. package/src/main/seedDevPlugin.cjs +1 -1
  265. package/src/main/seedSchedulerMcp.cjs +7 -6
  266. package/src/main/seedStatus.cjs +1 -1
  267. package/src/main/supervisor.cjs +5 -3
  268. package/src/main/templates/PRD_AUTHORING.md +50 -0
  269. package/src/main/usage.cjs +140 -58
  270. package/src/preload/api.d.ts +110 -65
  271. package/src/preload/index.cjs +12 -4
  272. package/src/seed/agents/project-home-builder.md +31 -48
  273. package/dist/assets/AgentLibrary-DTFL7y8G.js +0 -3
  274. package/dist/assets/MarkdownEditor-DCIubYWf.js +0 -1
  275. package/dist/assets/Permissions-BiYZNGYW.js +0 -3
  276. package/dist/assets/Scheduler-DcLBiJBq.js +0 -14
  277. package/dist/assets/Settings-Cv-pRyms.js +0 -3
  278. package/dist/assets/settingsSchema-BJVciriw.js +0 -3
  279. package/screenshots/.gitkeep +0 -0
  280. package/screenshots/README-screenshots.md +0 -13
  281. package/scripts/project-pages-logic/dist/logic.cjs +0 -4709
  282. package/scripts/render-project-pages/dist/renderer.cjs +0 -18900
  283. package/scripts/render-project-pages.cjs +0 -70
  284. package/scripts/validate-project-pages-summary.cjs +0 -62
  285. package/src/main/lib/projectPageSummarySchema.cjs +0 -181
  286. package/src/main/teams.cjs +0 -95
  287. package/src/main/templates/project-pages-catalog.json +0 -741
  288. package/src/main/templates/project-pages-default-home.html +0 -123
  289. package/src/main/templates/project-pages-pipeline.md +0 -417
  290. package/src/seed/prompts/code-review/ac-coverage-check.md +0 -8
  291. package/src/seed/prompts/code-review/correctness-only.md +0 -8
  292. package/src/seed/prompts/code-review/full-spectrum-high.md +0 -8
  293. package/src/seed/prompts/code-review/hallucination-check.md +0 -8
  294. package/src/seed/prompts/code-review/public-api-compat.md +0 -8
  295. package/src/seed/prompts/code-review/readability-naming.md +0 -8
  296. package/src/seed/prompts/debugging/bug-as-failing-test.md +0 -8
  297. package/src/seed/prompts/debugging/git-bisect-regression.md +0 -8
  298. package/src/seed/prompts/debugging/instrument-intermittent-bug.md +0 -8
  299. package/src/seed/prompts/debugging/localize-pipeline-failure.md +0 -8
  300. package/src/seed/prompts/debugging/reproduce-then-diagnose.md +0 -8
  301. package/src/seed/prompts/documentation/adr-from-change.md +0 -8
  302. package/src/seed/prompts/documentation/module-readme.md +0 -8
  303. package/src/seed/prompts/documentation/onboarding-plan.md +0 -8
  304. package/src/seed/prompts/documentation/refresh-claude-md.md +0 -8
  305. package/src/seed/prompts/documentation/tsdoc-public-exports.md +0 -8
  306. package/src/seed/prompts/git-pr/conventional-commit.md +0 -8
  307. package/src/seed/prompts/git-pr/draft-pr-title-body.md +0 -8
  308. package/src/seed/prompts/git-pr/pre-commit-safety-sweep.md +0 -8
  309. package/src/seed/prompts/git-pr/release-notes-block.md +0 -8
  310. package/src/seed/prompts/git-pr/split-large-pr.md +0 -8
  311. package/src/seed/prompts/performance/bundle-startup-audit.md +0 -8
  312. package/src/seed/prompts/performance/complexity-audit.md +0 -8
  313. package/src/seed/prompts/performance/cpu-profile-hot-path.md +0 -8
  314. package/src/seed/prompts/performance/db-query-plan-review.md +0 -8
  315. package/src/seed/prompts/performance/memory-leak-hunt.md +0 -8
  316. package/src/seed/prompts/qa/api-contract-tests.md +0 -8
  317. package/src/seed/prompts/qa/e2e-critical-path.md +0 -8
  318. package/src/seed/prompts/qa/failing-test-for-bug.md +0 -8
  319. package/src/seed/prompts/qa/find-missing-test-coverage.md +0 -8
  320. package/src/seed/prompts/qa/stabilize-flaky-test.md +0 -8
  321. package/src/seed/prompts/qa/tdd-red-first.md +0 -8
  322. package/src/seed/prompts/qa/visual-regression-review.md +0 -8
  323. package/src/seed/prompts/qa/wcag-axe-scan.md +0 -8
  324. package/src/seed/prompts/refactoring/dead-code-sweep.md +0 -8
  325. package/src/seed/prompts/refactoring/extract-duplicated-pattern.md +0 -8
  326. package/src/seed/prompts/refactoring/modernize-legacy-file.md +0 -8
  327. package/src/seed/prompts/refactoring/reduce-cyclomatic-complexity.md +0 -8
  328. package/src/seed/prompts/refactoring/tighten-module-boundaries.md +0 -8
  329. package/src/seed/prompts/security/authz-audit.md +0 -8
  330. package/src/seed/prompts/security/crypto-correctness.md +0 -8
  331. package/src/seed/prompts/security/cwe-top-25-hunt.md +0 -8
  332. package/src/seed/prompts/security/dependency-audit.md +0 -8
  333. package/src/seed/prompts/security/ipc-boundary-hardening.md +0 -8
  334. package/src/seed/prompts/security/owasp-top-10-staged-diff.md +0 -8
  335. package/src/seed/prompts/security/secret-credential-scan.md +0 -8
@@ -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,239 @@
1
+ /**
2
+ * effectiveModelInfo.test.cjs — the main-process half of "what will this
3
+ * Epic actually run as": overlay-aware persona alias resolution plus
4
+ * evidence-based concrete model id (scheduler run log, then session
5
+ * transcript), never fabricated when there's no evidence.
6
+ *
7
+ * All fixtures live under a per-test mkdtemp'd $HOME (never the real
8
+ * ~/.claude) so persona lookup (which reads os.homedir()), scheduler run
9
+ * logs, and transcripts all resolve consistently.
10
+ *
11
+ * Run: timeout 120 npx vitest run src/main/lib/__tests__/effectiveModelInfo.test.cjs
12
+ */
13
+
14
+ 'use strict';
15
+
16
+ import { test, expect, beforeEach, afterEach } from 'vitest';
17
+ const fs = require('node:fs');
18
+ const path = require('node:path');
19
+ const os = require('node:os');
20
+ const { encodeCwd } = require('../encodeCwd.cjs');
21
+ const { resolveEffectiveModelInfo, isConcreteModelId, computeEffortReachable } = require('../effectiveModelInfo.cjs');
22
+ // config.cjs's `allowedRoots` is a module-level singleton seeded from
23
+ // os.homedir() at FIRST require — since this test's HOME changes every test
24
+ // (fresh mkdtemp), a persona lookup (getPersonaBody -> validatePath) would
25
+ // only pass for whichever tmpHome happened to be active the first time
26
+ // config.cjs got lazily required. addAllowedRoot mutates the live Set
27
+ // instead, so every test's own tmpHome is explicitly admitted.
28
+ const { addAllowedRoot } = require('../../config.cjs');
29
+
30
+ let tmpHome;
31
+ let originalHome;
32
+ let cwd;
33
+
34
+ beforeEach(() => {
35
+ originalHome = process.env.HOME;
36
+ tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), 'sm-effective-model-home-'));
37
+ process.env.HOME = tmpHome;
38
+ addAllowedRoot(tmpHome);
39
+ cwd = path.join(tmpHome, 'project');
40
+ fs.mkdirSync(cwd, { recursive: true });
41
+ });
42
+
43
+ afterEach(() => {
44
+ process.env.HOME = originalHome;
45
+ fs.rmSync(tmpHome, { recursive: true, force: true });
46
+ });
47
+
48
+ function writePersona(dir, name, frontmatterLines) {
49
+ fs.mkdirSync(dir, { recursive: true });
50
+ const lines = ['---', ...frontmatterLines, '---', 'Body text.'];
51
+ fs.writeFileSync(path.join(dir, `${name}.md`), lines.join('\n'));
52
+ }
53
+
54
+ function writeGlobalPersona(name, frontmatterLines) {
55
+ writePersona(path.join(tmpHome, '.claude', 'agents'), name, frontmatterLines);
56
+ }
57
+
58
+ function writeOverlayPersona(name, frontmatterLines) {
59
+ writePersona(path.join(cwd, '.claude', 'agents'), name, frontmatterLines);
60
+ }
61
+
62
+ function writeActiveIndex(sessions) {
63
+ const dir = path.join(cwd, 'session-manager-operations', 'prompt-sessions');
64
+ fs.mkdirSync(dir, { recursive: true });
65
+ fs.writeFileSync(path.join(dir, 'active-index.json'), JSON.stringify({ sessions, events: {} }, null, 2));
66
+ }
67
+
68
+ function writeQueueJobs(jobs) {
69
+ const dir = path.join(cwd, 'session-manager-operations', 'scheduler', 'state');
70
+ fs.mkdirSync(dir, { recursive: true });
71
+ fs.writeFileSync(path.join(dir, 'queue.json'), JSON.stringify({ jobs }, null, 2));
72
+ }
73
+
74
+ function writeHistoryEntries(entries) {
75
+ const dir = path.join(cwd, 'session-manager-operations', 'scheduler', 'state');
76
+ fs.mkdirSync(dir, { recursive: true });
77
+ fs.writeFileSync(path.join(dir, 'history.jsonl'), entries.map((e) => JSON.stringify(e)).join('\n') + '\n');
78
+ }
79
+
80
+ function writeRunLog(runId, slug, text) {
81
+ const dir = path.join(tmpHome, '.claude', 'session-manager', 'scheduled-plans', 'runs', runId);
82
+ fs.mkdirSync(dir, { recursive: true });
83
+ fs.writeFileSync(path.join(dir, `${slug}.log`), text);
84
+ }
85
+
86
+ function writeTranscript(sessionId, lines) {
87
+ const dir = path.join(tmpHome, '.claude', 'projects', encodeCwd(cwd));
88
+ fs.mkdirSync(dir, { recursive: true });
89
+ fs.writeFileSync(path.join(dir, `${sessionId}.jsonl`), lines.map((l) => JSON.stringify(l)).join('\n') + '\n');
90
+ }
91
+
92
+ // ---------- persona alias resolution ----------
93
+
94
+ test('resolves a global persona alias with modelSource "persona" and no fabricated evidence', async () => {
95
+ writeGlobalPersona('writer', ['model: sonnet']);
96
+ const info = await resolveEffectiveModelInfo({ cwd, agentType: 'writer' });
97
+ expect(info.modelAlias).toBe('sonnet');
98
+ expect(info.modelSource).toBe('persona');
99
+ expect(info.resolvedModelId).toBeNull();
100
+ expect(info.resolvedFrom).toBeNull();
101
+ });
102
+
103
+ test('a persona model of "inherit" reports modelAlias null and modelSource "inherit"', async () => {
104
+ writeGlobalPersona('inheriting', ['model: inherit']);
105
+ const info = await resolveEffectiveModelInfo({ cwd, agentType: 'inheriting' });
106
+ expect(info.modelAlias).toBeNull();
107
+ expect(info.modelSource).toBe('inherit');
108
+ });
109
+
110
+ test('a persona with no model field at all is treated the same as inherit', async () => {
111
+ writeGlobalPersona('bare', []);
112
+ const info = await resolveEffectiveModelInfo({ cwd, agentType: 'bare' });
113
+ expect(info.modelAlias).toBeNull();
114
+ expect(info.modelSource).toBe('inherit');
115
+ });
116
+
117
+ test('a persona pinned to an already-concrete model id echoes it back with resolvedFrom null, ignoring conflicting evidence', async () => {
118
+ writeGlobalPersona('pinned', ['model: claude-opus-5-2027']);
119
+ // Conflicting scheduler evidence exists but must never override an
120
+ // already-concrete alias — no evidence lookup should even be attempted.
121
+ writeQueueJobs([{ slug: 'x', runId: 'r1', agentType: 'pinned', finishedAt: new Date().toISOString() }]);
122
+ writeRunLog('r1', 'x', '[scheduler] agentType=pinned persona=/whatever model=claude-sonnet-1\n');
123
+
124
+ const info = await resolveEffectiveModelInfo({ cwd, agentType: 'pinned' });
125
+ expect(info.modelAlias).toBe('claude-opus-5-2027');
126
+ expect(info.modelSource).toBe('persona');
127
+ expect(info.resolvedModelId).toBe('claude-opus-5-2027');
128
+ expect(info.resolvedFrom).toBeNull();
129
+ });
130
+
131
+ test('a project-overlay persona wins over the global definition and reports modelSource "persona-overlay"', async () => {
132
+ writeGlobalPersona('shared-name', ['model: sonnet']);
133
+ writeOverlayPersona('shared-name', ['model: opus']);
134
+ const info = await resolveEffectiveModelInfo({ cwd, agentType: 'shared-name' });
135
+ expect(info.modelAlias).toBe('opus');
136
+ expect(info.modelSource).toBe('persona-overlay');
137
+ });
138
+
139
+ test('a dangling agentType (no persona file anywhere) resolves modelSource "fallback" without throwing', async () => {
140
+ await expect(resolveEffectiveModelInfo({ cwd, agentType: 'ghost-persona' })).resolves.toBeTruthy();
141
+ const info = await resolveEffectiveModelInfo({ cwd, agentType: 'ghost-persona' });
142
+ expect(info.modelAlias).toBeNull();
143
+ expect(info.modelSource).toBe('fallback');
144
+ expect(info.resolvedModelId).toBeNull();
145
+ });
146
+
147
+ // ---------- concrete-model-id evidence ----------
148
+
149
+ test('evidence hit via the scheduler run record: reads the model logged in the most recent matching job\'s run log', async () => {
150
+ writeGlobalPersona('runner', ['model: sonnet']);
151
+ writeQueueJobs([
152
+ { slug: 'older', runId: 'run-older', agentType: 'runner', finishedAt: '2026-01-01T00:00:00.000Z' },
153
+ { slug: 'newer', runId: 'run-newer', agentType: 'runner', finishedAt: '2026-02-01T00:00:00.000Z' },
154
+ ]);
155
+ writeRunLog('run-older', 'older', '[scheduler] agentType=runner persona=/p/runner.md model=claude-old-1\n');
156
+ writeRunLog('run-newer', 'newer', '[scheduler] agentType=runner persona=/p/runner.md model=claude-sonnet-5\n');
157
+
158
+ const info = await resolveEffectiveModelInfo({ cwd, agentType: 'runner' });
159
+ expect(info.resolvedModelId).toBe('claude-sonnet-5');
160
+ expect(info.resolvedFrom).toBe('scheduler-run');
161
+ });
162
+
163
+ test('scheduler-run evidence also matches jobs already archived into history.jsonl', async () => {
164
+ writeGlobalPersona('archived-runner', ['model: opus']);
165
+ writeHistoryEntries([
166
+ { slug: 'old-hist', runId: 'run-hist', agentType: 'archived-runner', finishedAt: '2026-01-01T00:00:00.000Z' },
167
+ ]);
168
+ writeRunLog('run-hist', 'old-hist', '[scheduler] agentType=archived-runner persona=/p/x.md model=claude-opus-5\n');
169
+
170
+ const info = await resolveEffectiveModelInfo({ cwd, agentType: 'archived-runner' });
171
+ expect(info.resolvedModelId).toBe('claude-opus-5');
172
+ expect(info.resolvedFrom).toBe('scheduler-run');
173
+ });
174
+
175
+ test('evidence hit via transcript when no scheduler run exists: takes the LAST "model" occurrence', async () => {
176
+ writeGlobalPersona('chatty', ['model: opus']);
177
+ writeActiveIndex({
178
+ 'epic-1': { id: 'epic-1', claudeSessionId: 'sess-1', agentType: 'chatty', createdAt: '2026-03-01T00:00:00.000Z' },
179
+ });
180
+ writeTranscript('sess-1', [
181
+ { type: 'assistant', message: { model: 'claude-opus-4' } },
182
+ { type: 'assistant', message: { model: 'claude-opus-5' } },
183
+ ]);
184
+
185
+ const info = await resolveEffectiveModelInfo({ cwd, agentType: 'chatty' });
186
+ expect(info.resolvedModelId).toBe('claude-opus-5');
187
+ expect(info.resolvedFrom).toBe('transcript');
188
+ });
189
+
190
+ test('transcript evidence prefers the most recently created matching Epic', async () => {
191
+ writeGlobalPersona('multi-epic', ['model: haiku']);
192
+ writeActiveIndex({
193
+ 'epic-old': { id: 'epic-old', claudeSessionId: 'sess-old', agentType: 'multi-epic', createdAt: '2026-01-01T00:00:00.000Z' },
194
+ 'epic-new': { id: 'epic-new', claudeSessionId: 'sess-new', agentType: 'multi-epic', createdAt: '2026-05-01T00:00:00.000Z' },
195
+ });
196
+ writeTranscript('sess-old', [{ model: 'claude-haiku-old' }]);
197
+ writeTranscript('sess-new', [{ model: 'claude-haiku-new' }]);
198
+
199
+ const info = await resolveEffectiveModelInfo({ cwd, agentType: 'multi-epic' });
200
+ expect(info.resolvedModelId).toBe('claude-haiku-new');
201
+ expect(info.resolvedFrom).toBe('transcript');
202
+ });
203
+
204
+ test('evidence miss (no scheduler run, no transcript) reports resolvedModelId/resolvedFrom as null — never a guessed pricing-key fallback', async () => {
205
+ writeGlobalPersona('unseen', ['model: haiku']);
206
+ const info = await resolveEffectiveModelInfo({ cwd, agentType: 'unseen' });
207
+ expect(info.resolvedModelId).toBeNull();
208
+ expect(info.resolvedFrom).toBeNull();
209
+ });
210
+
211
+ // ---------- edge cases ----------
212
+
213
+ test('never throws for a missing cwd/agentType', async () => {
214
+ await expect(resolveEffectiveModelInfo({})).resolves.toMatchObject({
215
+ modelAlias: null,
216
+ modelSource: 'fallback',
217
+ resolvedModelId: null,
218
+ resolvedFrom: null,
219
+ });
220
+ });
221
+
222
+ test('isConcreteModelId treats known CLI aliases as non-concrete and everything else as concrete', () => {
223
+ expect(isConcreteModelId('opus')).toBe(false);
224
+ expect(isConcreteModelId('sonnet')).toBe(false);
225
+ expect(isConcreteModelId('haiku')).toBe(false);
226
+ expect(isConcreteModelId('fable')).toBe(false);
227
+ expect(isConcreteModelId(null)).toBe(false);
228
+ expect(isConcreteModelId('claude-opus-5')).toBe(true);
229
+ });
230
+
231
+ test('effortReachable is always false for an app-launched session — cleanChildEnv strips CLAUDE_EFFORT and CLAUDE_CODE_* before a child ever sees them', () => {
232
+ expect(computeEffortReachable({})).toBe(false);
233
+ });
234
+
235
+ test('resolveEffectiveModelInfo surfaces effortReachable alongside the model fields', async () => {
236
+ writeGlobalPersona('any', ['model: sonnet']);
237
+ const info = await resolveEffectiveModelInfo({ cwd, agentType: 'any' });
238
+ expect(info.effortReachable).toBe(false);
239
+ });
@@ -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
+ ]
@@ -24,6 +24,7 @@ let originalJobDisable;
24
24
  let originalJobMax;
25
25
  let originalEpicDisable;
26
26
  let originalEpicMax;
27
+ let originalWorktreeRoot;
27
28
 
28
29
  function git(args, cwd) {
29
30
  return execFileSync('git', args, { cwd, encoding: 'utf8' });
@@ -43,6 +44,9 @@ beforeEach(() => {
43
44
  tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'sm-gitworktree-'));
44
45
  repoCwd = path.join(tmpRoot, 'repo');
45
46
  initRepo(repoCwd);
47
+ // Every sweep below runs against THIS throwaway root, never the live job-worktree root.
48
+ originalWorktreeRoot = process.env.SM_WORKTREE_ROOT;
49
+ process.env.SM_WORKTREE_ROOT = path.join(tmpRoot, 'worktrees');
46
50
  originalJobDisable = process.env.SM_JOB_WORKTREE_DISABLE;
47
51
  originalJobMax = process.env.SM_JOB_WORKTREE_MAX;
48
52
  originalEpicDisable = process.env.SM_EPIC_WORKTREE_DISABLE;
@@ -65,6 +69,7 @@ afterEach(async () => {
65
69
  // Best-effort: prune any worktree either kind's tests created before removing the repo.
66
70
  try { await gitWorktree.reconcileWorktreesOnBoot([repoCwd], { kind: 'job' }); } catch { /* ignore */ }
67
71
  try { await gitWorktree.reconcileWorktreesOnBoot([repoCwd], { kind: 'epic' }); } catch { /* ignore */ }
72
+ restore('SM_WORKTREE_ROOT', originalWorktreeRoot);
68
73
  fs.rmSync(tmpRoot, { recursive: true, force: true });
69
74
  });
70
75
 
@@ -300,6 +305,95 @@ test('[job] carry-over failure (git apply cannot apply) degrades cleanly: worktr
300
305
  }
301
306
  });
302
307
 
308
+ // ──────────────────────────────────────────── PRD: stream the base-WIP diff capture (job 1192)
309
+
310
+ test('[job] a wide dirty tree (300+ changed paths incl. a multi-MB file) captures and carries over without a maxBuffer overflow', async () => {
311
+ const files = [];
312
+ for (let i = 0; i < 300; i++) {
313
+ const rel = `wide-${i}.txt`;
314
+ fs.writeFileSync(path.join(repoCwd, rel), `initial ${i}\n`, 'utf8');
315
+ files.push(rel);
316
+ }
317
+ const bigRel = 'big-file.bin';
318
+ fs.writeFileSync(path.join(repoCwd, bigRel), Buffer.alloc(1024, 1));
319
+ git(['add', '-A'], repoCwd);
320
+ git(['commit', '-q', '-m', 'wide baseline'], repoCwd);
321
+
322
+ // Dirty every tracked file (uncommitted tracked edits) and grow the binary
323
+ // file well past Node's execFile default maxBuffer (1MB) — the exact shape
324
+ // that overflowed the old buffered capture (job 1192: ~240 dirty paths).
325
+ for (const rel of files) fs.appendFileSync(path.join(repoCwd, rel), 'dirtied\n', 'utf8');
326
+ const bigBuf = Buffer.alloc(3 * 1024 * 1024); // 3MB of NUL bytes — git detects this as binary
327
+ fs.writeFileSync(path.join(repoCwd, bigRel), bigBuf);
328
+
329
+ const result = await gitWorktree.createJobWorktree({ cwd: repoCwd, slug: 'wide-tree' });
330
+ expect(result.ok).toBe(true);
331
+ expect(result.carriedPaths.length).toBe(files.length + 1);
332
+ expect(fs.readFileSync(path.join(result.dir, files[0]), 'utf8')).toContain('dirtied');
333
+ expect(Buffer.compare(fs.readFileSync(path.join(result.dir, bigRel)), bigBuf)).toBe(0);
334
+
335
+ await gitWorktree.cleanupJobWorktree({ cwd: repoCwd, dir: result.dir, branch: result.branch });
336
+ }, 30_000);
337
+
338
+ test('[job] a renamed file and a mutated binary file survive the capture/apply round trip using the same flags as production', async () => {
339
+ const binRel = 'asset.bin';
340
+ const binInitial = Buffer.from([0, 1, 2, 3, 4, 0, 255, 254]);
341
+ fs.writeFileSync(path.join(repoCwd, binRel), binInitial);
342
+ fs.writeFileSync(path.join(repoCwd, 'to-rename.txt'), 'original name content\n', 'utf8');
343
+ git(['add', '-A'], repoCwd);
344
+ git(['commit', '-q', '-m', 'binary + rename baseline'], repoCwd);
345
+
346
+ // Dirty tree: a tracked rename plus a mutated binary file, both uncommitted.
347
+ git(['mv', 'to-rename.txt', 'renamed.txt'], repoCwd);
348
+ const binChanged = Buffer.from([9, 8, 7, 0, 6, 5, 0, 4]);
349
+ fs.writeFileSync(path.join(repoCwd, binRel), binChanged);
350
+
351
+ const result = await gitWorktree.createJobWorktree({ cwd: repoCwd, slug: 'rename-binary' });
352
+ expect(result.ok).toBe(true);
353
+ expect(fs.existsSync(path.join(result.dir, 'to-rename.txt'))).toBe(false);
354
+ expect(fs.readFileSync(path.join(result.dir, 'renamed.txt'), 'utf8')).toBe('original name content\n');
355
+ expect(Buffer.compare(fs.readFileSync(path.join(result.dir, binRel)), binChanged)).toBe(0);
356
+
357
+ await gitWorktree.cleanupJobWorktree({ cwd: repoCwd, dir: result.dir, branch: result.branch });
358
+ });
359
+
360
+ test('[job] a capture that cannot write its diff file (target dir missing) falls back with a specific reason, never the old generic maxBuffer message', async () => {
361
+ fs.writeFileSync(path.join(repoCwd, 'README.md'), 'uncommitted tracked edit\n', 'utf8');
362
+ const bogusDir = path.join(tmpRoot, 'does-not-exist');
363
+ const result = await gitWorktree.captureAndCarryBaseDiff({ cwd: repoCwd, dir: bogusDir });
364
+ expect(result.ok).toBe(false);
365
+ expect(result.reason).toMatch(/capturing base diff failed/);
366
+ expect(result.reason).not.toMatch(/maxBuffer/i);
367
+ expect(fs.existsSync(bogusDir)).toBe(false); // nothing leaked into a directory that never existed
368
+ });
369
+
370
+ test('[job] the temp diff file is removed after carry-over on both the success and the git-apply-failure paths', async () => {
371
+ fs.writeFileSync(path.join(repoCwd, 'README.md'), 'uncommitted tracked edit\n', 'utf8');
372
+ const success = await gitWorktree.createJobWorktree({ cwd: repoCwd, slug: 'cleanup-ok' });
373
+ expect(success.ok).toBe(true);
374
+ expect(fs.readdirSync(success.dir).filter((f) => f.startsWith('.sm-worktree-carry-'))).toEqual([]);
375
+ await gitWorktree.cleanupJobWorktree({ cwd: repoCwd, dir: success.dir, branch: success.branch });
376
+
377
+ // Force a git-apply failure on a genuine clean-base worktree by making the
378
+ // target file read-only before the capture/apply call, so the patch writes
379
+ // fine but applying it fails — the finally block must still remove the temp
380
+ // patch file even though apply itself errored.
381
+ const clean = await gitWorktree.createJobWorktree({ cwd: repoCwd, slug: 'cleanup-fail' });
382
+ expect(clean.ok).toBe(true);
383
+ fs.writeFileSync(path.join(repoCwd, 'README.md'), 'another uncommitted tracked edit\n', 'utf8');
384
+ const targetFile = path.join(clean.dir, 'README.md');
385
+ fs.chmodSync(targetFile, 0o444);
386
+ try {
387
+ const fail = await gitWorktree.captureAndCarryBaseDiff({ cwd: repoCwd, dir: clean.dir });
388
+ expect(fail.ok).toBe(false);
389
+ expect(fail.reason).toMatch(/git apply failed/);
390
+ } finally {
391
+ fs.chmodSync(targetFile, 0o644);
392
+ }
393
+ expect(fs.readdirSync(clean.dir).filter((f) => f.startsWith('.sm-worktree-carry-'))).toEqual([]);
394
+ await gitWorktree.cleanupJobWorktree({ cwd: repoCwd, dir: clean.dir, branch: clean.branch });
395
+ }, 30_000);
396
+
303
397
  test('[job] falls back once the concurrency cap is reached, and recovers after cleanup', async () => {
304
398
  process.env.SM_JOB_WORKTREE_MAX = '1';
305
399
  const first = await gitWorktree.createJobWorktree({ cwd: repoCwd, slug: 'cap-1' });
@@ -954,7 +1048,9 @@ test('[job] sweepStaleWorktreeCheckouts does NOT remove an old-mtime checkout wi
954
1048
  test('[job] sweepStaleWorktreeCheckouts removes an old-mtime checkout when there is no live holder', async () => {
955
1049
  const worktree = await gitWorktree.createJobWorktree({ cwd: repoCwd, slug: 'stale-and-dead' });
956
1050
  expect(worktree.ok).toBe(true);
957
- const holders = new Set(); // nothing holds it
1051
+ // A populated holders set (some unrelated live cwd) that does not cover this checkout —
1052
+ // exercises the liveness gate for real instead of defeating it with an empty set.
1053
+ const holders = new Set([path.resolve(tmpRoot, 'unrelated-live-holder')]);
958
1054
 
959
1055
  const result = await gitWorktree.sweepStaleWorktreeCheckouts('job', { staleAgeMs: 0, holders });
960
1056
 
@@ -0,0 +1,151 @@
1
+ /**
2
+ * guardShims.test.cjs — unit tests for the stable, app-upgrade-proof guard
3
+ * hook indirection (guardShims.cjs). Uses a temp "home" dir per test so the
4
+ * real ~/.claude is never touched.
5
+ *
6
+ * Run: timeout 120 npx vitest run src/main/lib/__tests__/guardShims.test.cjs
7
+ */
8
+
9
+ import { test, expect, afterEach } from 'vitest';
10
+ const fs = require('node:fs');
11
+ const fsp = require('node:fs/promises');
12
+ const os = require('node:os');
13
+ const path = require('node:path');
14
+ const { execFileSync } = require('node:child_process');
15
+ const {
16
+ APP_ROOT,
17
+ GUARD_NAMES,
18
+ hooksDir,
19
+ pointerPath,
20
+ shimPath,
21
+ writeGuardShims,
22
+ guardShimsExist,
23
+ } = require('../guardShims.cjs');
24
+
25
+ const tmpDirs = [];
26
+ afterEach(async () => {
27
+ while (tmpDirs.length) {
28
+ const d = tmpDirs.pop();
29
+ await fsp.rm(d, { recursive: true, force: true });
30
+ }
31
+ });
32
+
33
+ async function mkTmp(prefix) {
34
+ const d = await fsp.mkdtemp(path.join(os.tmpdir(), prefix));
35
+ tmpDirs.push(d);
36
+ return d;
37
+ }
38
+
39
+ /** Run a guard (by absolute path, e.g. a shim) as a child process, same
40
+ * pattern delegationReadiness.test.cjs uses to exercise the real guards —
41
+ * NEVER require() a guard/shim in-process, since its main() calls
42
+ * process.exit(0) as a require-time side effect. */
43
+ function runGuard(scriptPath, { toolName = 'Write', toolInput, cwd }) {
44
+ return JSON.parse(execFileSync('node', [scriptPath], {
45
+ input: JSON.stringify({ tool_name: toolName, tool_input: toolInput, cwd }),
46
+ encoding: 'utf8',
47
+ }));
48
+ }
49
+
50
+ test('writeGuardShims: writes the pointer file and all three shims under homeDir', async () => {
51
+ const homeDir = await mkTmp('sm-guard-shims-home-');
52
+ const r = await writeGuardShims({ homeDir });
53
+ expect(r.ok).toBe(true);
54
+
55
+ expect(fs.existsSync(pointerPath(homeDir))).toBe(true);
56
+ expect(JSON.parse(fs.readFileSync(pointerPath(homeDir), 'utf8'))).toEqual({ appRoot: APP_ROOT });
57
+ for (const name of GUARD_NAMES) {
58
+ expect(fs.existsSync(shimPath(name, homeDir))).toBe(true);
59
+ }
60
+ expect(guardShimsExist(homeDir)).toBe(true);
61
+ });
62
+
63
+ test('writeGuardShims: does NOT vendor guard logic — the shim body only references the guard by name, never copies its source', async () => {
64
+ const homeDir = await mkTmp('sm-guard-shims-home-');
65
+ await writeGuardShims({ homeDir });
66
+
67
+ const realSource = fs.readFileSync(path.join(APP_ROOT, 'scripts', 'hooks', 'guard-prd-writes.cjs'), 'utf8');
68
+ const shimSource = fs.readFileSync(shimPath('guard-prd-writes.cjs', homeDir), 'utf8');
69
+ expect(shimSource).not.toContain('SCHEDULER_SEGMENT'); // a guard-prd-writes-specific symbol
70
+ expect(shimSource.length).toBeLessThan(realSource.length / 2);
71
+ expect(shimSource).toContain("require(path.join(ptr.appRoot, 'scripts', 'hooks', 'guard-prd-writes.cjs'))");
72
+ });
73
+
74
+ test('writeGuardShims: idempotent — re-running with the same appRoot produces byte-identical files', async () => {
75
+ const homeDir = await mkTmp('sm-guard-shims-home-');
76
+ const r1 = await writeGuardShims({ homeDir });
77
+ expect(r1.ok).toBe(true);
78
+ const pointerBefore = fs.readFileSync(pointerPath(homeDir), 'utf8');
79
+ const shimsBefore = GUARD_NAMES.map((n) => fs.readFileSync(shimPath(n, homeDir), 'utf8'));
80
+
81
+ const r2 = await writeGuardShims({ homeDir });
82
+ expect(r2.ok).toBe(true);
83
+ expect(fs.readFileSync(pointerPath(homeDir), 'utf8')).toBe(pointerBefore);
84
+ GUARD_NAMES.forEach((n, i) => {
85
+ expect(fs.readFileSync(shimPath(n, homeDir), 'utf8')).toBe(shimsBefore[i]);
86
+ });
87
+ });
88
+
89
+ test('writeGuardShims: the installed shim actually resolves through the pointer and runs the real guard logic', async () => {
90
+ const homeDir = await mkTmp('sm-guard-shims-home-');
91
+ const cwd = await mkTmp('sm-guard-shims-cwd-');
92
+ await writeGuardShims({ homeDir });
93
+
94
+ const denied = runGuard(shimPath('guard-prd-writes.cjs', homeDir), {
95
+ toolInput: { file_path: path.join(cwd, 'session-manager-operations', 'scheduler', 'prds', '1234-x.md'), content: '# x' },
96
+ cwd,
97
+ });
98
+ expect(denied.hookSpecificOutput?.permissionDecision).toBe('deny');
99
+
100
+ const allowed = runGuard(shimPath('guard-prd-writes.cjs', homeDir), {
101
+ toolInput: { file_path: path.join(cwd, 'src', 'x.ts'), content: 'x' },
102
+ cwd,
103
+ });
104
+ expect(allowed.hookSpecificOutput?.permissionDecision).not.toBe('deny');
105
+ });
106
+
107
+ test('writeGuardShims: rewriting the pointer to a DIFFERENT app root lets an already-installed shim follow an app upgrade with no re-install', async () => {
108
+ const homeDir = await mkTmp('sm-guard-shims-home-');
109
+ const cwd = await mkTmp('sm-guard-shims-cwd-');
110
+ await writeGuardShims({ homeDir }); // "first install", points at the real APP_ROOT
111
+ const shimFile = shimPath('guard-prd-writes.cjs', homeDir);
112
+ const shimBytesBefore = fs.readFileSync(shimFile, 'utf8');
113
+
114
+ // Simulate an app upgrade: a second "app root" at a brand-new path, with
115
+ // its own copy of scripts/hooks/guard-prd-writes.cjs (this is the ONLY file
116
+ // guardShims ever reads through the pointer at invocation time).
117
+ const secondAppRoot = await mkTmp('sm-guard-shims-upgraded-app-');
118
+ await fsp.mkdir(path.join(secondAppRoot, 'scripts', 'hooks'), { recursive: true });
119
+ await fsp.copyFile(
120
+ path.join(APP_ROOT, 'scripts', 'hooks', 'guard-prd-writes.cjs'),
121
+ path.join(secondAppRoot, 'scripts', 'hooks', 'guard-prd-writes.cjs'),
122
+ );
123
+
124
+ const r = await writeGuardShims({ homeDir, appRoot: secondAppRoot });
125
+ expect(r.ok).toBe(true);
126
+ expect(JSON.parse(fs.readFileSync(pointerPath(homeDir), 'utf8')).appRoot).toBe(secondAppRoot);
127
+ // The shim FILE at the already-installed path never changed.
128
+ expect(fs.readFileSync(shimFile, 'utf8')).toBe(shimBytesBefore);
129
+
130
+ // A project hook that already references this exact shim path keeps working.
131
+ const denied = runGuard(shimFile, {
132
+ toolInput: { file_path: path.join(cwd, 'session-manager-operations', 'scheduler', 'prds', '1234-x.md'), content: '# x' },
133
+ cwd,
134
+ });
135
+ expect(denied.hookSpecificOutput?.permissionDecision).toBe('deny');
136
+ });
137
+
138
+ test('writeGuardShims: returns { ok:false, error } without throwing when homeDir cannot be written to', async () => {
139
+ const base = await mkTmp('sm-guard-shims-unwritable-');
140
+ const blocker = path.join(base, 'blocker-file');
141
+ await fsp.writeFile(blocker, 'x', 'utf8');
142
+ // A homeDir whose parent path segment is a plain FILE can never be mkdir'd
143
+ // into (ENOTDIR) — reproduces "unwritable ~/.claude" without relying on
144
+ // permission bits, which root would bypass.
145
+ const homeDir = path.join(blocker, 'home');
146
+
147
+ const r = await writeGuardShims({ homeDir });
148
+ expect(r.ok).toBe(false);
149
+ expect(r.error).toBeTruthy();
150
+ expect(guardShimsExist(homeDir)).toBe(false);
151
+ });