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
@@ -18,13 +18,18 @@ const opsErrorLog = require('../lib/opsErrorLog.cjs');
18
18
  const tmpDirs = [];
19
19
  let originalHome;
20
20
  let originalSmTelemetrySpool;
21
+ let originalWorktreeRoot;
21
22
 
22
23
  afterEach(async () => {
23
24
  if (originalHome !== undefined) process.env.HOME = originalHome;
24
25
  if (originalSmTelemetrySpool === undefined) delete process.env.SM_TELEMETRY_SPOOL; else process.env.SM_TELEMETRY_SPOOL = originalSmTelemetrySpool;
26
+ if (originalWorktreeRoot !== undefined) {
27
+ if (originalWorktreeRoot === null) delete process.env.SM_WORKTREE_ROOT; else process.env.SM_WORKTREE_ROOT = originalWorktreeRoot;
28
+ originalWorktreeRoot = undefined;
29
+ }
25
30
  const telemetryPath = require.resolve('../lib/telemetryClient.cjs');
26
31
  delete require.cache[telemetryPath];
27
- const activeSessionsPath = require.resolve('../../../scripts/lib/activeSessions.cjs');
32
+ const activeSessionsPath = require.resolve('../lib/activeSessions.cjs');
28
33
  delete require.cache[activeSessionsPath];
29
34
  while (tmpDirs.length) {
30
35
  const d = tmpDirs.pop();
@@ -85,12 +90,23 @@ test('level "warn" routes to telemetryClient.logLine, default "error" routes to
85
90
  // ─── ephemeral cwd: local refused, telemetry still fires ─────────────────
86
91
 
87
92
  test('an ephemeral cwd yields zero local lines but one telemetry record, with a normalized projectHash', () => {
93
+ // Why this test depends on the process tmpdir: ephemeral classification
94
+ // (cwdClassify.classifyCwd, via ephemeralCwd.isEphemeralCwd) is an exact match
95
+ // on os.tmpdir() plus a prefix match on the managed job/epic worktree roots,
96
+ // and those roots are <SM_WORKTREE_ROOT || os.tmpdir()>/session-manager-*-worktrees
97
+ // (schedulerPaths.worktreeRoot). The sandbox globalSetup mkdtemp's that base under
98
+ // the tmpdir it saw at startup, so a relocated TMPDIR / prior env mutation changes
99
+ // where KIND_CONFIG.epic.root points. Pin SM_WORKTREE_ROOT to a fixture dir this
100
+ // test owns and derive the cwd from KIND_CONFIG, so expected and actual roots
101
+ // resolve through the same code path regardless of TMPDIR.
102
+ originalWorktreeRoot = process.env.SM_WORKTREE_ROOT === undefined ? null : process.env.SM_WORKTREE_ROOT;
103
+ process.env.SM_WORKTREE_ROOT = mkTmpProject();
88
104
  const { KIND_CONFIG } = require('../lib/gitWorktree.cjs');
89
105
  const worktreeCwd = path.join(KIND_CONFIG.epic.root, 'fakehash', 'fake-epic-id');
90
106
  const realRoot = '/home/bilko/Projects/real-project';
91
107
 
92
- const activeSessionsPath = require.resolve('../../../scripts/lib/activeSessions.cjs');
93
- const original = require('../../../scripts/lib/activeSessions.cjs');
108
+ const activeSessionsPath = require.resolve('../lib/activeSessions.cjs');
109
+ const original = require('../lib/activeSessions.cjs');
94
110
  require.cache[activeSessionsPath] = {
95
111
  id: activeSessionsPath,
96
112
  filename: activeSessionsPath,
@@ -103,7 +119,9 @@ test('an ephemeral cwd yields zero local lines but one telemetry record, with a
103
119
 
104
120
  opsErrorLog.appendError({ cwd: worktreeCwd, scope: 'chatRunner', message: 'worktree err' });
105
121
 
106
- expect(fs.existsSync(opsErrorLog.todayFile(worktreeCwd))).toBe(false);
122
+ // resolveProjectRoot now consumes cwdClassify (not the stubbed projectRootOf):
123
+ // an ephemeral worktree cwd is refused outright, so no local path is even built.
124
+ expect(() => opsErrorLog.todayFile(worktreeCwd)).toThrow(/ephemeral/);
107
125
  expect(reportErrorCalls).toHaveLength(1);
108
126
  expect(reportErrorCalls[0].context.cwd).toBe(realRoot);
109
127
  expect(reportErrorCalls[0].context.cwd).not.toBe(worktreeCwd);
@@ -139,6 +157,12 @@ test('a realistic error with an absolute path and a prompt-like string never rea
139
157
  message: `run failed while processing "${promptLike}" at ${absPath}`,
140
158
  });
141
159
 
160
+ // The telemetry write is fire-and-forget async; poll (bounded) for the queue
161
+ // file instead of a fixed sleep, which raced under load.
162
+ const deadline = Date.now() + 5000;
163
+ while (!fs.existsSync(client.queuePath()) && Date.now() < deadline) {
164
+ await new Promise((r) => setTimeout(r, 25));
165
+ }
142
166
  await new Promise((r) => setTimeout(r, 50));
143
167
 
144
168
  const raw = await fsp.readFile(client.queuePath(), 'utf8');
@@ -16,6 +16,16 @@
16
16
  * whether that pass ends in an actual job launch (see classifyQueueStarvation's
17
17
  * header for why this must be distinct from `lastRunAt`).
18
18
  *
19
+ * Usage-meter circuit PRD update: pollLoop no longer fires blind at a hard-
20
+ * coded cachedUtilization=0 on failure — it falls back to
21
+ * usageCircuit.degradedBudget(), which carries forward the last known-good
22
+ * BINDING window's utilization (or conservatively assumes 100/no-headroom
23
+ * when there's no known-good reading yet at all). So each test below first
24
+ * drives ONE successful ('ok') poll — with the queue still empty, so it
25
+ * can't itself cause a dispatch — to seed that known-good low-utilization
26
+ * reading, THEN seeds the pending job and drives the failure. This proves
27
+ * dispatch proceeds under the CARRIED-FORWARD degraded budget, not a blind 0.
28
+ *
19
29
  * No existing test in this repo mocks a module import (see
20
30
  * pty-epic-worktree-spawn-cwd.test.cjs's header) — this monkey-patches
21
31
  * usage.cjs's cached module object directly instead, same pattern.
@@ -73,10 +83,24 @@ function mkProject() {
73
83
  return { cwd, slug };
74
84
  }
75
85
 
86
+ // dependsOn deliberately references a slug that can never resolve (no such
87
+ // row, no history/archive entry) so pickNextBatch's dep gate holds this job
88
+ // forever instead of ever handing it to spawnJob. tickQueue() still reaches
89
+ // the picker and stamps lastDispatchAttemptAt (that stamp is unconditional —
90
+ // see lastDispatchAttemptAt's own header comment), which is the only thing
91
+ // these tests assert on. Without this, the picker actually dispatches the
92
+ // job: spawnJob() is fire-and-forget (scheduler.cjs's own comment: "spawnJob
93
+ // is fire-and-forget; it calls tickQueue() on completion") and its real
94
+ // child-process exit handler later calls mutate() independently of the
95
+ // tickTail chain flushPendingTick() drains — round-tripping whatever
96
+ // lastDispatchAttemptAt was in memory at ITS read time. Reproduced outside
97
+ // vitest: that stray write landed ~2s after this suite's own reset-to-null,
98
+ // clobbering the NEXT test's "still null" assertion with the previous test's
99
+ // stamp. A held (never-dispatched) job has no such background continuation.
76
100
  async function seedOnePendingJob() {
77
101
  const { cwd, slug } = mkProject();
78
102
  await scheduler.writeQueue({
79
- jobs: [{ slug, title: 'x', cwd, status: 'pending', dependsOn: [] }],
103
+ jobs: [{ slug, title: 'x', cwd, status: 'pending', dependsOn: ['unresolvable-dep-never-completes'] }],
80
104
  config: {},
81
105
  paused: null,
82
106
  });
@@ -96,11 +120,23 @@ async function flushPendingTick() {
96
120
  await scheduler.tickQueue();
97
121
  }
98
122
 
99
- test('pollLoop still attempts a dispatch after a transient billing failure', async () => {
123
+ test('pollLoop still attempts a dispatch after a transient billing failure, using the degraded budget (not utilization 0)', async () => {
124
+ // Seed a known-good low-utilization reading via an 'ok' poll first.
125
+ process.env.SM_E2E = '1';
126
+ process.env.SM_MOCK_BILLING_KIND = 'ok';
127
+ await scheduler.pollLoop();
128
+ // A prior test's leftover project (a REAL, still-registered project cwd —
129
+ // readQueue() merges every such cwd) can still hold its own permanently-
130
+ // pending job, so this seed poll's own maybeLaunchWhenAvailable may itself
131
+ // spawn a fire-and-forget tick; drain it before resetting/reseeding so
132
+ // that stray tick's async mutate() can't land AFTER (and clobber) the
133
+ // reset below.
134
+ await flushPendingTick();
135
+ await scheduler.writeQueue({ jobs: [], config: {}, paused: null });
136
+
100
137
  await seedOnePendingJob();
101
138
  expect(await lastDispatchAttemptAt()).toBeNull();
102
139
 
103
- process.env.SM_E2E = '1';
104
140
  process.env.SM_MOCK_BILLING_KIND = 'transient';
105
141
  await scheduler.pollLoop();
106
142
  await flushPendingTick();
@@ -108,10 +144,18 @@ test('pollLoop still attempts a dispatch after a transient billing failure', asy
108
144
  expect(await lastDispatchAttemptAt()).not.toBeNull();
109
145
  });
110
146
 
111
- test('pollLoop still attempts a dispatch after the billing poll throws', async () => {
147
+ test('pollLoop still attempts a dispatch after the billing poll throws, using the degraded budget (not utilization 0)', async () => {
148
+ process.env.SM_E2E = '1';
149
+ process.env.SM_MOCK_BILLING_KIND = 'ok';
150
+ await scheduler.pollLoop(); // seed a known-good low-utilization reading
151
+ await flushPendingTick();
152
+ await scheduler.writeQueue({ jobs: [], config: {}, paused: null });
153
+
112
154
  await seedOnePendingJob();
113
155
  expect(await lastDispatchAttemptAt()).toBeNull();
114
156
 
157
+ delete process.env.SM_E2E;
158
+ delete process.env.SM_MOCK_BILLING_KIND;
115
159
  billing.fetchUsage = async () => { throw new Error('simulated IPC transport failure'); };
116
160
  await scheduler.pollLoop();
117
161
  await flushPendingTick();
@@ -8,12 +8,12 @@
8
8
  * so a collision silently merges two unrelated projects' PRDs into one
9
9
  * group.
10
10
  *
11
- * Run: timeout 120 node --test src/main/__tests__/prd-group-allocator.test.cjs
11
+ * Run: timeout 300 npx vitest run src/main/__tests__/prd-group-allocator.test.cjs
12
12
  */
13
13
 
14
14
  'use strict';
15
15
 
16
- const { test } = require('node:test');
16
+ import { test } from 'vitest';
17
17
  const assert = require('node:assert/strict');
18
18
  const fs = require('node:fs');
19
19
  const fsp = require('node:fs/promises');
@@ -40,6 +40,8 @@ const REQUIRED_CAPABILITIES = [
40
40
  { capability: 'retag a PRD (parallelGroup/estimateMinutes)', ipcChannel: 'schedule:retag-prd', adminRoute: 'POST /admin/scheduler/retag-prd' },
41
41
  { capability: 'list scheduler jobs', ipcChannel: null, adminRoute: 'GET /admin/scheduler/jobs' },
42
42
  { capability: 'reset a stuck job', ipcChannel: 'schedule:reset-job', adminRoute: 'POST /admin/scheduler/reset-job' },
43
+ { capability: 'pause new dispatch (user-initiated)', ipcChannel: 'schedule:pause', adminRoute: 'POST /admin/scheduler/pause' },
44
+ { capability: 'resume a paused scheduler', ipcChannel: 'schedule:resume', adminRoute: 'POST /admin/scheduler/resume' },
43
45
  { capability: 'cancel a not-yet-terminal job', ipcChannel: null, adminRoute: 'POST /admin/scheduler/cancel-job' },
44
46
  ];
45
47
 
@@ -499,6 +499,12 @@ test('POST /admin/scheduler/reset-job with cwd resets the SAME-slug job belongin
499
499
  bustCwdCache();
500
500
  const slug = uniqueSlug('collision-slug');
501
501
  const epicId = 'epic-reset-collision';
502
+ // Both projects' rows need a real PRD source — a 'needs_review' row whose
503
+ // PRD is invisible to reconcile()'s onDisk scan is silently dropped (never
504
+ // backfilled to history.jsonl the way a completed/failed/skipped orphan
505
+ // is), which would make jobA vanish for a reason unrelated to what this
506
+ // test actually verifies (cwd-scoped job lookup).
507
+ await writePrdFixture({ cwd: cwdA, epicId, slug });
502
508
  await writePrdFixture({ cwd: cwdB, epicId, slug });
503
509
  await scheduler.writeQueue({
504
510
  jobs: [
@@ -518,8 +524,14 @@ test('POST /admin/scheduler/reset-job with cwd resets the SAME-slug job belongin
518
524
  expect(res.json).toEqual({ ok: true, slug, status: 'pending' });
519
525
 
520
526
  const state = await scheduler.remote.listJobs();
521
- const jobA = state.find((j) => j.cwd === cwdA);
522
- const jobB = state.find((j) => j.cwd === cwdB);
527
+ // Filter by slug too, not just cwd: cwdA is a shared fixture directory
528
+ // reused by many tests in this file, each of which leaves its own
529
+ // never-archived PRD file behind (only the queue row is cleared in
530
+ // `finally`) — reconcile()'s fresh-discovery pass can quarantine one of
531
+ // those leftovers as a NEW row under this same cwd on any later pass, so
532
+ // `cwd` alone no longer uniquely identifies this test's own job.
533
+ const jobA = state.find((j) => j.cwd === cwdA && j.slug === slug);
534
+ const jobB = state.find((j) => j.cwd === cwdB && j.slug === slug);
523
535
  expect(jobA.status).toBe('needs_review'); // untouched
524
536
  expect(jobB.status).toBe('pending'); // the one actually targeted
525
537
  } finally {
@@ -0,0 +1,39 @@
1
+ 'use strict';
2
+ const fs = require('fs');
3
+ const os = require('os');
4
+ const path = require('path');
5
+ const { seedAuthoringGuide } = require('../lib/prdAuthoringSeed.cjs');
6
+
7
+ const write = async (abs, text) => fs.writeFileSync(abs, text, 'utf8');
8
+
9
+ describe('seedAuthoringGuide', () => {
10
+ let dir, src, dest;
11
+ beforeEach(() => {
12
+ dir = fs.mkdtempSync(path.join(os.tmpdir(), 'prd-seed-'));
13
+ src = path.join(dir, 'src.md');
14
+ dest = path.join(dir, 'dest.md');
15
+ fs.writeFileSync(src, '<!-- PRD_AUTHORING.md v3 -->\nnew body\n');
16
+ });
17
+ afterEach(() => fs.rmSync(dir, { recursive: true, force: true }));
18
+
19
+ it('seeds an absent destination', async () => {
20
+ expect(await seedAuthoringGuide({ src, dest, write })).toBe(true);
21
+ expect(fs.readFileSync(dest, 'utf8')).toContain('new body');
22
+ });
23
+ it('overwrites a stale stamp', async () => {
24
+ fs.writeFileSync(dest, '<!-- PRD_AUTHORING.md v2 -->\nold\n');
25
+ expect(await seedAuthoringGuide({ src, dest, write })).toBe(true);
26
+ expect(fs.readFileSync(dest, 'utf8')).toContain('v3');
27
+ });
28
+ it('leaves a matching stamp untouched', async () => {
29
+ const same = '<!-- PRD_AUTHORING.md v3 -->\nlocal edit\n';
30
+ fs.writeFileSync(dest, same);
31
+ expect(await seedAuthoringGuide({ src, dest, write })).toBe(false);
32
+ expect(fs.readFileSync(dest, 'utf8')).toBe(same);
33
+ });
34
+ it('overwrites a destination with no stamp', async () => {
35
+ fs.writeFileSync(dest, '# no stamp\n');
36
+ expect(await seedAuthoringGuide({ src, dest, write })).toBe(true);
37
+ expect(fs.readFileSync(dest, 'utf8')).toContain('v3');
38
+ });
39
+ });
@@ -0,0 +1,201 @@
1
+ /**
2
+ * prdCreateDisposition.test.cjs — scheduler wave-disposition PRD:
3
+ * createPrd()'s authoring-time disposition gate. A PRD landing as a wave
4
+ * ROOT (no dependsOn of its own) in an Epic that already has incomplete
5
+ * PRDs must record an explicit 'append'/'new-head' choice — required for an
6
+ * interactive caller, defaulted (and logged) for a non-interactive one.
7
+ *
8
+ * Run: timeout 120 npx vitest run src/main/__tests__/prdCreateDisposition.test.cjs
9
+ */
10
+
11
+ 'use strict';
12
+
13
+ import { test, expect, beforeEach, afterEach } from 'vitest';
14
+ const fsp = require('node:fs/promises');
15
+ const os = require('node:os');
16
+ const path = require('node:path');
17
+ const config = require('../config.cjs');
18
+ const prdParser = require('../scheduler/prdParser.cjs');
19
+ const { parsePrdFile } = require('../lib/prdFrontmatter.cjs');
20
+ const { createPrd } = require('../lib/prdCreate.cjs');
21
+
22
+ async function mkTmpPrdsDir() {
23
+ const root = await fsp.mkdtemp(path.join(os.tmpdir(), 'sm-disposition-create-prd-'));
24
+ config.addAllowedRoot(root);
25
+ const prdsDir = path.join(root, '.claude', 'prds');
26
+ await fsp.mkdir(prdsDir, { recursive: true });
27
+ return prdsDir;
28
+ }
29
+
30
+ /** Minimal fake `remote` — same shape as prdCreate.test.cjs's
31
+ * makeFakeRemoteWithPrdsDir, but listPrds also reports sourcePromptId/
32
+ * dependsOn (read back from each seeded file's real frontmatter) and
33
+ * status (from the test-supplied statusBySlug map) — the fields
34
+ * createPrd()'s disposition gate actually reads. */
35
+ function makeFakeRemote(prdsDir, statusBySlug = {}) {
36
+ return {
37
+ async allocateParallelGroup() {
38
+ return prdParser.allocateParallelGroup(prdsDir);
39
+ },
40
+ async readPrd(slug) {
41
+ try {
42
+ const text = await fsp.readFile(path.join(prdsDir, `${slug}.md`), 'utf8');
43
+ return { ok: true, text };
44
+ } catch (e) {
45
+ return { ok: false, error: e?.message };
46
+ }
47
+ },
48
+ async writePrd(slug, body) {
49
+ try {
50
+ const filePath = path.join(prdsDir, `${slug}.md`);
51
+ await config.writeTextAtomic(filePath, body, { writer: 'scheduler' });
52
+ return { ok: true };
53
+ } catch (e) {
54
+ return { ok: false, error: e?.message };
55
+ }
56
+ },
57
+ async listPrds() {
58
+ const entries = await fsp.readdir(prdsDir);
59
+ const prds = [];
60
+ for (const f of entries) {
61
+ if (!f.endsWith('.md')) continue;
62
+ const slug = f.slice(0, -3);
63
+ const raw = await fsp.readFile(path.join(prdsDir, f), 'utf8');
64
+ const { frontmatter } = parsePrdFile(raw);
65
+ prds.push({
66
+ slug,
67
+ sourcePromptId: frontmatter.sourcePromptId ?? null,
68
+ dependsOn: frontmatter.dependsOn ?? null,
69
+ status: statusBySlug[slug] ?? null,
70
+ });
71
+ }
72
+ return { prds, total: prds.length, limit: prds.length, offset: 0, hasMore: false };
73
+ },
74
+ };
75
+ }
76
+
77
+ /** Seeds a fixture PRD file directly (bypassing createPrd) at an explicit,
78
+ * already-NN-prefixed slug, so tests can assert dependsOn against a known
79
+ * name instead of a freshly-allocated one. */
80
+ async function seedPrd(prdsDir, slug, { sourcePromptId, dependsOn } = {}) {
81
+ const lines = ['---', 'title: seed', `sourcePromptId: ${sourcePromptId}`];
82
+ if (dependsOn && dependsOn.length) lines.push(`dependsOn: [${dependsOn.join(', ')}]`);
83
+ lines.push('---', '# Goal', '', 'seed body', '');
84
+ await fsp.writeFile(path.join(prdsDir, `${slug}.md`), lines.join('\n'));
85
+ }
86
+
87
+ function validBody(overrides = {}) {
88
+ return {
89
+ title: 'Wave 2 root',
90
+ cwd: os.homedir(),
91
+ estimateMinutes: 10,
92
+ goal: 'Do the follow-on work.',
93
+ acceptanceCriteria: ['a'],
94
+ implementationNotes: 'n',
95
+ ...overrides,
96
+ };
97
+ }
98
+
99
+ // This suite's own process may itself be running as a scheduled job (this
100
+ // file's disposition gate keys directly off SM_SCHEDULER_JOB_SLUG — see
101
+ // createPrd()) — never assume it's unset going in; each test controls it
102
+ // explicitly.
103
+ const originalJobSlug = process.env.SM_SCHEDULER_JOB_SLUG;
104
+ beforeEach(() => {
105
+ delete process.env.SM_SCHEDULER_JOB_SLUG;
106
+ });
107
+ afterEach(() => {
108
+ if (originalJobSlug === undefined) delete process.env.SM_SCHEDULER_JOB_SLUG;
109
+ else process.env.SM_SCHEDULER_JOB_SLUG = originalJobSlug;
110
+ });
111
+
112
+ test('refuses to write, without a disposition, a new root joining an Epic that already has an incomplete PRD', async () => {
113
+ const prdsDir = await mkTmpPrdsDir();
114
+ await seedPrd(prdsDir, '1-wave1-tail', { sourcePromptId: 'epic-1' });
115
+ const remote = makeFakeRemote(prdsDir, { '1-wave1-tail': 'pending' });
116
+
117
+ const result = await createPrd(validBody({ sourcePromptId: 'epic-1' }), remote);
118
+
119
+ expect(result.ok).toBe(false);
120
+ expect(result.status).toBe(400);
121
+ expect(result.error).toMatch(/disposition/i);
122
+ });
123
+
124
+ test('disposition: append attaches the new root behind the Epic\'s current terminal PRD', async () => {
125
+ const prdsDir = await mkTmpPrdsDir();
126
+ await seedPrd(prdsDir, '1-wave1-tail', { sourcePromptId: 'epic-1' });
127
+ const remote = makeFakeRemote(prdsDir, { '1-wave1-tail': 'pending' });
128
+
129
+ const result = await createPrd(validBody({ sourcePromptId: 'epic-1', disposition: 'append' }), remote);
130
+
131
+ expect(result.ok).toBe(true);
132
+ const written = await fsp.readFile(path.join(prdsDir, result.filename), 'utf8');
133
+ expect(written).toMatch(/^dependsOn: \[1-wave1-tail\]$/m);
134
+ expect(written).toMatch(/^disposition: append$/m);
135
+ });
136
+
137
+ test('disposition: new-head leaves the new root independent (no dependsOn)', async () => {
138
+ const prdsDir = await mkTmpPrdsDir();
139
+ await seedPrd(prdsDir, '1-wave1-tail', { sourcePromptId: 'epic-1' });
140
+ const remote = makeFakeRemote(prdsDir, { '1-wave1-tail': 'pending' });
141
+
142
+ const result = await createPrd(validBody({ sourcePromptId: 'epic-1', disposition: 'new-head' }), remote);
143
+
144
+ expect(result.ok).toBe(true);
145
+ const written = await fsp.readFile(path.join(prdsDir, result.filename), 'utf8');
146
+ expect(written).not.toContain('dependsOn');
147
+ expect(written).toMatch(/^disposition: new-head$/m);
148
+ });
149
+
150
+ test('a non-interactive caller (SM_SCHEDULER_JOB_SLUG set) with no disposition defaults to append, logged as a default', async () => {
151
+ const prdsDir = await mkTmpPrdsDir();
152
+ await seedPrd(prdsDir, '1-wave1-tail', { sourcePromptId: 'epic-1' });
153
+ const remote = makeFakeRemote(prdsDir, { '1-wave1-tail': 'pending' });
154
+ process.env.SM_SCHEDULER_JOB_SLUG = 'some-running-job';
155
+
156
+ const result = await createPrd(validBody({ sourcePromptId: 'epic-1' }), remote);
157
+
158
+ expect(result.ok).toBe(true);
159
+ const written = await fsp.readFile(path.join(prdsDir, result.filename), 'utf8');
160
+ expect(written).toMatch(/^dependsOn: \[1-wave1-tail\]$/m);
161
+ expect(written).toMatch(/^disposition: append$/m);
162
+ });
163
+
164
+ test('no disposition required for the first PRD in a brand-new Epic', async () => {
165
+ const prdsDir = await mkTmpPrdsDir();
166
+ const remote = makeFakeRemote(prdsDir);
167
+
168
+ const result = await createPrd(validBody({ sourcePromptId: 'epic-empty' }), remote);
169
+
170
+ expect(result.ok).toBe(true);
171
+ const written = await fsp.readFile(path.join(prdsDir, result.filename), 'utf8');
172
+ expect(written).not.toContain('disposition');
173
+ });
174
+
175
+ test('no disposition required when every existing PRD in the Epic is already completed', async () => {
176
+ const prdsDir = await mkTmpPrdsDir();
177
+ await seedPrd(prdsDir, '1-wave1-tail', { sourcePromptId: 'epic-done' });
178
+ const remote = makeFakeRemote(prdsDir, { '1-wave1-tail': 'completed' });
179
+
180
+ const result = await createPrd(validBody({ sourcePromptId: 'epic-done' }), remote);
181
+
182
+ expect(result.ok).toBe(true);
183
+ const written = await fsp.readFile(path.join(prdsDir, result.filename), 'utf8');
184
+ expect(written).not.toContain('disposition');
185
+ });
186
+
187
+ test('an explicit dependsOn bypasses the disposition gate entirely', async () => {
188
+ const prdsDir = await mkTmpPrdsDir();
189
+ await seedPrd(prdsDir, '1-wave1-tail', { sourcePromptId: 'epic-1' });
190
+ const remote = makeFakeRemote(prdsDir, { '1-wave1-tail': 'pending' });
191
+
192
+ const result = await createPrd(
193
+ validBody({ sourcePromptId: 'epic-1', dependsOn: ['1-wave1-tail'] }),
194
+ remote,
195
+ );
196
+
197
+ expect(result.ok).toBe(true);
198
+ const written = await fsp.readFile(path.join(prdsDir, result.filename), 'utf8');
199
+ expect(written).toMatch(/^dependsOn: \[1-wave1-tail\]$/m);
200
+ expect(written).not.toContain('disposition');
201
+ });
@@ -0,0 +1,125 @@
1
+ /**
2
+ * prdFrontmatterDisposition.test.cjs — scheduler wave-disposition PRD:
3
+ * `disposition` ('append' | 'new-head') is a recognized, patchable PRD
4
+ * frontmatter key, same pattern as `dependsOn` in
5
+ * prdFrontmatterDependsOn.test.cjs. It records the authoring-time decision
6
+ * for a wave's root PRD: does it extend the Epic's existing dependsOn chain
7
+ * (append) or start an independent, parallel-eligible chain (new-head)?
8
+ *
9
+ * Run: timeout 120 npx vitest run src/main/__tests__/prdFrontmatterDisposition.test.cjs
10
+ */
11
+
12
+ 'use strict';
13
+
14
+ import { test, expect } from 'vitest';
15
+
16
+ const { parsePrdFile, serializePrdFile, RECOGNIZED_KEYS } = require('../lib/prdFrontmatter.cjs');
17
+
18
+ test('disposition is a recognized key', () => {
19
+ expect(RECOGNIZED_KEYS.has('disposition')).toBe(true);
20
+ });
21
+
22
+ test('parsePrdFile parses disposition: append and round-trips it byte-identically when unedited', () => {
23
+ const raw = [
24
+ '---',
25
+ 'title: A second wave',
26
+ 'cwd: ~/Projects/session-manager',
27
+ 'estimateMinutes: 20',
28
+ 'dependsOn: [widget-base]',
29
+ 'disposition: append',
30
+ '---',
31
+ '# Goal',
32
+ '',
33
+ 'Extend the chain.',
34
+ '',
35
+ ].join('\n');
36
+
37
+ const { frontmatter, body } = parsePrdFile(raw);
38
+ expect(frontmatter.disposition).toBe('append');
39
+ expect(serializePrdFile(frontmatter, body)).toBe(raw);
40
+ });
41
+
42
+ test('parsePrdFile parses disposition: new-head', () => {
43
+ const raw = [
44
+ '---',
45
+ 'title: A parallel wave',
46
+ 'cwd: ~/Projects/session-manager',
47
+ 'estimateMinutes: 20',
48
+ 'disposition: new-head',
49
+ '---',
50
+ 'body',
51
+ '',
52
+ ].join('\n');
53
+
54
+ const { frontmatter } = parsePrdFile(raw);
55
+ expect(frontmatter.disposition).toBe('new-head');
56
+ });
57
+
58
+ test('parsePrdFile leaves disposition unset for an unrecognized value rather than throwing', () => {
59
+ const raw = [
60
+ '---',
61
+ 'title: A hand-edited PRD',
62
+ 'cwd: ~/Projects/session-manager',
63
+ 'estimateMinutes: 20',
64
+ 'disposition: sideways',
65
+ '---',
66
+ 'body',
67
+ '',
68
+ ].join('\n');
69
+
70
+ const { frontmatter } = parsePrdFile(raw);
71
+ expect(frontmatter.disposition).toBeUndefined();
72
+ });
73
+
74
+ test('parsePrdFile leaves disposition undefined when omitted — a PRD authored before this field existed round-trips unaffected', () => {
75
+ const raw = [
76
+ '---',
77
+ 'title: An older PRD',
78
+ 'cwd: ~/Projects/session-manager',
79
+ 'estimateMinutes: 10',
80
+ '---',
81
+ '# Goal',
82
+ '',
83
+ 'Do the thing.',
84
+ '',
85
+ ].join('\n');
86
+
87
+ const { frontmatter, body } = parsePrdFile(raw);
88
+ expect(frontmatter.disposition).toBeUndefined();
89
+ expect(serializePrdFile(frontmatter, body)).toBe(raw);
90
+ });
91
+
92
+ test('serializePrdFile emits a newly-set disposition onto a frontmatter object that lacked one', () => {
93
+ const raw = [
94
+ '---',
95
+ 'title: Legacy PRD',
96
+ 'cwd: ~/Projects/session-manager',
97
+ 'estimateMinutes: 5',
98
+ '---',
99
+ 'body',
100
+ '',
101
+ ].join('\n');
102
+
103
+ const { frontmatter, body } = parsePrdFile(raw);
104
+ const updated = serializePrdFile({ ...frontmatter, disposition: 'new-head' }, body);
105
+ expect(updated).toContain('disposition: new-head');
106
+ });
107
+
108
+ test('serializePrdFile patches an existing disposition (promoting append to new-head)', () => {
109
+ const raw = [
110
+ '---',
111
+ 'title: A second wave',
112
+ 'cwd: ~/Projects/session-manager',
113
+ 'estimateMinutes: 20',
114
+ 'dependsOn: [widget-base]',
115
+ 'disposition: append',
116
+ '---',
117
+ 'body',
118
+ '',
119
+ ].join('\n');
120
+
121
+ const { frontmatter, body } = parsePrdFile(raw);
122
+ const updated = serializePrdFile({ ...frontmatter, dependsOn: [], disposition: 'new-head' }, body);
123
+ expect(updated).toContain('disposition: new-head');
124
+ expect(updated).not.toContain('dependsOn');
125
+ });