claude-code-session-manager 0.97.0 → 0.100.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 (430) hide show
  1. package/README.md +3 -3
  2. package/bin/cli.cjs +16 -4
  3. package/bin/node-floor.cjs +41 -0
  4. package/dist/assets/{DataModel-D93FoaOm.js → DataModel-HdR0AxFq.js} +1 -1
  5. package/dist/assets/{History-CXxxqNP1.js → History-Dr4Jr_5z.js} +2 -2
  6. package/dist/assets/{Hooks-CzeYFQjq.js → Hooks-NhrvH3JP.js} +3 -3
  7. package/dist/assets/{Library-D2zPkwOC.js → Library-Dv_EtIMr.js} +1 -1
  8. package/dist/assets/{MarkdownEditor-DbinP78A.js → MarkdownEditor-DTKDWV_n.js} +1 -1
  9. package/dist/assets/{McpServers-Bu_sVGLr.js → McpServers-M034G9B3.js} +2 -2
  10. package/dist/assets/{Memory-BgiI6LZ_.js → Memory-DzF1B4gx.js} +6 -6
  11. package/dist/assets/{Permissions-C1MMzZRm.js → Permissions-DrgbSGWl.js} +3 -3
  12. package/dist/assets/{Plugins-Dhu6wAZi.js → Plugins-Bd3ef35w.js} +2 -2
  13. package/dist/assets/{ProvenanceBadge-C3AcWKMG.js → ProvenanceBadge-DcBHT1Iy.js} +1 -1
  14. package/dist/assets/{SaveBar-plwYm0iO.js → SaveBar-rimJem-Y.js} +1 -1
  15. package/dist/assets/Scheduler-DjgFJl2w.js +16 -0
  16. package/dist/assets/{ScopeSwitcher-DFy0vv_o.js → ScopeSwitcher-CnAzTBrl.js} +1 -1
  17. package/dist/assets/Settings-Dc1F_sSg.js +3 -0
  18. package/dist/assets/{SkillReferenceGraph-DsI7hRPB.js → SkillReferenceGraph-B9aZDecM.js} +1 -1
  19. package/dist/assets/{Skills-3E748UfB.js → Skills-DRXd18gu.js} +2 -2
  20. package/dist/assets/{SystemPrompt-BqmfUJOd.js → SystemPrompt-ahy5-Fwe.js} +1 -1
  21. package/dist/assets/{TagLibrary-CPz6fO8Q.js → TagLibrary-C_tToDeL.js} +1 -1
  22. package/dist/assets/{TiptapBody-D9EoBQ1P.js → TiptapBody-DvdefS6K.js} +1 -1
  23. package/dist/assets/{Toggle-BHAmZNDn.js → Toggle-6Jl5Njc6.js} +1 -1
  24. package/dist/assets/{index-CKH5Uxik.css → index-Bta-hwud.css} +1 -1
  25. package/dist/assets/{index-tn5JIVPj.js → index-vx8O73l8.js} +497 -502
  26. package/dist/assets/{settingsSchema-Crqy_aNz.js → settingsSchema-D6iRguNV.js} +1 -1
  27. package/dist/index.html +2 -2
  28. package/package.json +26 -29
  29. package/plugins/CLAUDE.md +6 -6
  30. package/plugins/session-manager-dev/skills/builder/4-manual/SKILL.md +23 -19
  31. package/plugins/session-manager-dev/skills/builder/SKILL.md +3 -3
  32. package/plugins/session-manager-dev/skills/develop/SKILL.md +254 -510
  33. package/plugins/session-manager-dev/skills/develop/standards.md +17 -22
  34. package/scripts/README.md +3 -11
  35. package/scripts/audit-ops-hygiene.cjs +3 -3
  36. package/scripts/hooks/lib/guard-prd-writes-policy.cjs +1 -1
  37. package/scripts/hooks/lib/guard-self-schedule-policy.cjs +1 -1
  38. package/scripts/mint-epic.cjs +2 -3
  39. package/scripts/scheduler-mcp-server.cjs +36 -7
  40. package/src/main/agentLibrary.cjs +22 -2
  41. package/src/main/build-info.json +4 -4
  42. package/src/main/config.cjs +41 -38
  43. package/src/main/docEdit.cjs +4 -1
  44. package/src/main/files.cjs +2 -5
  45. package/src/main/health.cjs +1 -1
  46. package/src/main/index.cjs +23 -10
  47. package/src/main/ipcSchemas.cjs +28 -35
  48. package/src/main/lib/agentPersonaSchema.cjs +13 -2
  49. package/src/main/lib/atomicFs.cjs +116 -0
  50. package/src/main/lib/branchSweep.cjs +13 -12
  51. package/src/main/lib/buildTarget.cjs +3 -3
  52. package/src/main/lib/claudeCliCaps.cjs +129 -0
  53. package/src/main/lib/credentials.cjs +2 -4
  54. package/src/main/lib/crossProjectFeedback.cjs +3 -3
  55. package/src/main/lib/cwdClassify.cjs +15 -3
  56. package/src/main/lib/definitionOfDone.cjs +207 -125
  57. package/src/main/lib/dodDrainHook.cjs +11 -5
  58. package/src/main/lib/epicMint.cjs +2 -4
  59. package/src/main/lib/epicStatusMirror.cjs +8 -10
  60. package/src/main/lib/epicWorktreeProjectConfig.cjs +2 -4
  61. package/src/main/lib/gateAuthority.cjs +96 -0
  62. package/src/main/lib/gitExec.cjs +69 -0
  63. package/src/main/lib/gitWorktree.cjs +656 -92
  64. package/src/main/lib/instanceLock.cjs +5 -8
  65. package/src/main/lib/launchFailure.cjs +2 -3
  66. package/src/main/lib/macroLibrary.cjs +386 -0
  67. package/src/main/lib/mcpToolCatalog.cjs +26 -17
  68. package/src/main/lib/needsReviewLedger.cjs +1 -1
  69. package/src/main/lib/opsErrorLog.cjs +11 -1
  70. package/src/main/lib/opsOwnership.cjs +0 -5
  71. package/src/main/lib/pidAlive.cjs +32 -0
  72. package/src/main/lib/prdCreate.cjs +153 -19
  73. package/src/main/lib/prdDisposition.cjs +2 -2
  74. package/src/main/lib/prdGateFiles.cjs +284 -0
  75. package/src/main/lib/prdLocations.cjs +61 -0
  76. package/src/main/lib/prdMigration.cjs +2 -3
  77. package/src/main/lib/prdSizing.cjs +2 -1
  78. package/src/main/lib/promptSessionSchema.cjs +10 -2
  79. package/src/main/lib/rcaReport.cjs +4 -6
  80. package/src/main/lib/reaperHelpers.cjs +8 -1
  81. package/src/main/lib/reservationExpiry.cjs +6 -1
  82. package/src/main/lib/reviewNotice.cjs +263 -0
  83. package/src/main/lib/runClaudeP.cjs +4 -1
  84. package/src/main/lib/schedulerPaths.cjs +22 -2
  85. package/src/main/lib/sessionSlots.cjs +2 -4
  86. package/src/main/lib/shippedPersonaSeeds.cjs +179 -0
  87. package/src/main/lib/timeoutShim.cjs +123 -0
  88. package/src/main/lib/timeoutShimScript.cjs +275 -0
  89. package/src/main/lib/upgradeDrain.cjs +4 -10
  90. package/src/main/lib/watchdogHelpers.cjs +33 -30
  91. package/src/main/lib/workTypeLibrary.cjs +7 -1
  92. package/src/main/promptSessionEvents.cjs +37 -13
  93. package/src/main/queueOps.cjs +17 -6
  94. package/src/main/scheduler.cjs +975 -203
  95. package/src/main/seedAgentPersonas.cjs +278 -23
  96. package/src/main/supervisor.cjs +4 -1
  97. package/src/main/templates/PRD_AUTHORING.md +126 -406
  98. package/src/preload/__tests__/preload-surface.test.cjs +68 -0
  99. package/src/preload/api.d.ts +41 -94
  100. package/src/preload/index.cjs +14 -9
  101. package/src/seed/agents/architect.md +1 -0
  102. package/src/seed/agents/dev-lead.md +17 -18
  103. package/src/seed/agents/project-home-builder.md +1 -0
  104. package/src/seed/agents/validator.md +10 -4
  105. package/dist/assets/HostBilko-nESrTRIg.js +0 -1
  106. package/dist/assets/Scheduler-BVYTeh39.js +0 -16
  107. package/dist/assets/Settings-DIhGgdfs.js +0 -3
  108. package/src/main/__tests__/activeIndexMerge.test.cjs +0 -235
  109. package/src/main/__tests__/agentEffortResolve.test.cjs +0 -117
  110. package/src/main/__tests__/agentLibrary.test.cjs +0 -276
  111. package/src/main/__tests__/agentModelResolve.test.cjs +0 -311
  112. package/src/main/__tests__/agentOverlayWrite.test.cjs +0 -95
  113. package/src/main/__tests__/bilkoHost-deriveSlug.test.cjs +0 -26
  114. package/src/main/__tests__/bilkoHost-integration.test.cjs +0 -118
  115. package/src/main/__tests__/bilkoHostCore.test.cjs +0 -72
  116. package/src/main/__tests__/broadcastCoalescer.test.cjs +0 -122
  117. package/src/main/__tests__/chat-cancel-terminal.test.cjs +0 -120
  118. package/src/main/__tests__/chat-dead-channels.test.cjs +0 -63
  119. package/src/main/__tests__/chat-exit-close-race.test.cjs +0 -146
  120. package/src/main/__tests__/chat-mcp-consent-notice.test.cjs +0 -139
  121. package/src/main/__tests__/chat-preamble-anchors.test.cjs +0 -100
  122. package/src/main/__tests__/chat-queue.test.cjs +0 -97
  123. package/src/main/__tests__/chat-stop-signal.test.cjs +0 -89
  124. package/src/main/__tests__/chatRunner-epic-worktree-execcwd.test.cjs +0 -125
  125. package/src/main/__tests__/chatRunner-session-flag-retry.test.cjs +0 -252
  126. package/src/main/__tests__/classifyPromptTicket.test.cjs +0 -101
  127. package/src/main/__tests__/classifyTranscriptLine.test.cjs +0 -201
  128. package/src/main/__tests__/computeDepHistorySatisfaction.test.cjs +0 -66
  129. package/src/main/__tests__/config-readText-bounded.test.cjs +0 -84
  130. package/src/main/__tests__/configWriteBoundaryOwners.test.cjs +0 -59
  131. package/src/main/__tests__/crossProjectFeedback.test.cjs +0 -334
  132. package/src/main/__tests__/crossProjectFeedbackRoutes.test.cjs +0 -161
  133. package/src/main/__tests__/dep-orphan-archive-health.test.cjs +0 -77
  134. package/src/main/__tests__/develop-skill-failure-modes.test.cjs +0 -70
  135. package/src/main/__tests__/docEdit.test.cjs +0 -244
  136. package/src/main/__tests__/dod-batchkey.test.cjs +0 -183
  137. package/src/main/__tests__/dod-drain-hook.test.cjs +0 -302
  138. package/src/main/__tests__/dod-report.test.cjs +0 -304
  139. package/src/main/__tests__/dod-reverify.test.cjs +0 -285
  140. package/src/main/__tests__/epicContextDigest.test.cjs +0 -174
  141. package/src/main/__tests__/epicMint.test.cjs +0 -332
  142. package/src/main/__tests__/epicMintTelemetryTap.test.cjs +0 -64
  143. package/src/main/__tests__/epicStatusMirror.test.cjs +0 -110
  144. package/src/main/__tests__/epicValidationHook.test.cjs +0 -291
  145. package/src/main/__tests__/exchanges.test.cjs +0 -122
  146. package/src/main/__tests__/exchangesPromptId.test.cjs +0 -61
  147. package/src/main/__tests__/extractJson.test.cjs +0 -51
  148. package/src/main/__tests__/files-reject-credentials.test.cjs +0 -40
  149. package/src/main/__tests__/fixtures/1218-fo-01-move-scripts-lib-into-src-main-lib.log +0 -556
  150. package/src/main/__tests__/flatPrdTickSweep.test.cjs +0 -110
  151. package/src/main/__tests__/health-build-freshness.test.cjs +0 -39
  152. package/src/main/__tests__/health-claude-md-budget.test.cjs +0 -57
  153. package/src/main/__tests__/health-credentials.test.cjs +0 -81
  154. package/src/main/__tests__/health-delegation-chain.test.cjs +0 -124
  155. package/src/main/__tests__/health-per-project-stall.test.cjs +0 -84
  156. package/src/main/__tests__/health-prd-migration.test.cjs +0 -37
  157. package/src/main/__tests__/health-queue-dispatch.test.cjs +0 -135
  158. package/src/main/__tests__/health-starve-escalation.test.cjs +0 -94
  159. package/src/main/__tests__/health-tick-liveness.test.cjs +0 -179
  160. package/src/main/__tests__/health-usage-poller.test.cjs +0 -144
  161. package/src/main/__tests__/health-worktree-cap-blocked.test.cjs +0 -65
  162. package/src/main/__tests__/heapSnapshot.test.cjs +0 -121
  163. package/src/main/__tests__/historyAggregatorIntraday.test.cjs +0 -313
  164. package/src/main/__tests__/historyDashboard.test.cjs +0 -163
  165. package/src/main/__tests__/historyRollup.test.cjs +0 -333
  166. package/src/main/__tests__/intradayRefresh.test.cjs +0 -39
  167. package/src/main/__tests__/ipcSchemas-dependsOn.test.cjs +0 -39
  168. package/src/main/__tests__/kg-augment.test.cjs +0 -195
  169. package/src/main/__tests__/loadGateDetailTick.test.cjs +0 -31
  170. package/src/main/__tests__/machineProfile.test.cjs +0 -152
  171. package/src/main/__tests__/mcpStatus.test.cjs +0 -61
  172. package/src/main/__tests__/memoryAggregate.test.cjs +0 -109
  173. package/src/main/__tests__/memoryStale.test.cjs +0 -88
  174. package/src/main/__tests__/needsReviewLedger.test.cjs +0 -162
  175. package/src/main/__tests__/openExternalApp-spawn-error.test.cjs +0 -25
  176. package/src/main/__tests__/opsErrorLog.test.cjs +0 -109
  177. package/src/main/__tests__/opsErrorLogTelemetryTap.test.cjs +0 -173
  178. package/src/main/__tests__/personaMerge.test.cjs +0 -169
  179. package/src/main/__tests__/planValidator.test.cjs +0 -125
  180. package/src/main/__tests__/pollLoop-dispatch-on-failure.test.cjs +0 -176
  181. package/src/main/__tests__/prd-group-allocator.test.cjs +0 -119
  182. package/src/main/__tests__/prdAdminRouteParity.test.cjs +0 -70
  183. package/src/main/__tests__/prdAdminRoutes.test.cjs +0 -718
  184. package/src/main/__tests__/prdAgentType.test.cjs +0 -103
  185. package/src/main/__tests__/prdAuthoringSeed.test.cjs +0 -39
  186. package/src/main/__tests__/prdCreate.test.cjs +0 -979
  187. package/src/main/__tests__/prdCreateDisposition.test.cjs +0 -201
  188. package/src/main/__tests__/prdCreatePlanId.test.cjs +0 -132
  189. package/src/main/__tests__/prdFrontmatterAgentType.test.cjs +0 -117
  190. package/src/main/__tests__/prdFrontmatterDependsOn.test.cjs +0 -136
  191. package/src/main/__tests__/prdFrontmatterDisposition.test.cjs +0 -125
  192. package/src/main/__tests__/prdFrontmatterQuietMachine.test.cjs +0 -108
  193. package/src/main/__tests__/prdLocations.test.cjs +0 -195
  194. package/src/main/__tests__/prdLocationsArchived.test.cjs +0 -201
  195. package/src/main/__tests__/prdMigration.test.cjs +0 -349
  196. package/src/main/__tests__/prdMigrationLegacyAdopt.test.cjs +0 -91
  197. package/src/main/__tests__/prdParserHighWater.test.cjs +0 -74
  198. package/src/main/__tests__/prdParserSourcePromptId.test.cjs +0 -65
  199. package/src/main/__tests__/prdSetDisposition.test.cjs +0 -222
  200. package/src/main/__tests__/prdSizing.test.cjs +0 -106
  201. package/src/main/__tests__/prdSourcePromptIdBackfill.test.cjs +0 -118
  202. package/src/main/__tests__/prdUpdateDependsOn.test.cjs +0 -160
  203. package/src/main/__tests__/proc-role-env.test.cjs +0 -125
  204. package/src/main/__tests__/procname-claude-spawn-sites.test.cjs +0 -304
  205. package/src/main/__tests__/procname-sm-processes.test.cjs +0 -127
  206. package/src/main/__tests__/projectHomeAdminRoutes.test.cjs +0 -177
  207. package/src/main/__tests__/projectPages.test.cjs +0 -137
  208. package/src/main/__tests__/promptSessionEvents.test.cjs +0 -234
  209. package/src/main/__tests__/promptSessionSchema.test.cjs +0 -101
  210. package/src/main/__tests__/promptSessionTranscript.test.cjs +0 -0
  211. package/src/main/__tests__/promptSessionsCreateEpicHandler.test.cjs +0 -159
  212. package/src/main/__tests__/pty-epic-worktree-spawn-cwd.test.cjs +0 -282
  213. package/src/main/__tests__/pty-session-open-telemetry.test.cjs +0 -96
  214. package/src/main/__tests__/pty-write-result.test.cjs +0 -46
  215. package/src/main/__tests__/queue-health-verdict.test.cjs +0 -170
  216. package/src/main/__tests__/queue-starvation-dispatch-driver.test.cjs +0 -286
  217. package/src/main/__tests__/queue-starvation-per-project.test.cjs +0 -147
  218. package/src/main/__tests__/queueHistory.test.cjs +0 -355
  219. package/src/main/__tests__/queueOps-interactive-ac-lint.test.cjs +0 -65
  220. package/src/main/__tests__/queueOpsArchiveDestination.test.cjs +0 -65
  221. package/src/main/__tests__/queueOpsAutoArchive.test.cjs +0 -154
  222. package/src/main/__tests__/rateLimitPollerStreak.test.cjs +0 -128
  223. package/src/main/__tests__/rcaReport.test.cjs +0 -266
  224. package/src/main/__tests__/reconcileFlatPrdSweep.test.cjs +0 -119
  225. package/src/main/__tests__/reconcileTiming.test.cjs +0 -135
  226. package/src/main/__tests__/runLogRetention.test.cjs +0 -489
  227. package/src/main/__tests__/runVerify-atomic-verdicts.test.cjs +0 -26
  228. package/src/main/__tests__/runVerify-blocked-by-foreign-wip.test.cjs +0 -58
  229. package/src/main/__tests__/runVerify-landed-commit-outranks.test.cjs +0 -191
  230. package/src/main/__tests__/runVerify-policy-denial.test.cjs +0 -89
  231. package/src/main/__tests__/runVerify-transcript-commit-evidence.test.cjs +0 -225
  232. package/src/main/__tests__/runVerify.test.cjs +0 -1784
  233. package/src/main/__tests__/scheduleJobSchema.test.cjs +0 -127
  234. package/src/main/__tests__/scheduleJobStatusDrift.test.cjs +0 -65
  235. package/src/main/__tests__/scheduleJobTransitions.test.cjs +0 -277
  236. package/src/main/__tests__/scheduleJobTransitionsGrep.test.cjs +0 -59
  237. package/src/main/__tests__/scheduleJobTransitionsTelemetryTap.test.cjs +0 -72
  238. package/src/main/__tests__/scheduler-admin-routes.test.cjs +0 -199
  239. package/src/main/__tests__/scheduler-adopted-run-supervision.test.cjs +0 -143
  240. package/src/main/__tests__/scheduler-already-satisfied-on-main.test.cjs +0 -105
  241. package/src/main/__tests__/scheduler-archive-completed-prd.test.cjs +0 -102
  242. package/src/main/__tests__/scheduler-archived-twin-guard.test.cjs +0 -155
  243. package/src/main/__tests__/scheduler-autofix-outcome.test.cjs +0 -188
  244. package/src/main/__tests__/scheduler-autofix-select.test.cjs +0 -439
  245. package/src/main/__tests__/scheduler-autopromote.test.cjs +0 -51
  246. package/src/main/__tests__/scheduler-bash-timeout-env.test.cjs +0 -101
  247. package/src/main/__tests__/scheduler-blocked-by-foreign-wip.test.cjs +0 -107
  248. package/src/main/__tests__/scheduler-boot-orphans.test.cjs +0 -153
  249. package/src/main/__tests__/scheduler-broadcast-reconcile.test.cjs +0 -121
  250. package/src/main/__tests__/scheduler-clear-queue-history.test.cjs +0 -134
  251. package/src/main/__tests__/scheduler-commit-guard-noop.test.cjs +0 -244
  252. package/src/main/__tests__/scheduler-committed-in-window.test.cjs +0 -182
  253. package/src/main/__tests__/scheduler-cross-project-batch.test.cjs +0 -43
  254. package/src/main/__tests__/scheduler-default-eligible-heal.test.cjs +0 -168
  255. package/src/main/__tests__/scheduler-dispatch-loop.test.cjs +0 -58
  256. package/src/main/__tests__/scheduler-effective-concurrency.test.cjs +0 -81
  257. package/src/main/__tests__/scheduler-epic-digest.test.cjs +0 -227
  258. package/src/main/__tests__/scheduler-failed-autoreset.test.cjs +0 -121
  259. package/src/main/__tests__/scheduler-finalize-dispatch-guards.test.cjs +0 -229
  260. package/src/main/__tests__/scheduler-find-prd-dir.test.cjs +0 -75
  261. package/src/main/__tests__/scheduler-fix-plan-path.test.cjs +0 -119
  262. package/src/main/__tests__/scheduler-force-tick-outcome.test.cjs +0 -46
  263. package/src/main/__tests__/scheduler-foreign-wip-manifest.test.cjs +0 -78
  264. package/src/main/__tests__/scheduler-gate-shadow.test.cjs +0 -119
  265. package/src/main/__tests__/scheduler-guard-verdict-autoresolve.test.cjs +0 -390
  266. package/src/main/__tests__/scheduler-heal-refusal.test.cjs +0 -61
  267. package/src/main/__tests__/scheduler-heartbeat-payload.test.cjs +0 -80
  268. package/src/main/__tests__/scheduler-inplace-salvage.test.cjs +0 -323
  269. package/src/main/__tests__/scheduler-integration-failure-stamp.test.cjs +0 -41
  270. package/src/main/__tests__/scheduler-investigation-clean-skip.test.cjs +0 -63
  271. package/src/main/__tests__/scheduler-investigation-prompt.test.cjs +0 -123
  272. package/src/main/__tests__/scheduler-job-budget.test.cjs +0 -172
  273. package/src/main/__tests__/scheduler-job-overrun.test.cjs +0 -175
  274. package/src/main/__tests__/scheduler-launch-failure.test.cjs +0 -199
  275. package/src/main/__tests__/scheduler-leftover-fields.test.cjs +0 -52
  276. package/src/main/__tests__/scheduler-leftover-quarantine.test.cjs +0 -199
  277. package/src/main/__tests__/scheduler-looks-done.test.cjs +0 -537
  278. package/src/main/__tests__/scheduler-manual-pause.test.cjs +0 -118
  279. package/src/main/__tests__/scheduler-mechanical-recovery.test.cjs +0 -245
  280. package/src/main/__tests__/scheduler-meta-code-sha.test.cjs +0 -46
  281. package/src/main/__tests__/scheduler-needs-review-autoresolve.test.cjs +0 -197
  282. package/src/main/__tests__/scheduler-never-stop.test.cjs +0 -157
  283. package/src/main/__tests__/scheduler-no-dead-end-status.test.cjs +0 -152
  284. package/src/main/__tests__/scheduler-no-orphan-run-dir.test.cjs +0 -81
  285. package/src/main/__tests__/scheduler-notify-originating-tab-transcript.test.cjs +0 -87
  286. package/src/main/__tests__/scheduler-notify-originating-tab.test.cjs +0 -343
  287. package/src/main/__tests__/scheduler-periodic-reverify-guard.test.cjs +0 -217
  288. package/src/main/__tests__/scheduler-porcelain-rename.test.cjs +0 -164
  289. package/src/main/__tests__/scheduler-prd-missing-skip.test.cjs +0 -161
  290. package/src/main/__tests__/scheduler-prd-persona-spawn.test.cjs +0 -178
  291. package/src/main/__tests__/scheduler-quarantine-autoresolve.test.cjs +0 -165
  292. package/src/main/__tests__/scheduler-quiet-machine-lease.test.cjs +0 -257
  293. package/src/main/__tests__/scheduler-rate-limit-cooldown-freshness.test.cjs +0 -123
  294. package/src/main/__tests__/scheduler-rate-limit-pause.test.cjs +0 -152
  295. package/src/main/__tests__/scheduler-rate-limit-spin-guard.test.cjs +0 -156
  296. package/src/main/__tests__/scheduler-reap-dead-running-jobs.test.cjs +0 -754
  297. package/src/main/__tests__/scheduler-reaper-helpers-basics.test.cjs +0 -87
  298. package/src/main/__tests__/scheduler-reconcile-cwd-preserve.test.cjs +0 -100
  299. package/src/main/__tests__/scheduler-reconcile-history-backfill.test.cjs +0 -105
  300. package/src/main/__tests__/scheduler-reconcile-invalid-repair.test.cjs +0 -203
  301. package/src/main/__tests__/scheduler-reconcile-quarantine.test.cjs +0 -247
  302. package/src/main/__tests__/scheduler-reset-job-fields-guard.test.cjs +0 -77
  303. package/src/main/__tests__/scheduler-resume-recovery.test.cjs +0 -254
  304. package/src/main/__tests__/scheduler-shard-quarantine.test.cjs +0 -115
  305. package/src/main/__tests__/scheduler-shared-tree-guard.test.cjs +0 -380
  306. package/src/main/__tests__/scheduler-sigterm-commit.test.cjs +0 -43
  307. package/src/main/__tests__/scheduler-stall-per-project.test.cjs +0 -126
  308. package/src/main/__tests__/scheduler-starve-escalation.test.cjs +0 -154
  309. package/src/main/__tests__/scheduler-stranded-autofix-park.test.cjs +0 -252
  310. package/src/main/__tests__/scheduler-stranded-investigation.test.cjs +0 -185
  311. package/src/main/__tests__/scheduler-stuck-failed-escalation.test.cjs +0 -136
  312. package/src/main/__tests__/scheduler-supervisor-record.test.cjs +0 -81
  313. package/src/main/__tests__/scheduler-tick-cancel-token.test.cjs +0 -54
  314. package/src/main/__tests__/scheduler-tick-wedge.test.cjs +0 -172
  315. package/src/main/__tests__/scheduler-transient-failure.test.cjs +0 -141
  316. package/src/main/__tests__/scheduler-unreadable-queue-guard.test.cjs +0 -62
  317. package/src/main/__tests__/scheduler-utilization-hold.test.cjs +0 -89
  318. package/src/main/__tests__/scheduler-verify-prd-path.test.cjs +0 -109
  319. package/src/main/__tests__/scheduler-worktree-cap-defer.test.cjs +0 -234
  320. package/src/main/__tests__/scheduler-worktree-exec-cwd.test.cjs +0 -120
  321. package/src/main/__tests__/scheduler-writeprd-epic-rollback.test.cjs +0 -105
  322. package/src/main/__tests__/schedulerBatchRootBlocker.test.cjs +0 -117
  323. package/src/main/__tests__/schedulerStateSidecarRestore.test.cjs +0 -110
  324. package/src/main/__tests__/seedAgentPersonas.test.cjs +0 -184
  325. package/src/main/__tests__/seedSchedulerMcp.test.cjs +0 -211
  326. package/src/main/__tests__/seedStatus.test.cjs +0 -100
  327. package/src/main/__tests__/seedValidatorPersona.test.cjs +0 -116
  328. package/src/main/__tests__/stop-signal-anchor.test.cjs +0 -75
  329. package/src/main/__tests__/telemetryClient.test.cjs +0 -1055
  330. package/src/main/__tests__/telemetryContract.test.cjs +0 -930
  331. package/src/main/__tests__/telemetrySettings.test.cjs +0 -210
  332. package/src/main/__tests__/transcripts-batch-flush.test.cjs +0 -249
  333. package/src/main/__tests__/transcripts-doFlush-array.test.cjs +0 -124
  334. package/src/main/__tests__/transcripts-paged-reads.test.cjs +0 -241
  335. package/src/main/__tests__/transcripts-worktree-epic-path.test.cjs +0 -154
  336. package/src/main/__tests__/transcriptsUsageFor.test.cjs +0 -206
  337. package/src/main/__tests__/uniquePrdNumbers.test.cjs +0 -153
  338. package/src/main/__tests__/usageSingleFlight.test.cjs +0 -169
  339. package/src/main/__tests__/validationSentinels.test.cjs +0 -84
  340. package/src/main/__tests__/workTypeLibrary.test.cjs +0 -89
  341. package/src/main/bilkoHost.cjs +0 -314
  342. package/src/main/bilkoHostCore.cjs +0 -89
  343. package/src/main/lib/__tests__/active-sessions.test.cjs +0 -251
  344. package/src/main/lib/__tests__/activeIndexRebuild.test.cjs +0 -179
  345. package/src/main/lib/__tests__/agentPersonaSchema.test.cjs +0 -67
  346. package/src/main/lib/__tests__/auditLog.test.cjs +0 -38
  347. package/src/main/lib/__tests__/bootSelfHeal.test.cjs +0 -107
  348. package/src/main/lib/__tests__/branchSweep.test.cjs +0 -164
  349. package/src/main/lib/__tests__/buildIdentity.test.cjs +0 -121
  350. package/src/main/lib/__tests__/buildTarget.test.cjs +0 -52
  351. package/src/main/lib/__tests__/childWithLog.test.cjs +0 -321
  352. package/src/main/lib/__tests__/coldBootPromptSessionsWrite.test.cjs +0 -87
  353. package/src/main/lib/__tests__/crashTelemetry.test.cjs +0 -103
  354. package/src/main/lib/__tests__/credentials-futile-refresh.test.cjs +0 -115
  355. package/src/main/lib/__tests__/cwdClassify.test.cjs +0 -111
  356. package/src/main/lib/__tests__/definitionOfDoneSequence.test.cjs +0 -95
  357. package/src/main/lib/__tests__/delegationReadiness.test.cjs +0 -1175
  358. package/src/main/lib/__tests__/dispatchLoop.test.cjs +0 -63
  359. package/src/main/lib/__tests__/effectiveModelInfo.test.cjs +0 -244
  360. package/src/main/lib/__tests__/ephemeralCwd.test.cjs +0 -91
  361. package/src/main/lib/__tests__/epicDelegationStats.test.cjs +0 -137
  362. package/src/main/lib/__tests__/epicSpawnCwd.test.cjs +0 -283
  363. package/src/main/lib/__tests__/epicSpawnPlan.test.cjs +0 -196
  364. package/src/main/lib/__tests__/epicTranscriptPath.test.cjs +0 -163
  365. package/src/main/lib/__tests__/epicWorktreeBoot.test.cjs +0 -136
  366. package/src/main/lib/__tests__/epicWorktreeMerge.test.cjs +0 -130
  367. package/src/main/lib/__tests__/epicWorktreeMint.test.cjs +0 -117
  368. package/src/main/lib/__tests__/epicWorktreeProjectConfig.test.cjs +0 -137
  369. package/src/main/lib/__tests__/fixChainDepth.test.cjs +0 -40
  370. package/src/main/lib/__tests__/fixtures/204-mercury-steam-horse.log.txt +0 -13
  371. package/src/main/lib/__tests__/fixtures/scheduler-machine.json.corrupt-1789147548 +0 -34
  372. package/src/main/lib/__tests__/gateFixtures.json +0 -20
  373. package/src/main/lib/__tests__/gitCacheBound.test.cjs +0 -69
  374. package/src/main/lib/__tests__/gitWorktree.test.cjs +0 -1478
  375. package/src/main/lib/__tests__/gitWorktreeSalvage.test.cjs +0 -107
  376. package/src/main/lib/__tests__/gitWorktreeSalvageDelta.test.cjs +0 -153
  377. package/src/main/lib/__tests__/guardShims.test.cjs +0 -158
  378. package/src/main/lib/__tests__/importReferences.spec.cjs +0 -56
  379. package/src/main/lib/__tests__/instanceLock.test.cjs +0 -173
  380. package/src/main/lib/__tests__/jobSupervisorRecord.test.cjs +0 -78
  381. package/src/main/lib/__tests__/jobWorktree.test.cjs +0 -199
  382. package/src/main/lib/__tests__/jobWorktreeBootLive.test.cjs +0 -82
  383. package/src/main/lib/__tests__/landedSinceRun.test.cjs +0 -133
  384. package/src/main/lib/__tests__/launchFailure.test.cjs +0 -220
  385. package/src/main/lib/__tests__/loadGate.test.cjs +0 -303
  386. package/src/main/lib/__tests__/localAdminHttp.test.cjs +0 -214
  387. package/src/main/lib/__tests__/loopDelay.test.cjs +0 -68
  388. package/src/main/lib/__tests__/mcpToolCatalog.test.cjs +0 -107
  389. package/src/main/lib/__tests__/modelCatalog.test.cjs +0 -202
  390. package/src/main/lib/__tests__/opsOwnership.test.cjs +0 -113
  391. package/src/main/lib/__tests__/opsRootAbsoluteCwd.test.cjs +0 -328
  392. package/src/main/lib/__tests__/opsRootNestedWrite.test.cjs +0 -51
  393. package/src/main/lib/__tests__/opsRootResolve.test.cjs +0 -149
  394. package/src/main/lib/__tests__/prdDeclaredPaths.test.cjs +0 -82
  395. package/src/main/lib/__tests__/prdDisposition.test.cjs +0 -224
  396. package/src/main/lib/__tests__/procIdentity.test.cjs +0 -119
  397. package/src/main/lib/__tests__/procName.test.cjs +0 -92
  398. package/src/main/lib/__tests__/projectBriefCore.test.cjs +0 -216
  399. package/src/main/lib/__tests__/projectRootResolve.test.cjs +0 -148
  400. package/src/main/lib/__tests__/queueHealth.test.cjs +0 -58
  401. package/src/main/lib/__tests__/queueStoreAtomicWrite.test.cjs +0 -88
  402. package/src/main/lib/__tests__/queueStoreMachineStateRecovery.test.cjs +0 -190
  403. package/src/main/lib/__tests__/quietMachineLease.test.cjs +0 -39
  404. package/src/main/lib/__tests__/rateLimitWindow.test.cjs +0 -88
  405. package/src/main/lib/__tests__/reaperHelpers.test.cjs +0 -577
  406. package/src/main/lib/__tests__/schedulerBatchDepends.test.cjs +0 -312
  407. package/src/main/lib/__tests__/schedulerBatchFairness.test.cjs +0 -213
  408. package/src/main/lib/__tests__/schedulerBatchLaunchHold.test.cjs +0 -125
  409. package/src/main/lib/__tests__/schedulerBatchProjectCap.test.cjs +0 -127
  410. package/src/main/lib/__tests__/schedulerBatchQuietMachine.test.cjs +0 -109
  411. package/src/main/lib/__tests__/schedulerMcpServerHeadlessRefusal.test.cjs +0 -71
  412. package/src/main/lib/__tests__/schedulerMcpServerHelp.test.cjs +0 -216
  413. package/src/main/lib/__tests__/schedulerMcpServerProjectHome.test.cjs +0 -183
  414. package/src/main/lib/__tests__/schedulerPaths.test.cjs +0 -226
  415. package/src/main/lib/__tests__/schedulerPathsWorktree.test.cjs +0 -133
  416. package/src/main/lib/__tests__/schedulerRuntimeState.test.cjs +0 -56
  417. package/src/main/lib/__tests__/sessionSlots.test.cjs +0 -144
  418. package/src/main/lib/__tests__/telemetryBacklog.test.cjs +0 -626
  419. package/src/main/lib/__tests__/telemetryBoot.test.cjs +0 -134
  420. package/src/main/lib/__tests__/telemetryConsent.test.cjs +0 -136
  421. package/src/main/lib/__tests__/telemetryCounters.test.cjs +0 -57
  422. package/src/main/lib/__tests__/telemetryCountersMetadataColumn.test.cjs +0 -98
  423. package/src/main/lib/__tests__/terminalRunOutcome.test.cjs +0 -200
  424. package/src/main/lib/__tests__/toolUseClassify.test.cjs +0 -53
  425. package/src/main/lib/__tests__/updateCheck.test.cjs +0 -63
  426. package/src/main/lib/__tests__/upgradeDrain.test.cjs +0 -130
  427. package/src/main/lib/__tests__/usageCircuit.test.cjs +0 -354
  428. package/src/main/lib/__tests__/watchdog-helpers.test.cjs +0 -375
  429. package/src/main/lib/__tests__/watchdog-relaunch.test.cjs +0 -266
  430. package/src/main/lib/kgExchangePairing.cjs +0 -75
@@ -1,979 +0,0 @@
1
- /**
2
- * prdCreate.test.cjs — unit tests for the create-PRD body builder (PRD 549,
3
- * gh-issue-6) and, since PRD 689, its registerAdminRoute create-prd HTTP
4
- * route (moved verbatim out of the former standalone admin HTTP server
5
- * module's handleRequest).
6
- *
7
- * Run: timeout 120 npx vitest run src/main/__tests__/prdCreate.test.cjs
8
- */
9
-
10
- 'use strict';
11
-
12
- import { test, expect, vi } from 'vitest';
13
- const fs = require('node:fs');
14
- const fsp = require('node:fs/promises');
15
- const os = require('node:os');
16
- const path = require('node:path');
17
- const http = require('node:http');
18
- const { execFileSync } = require('node:child_process');
19
- const {
20
- deriveSlugFromTitle,
21
- buildPrdBody,
22
- readStandards,
23
- STANDARDS_PATH,
24
- PRD_CREATE_SLUG_RE,
25
- registerAdminRoute,
26
- createPrd,
27
- resolveSourcePromptIdFromClaudeSession,
28
- } = require('../lib/prdCreate.cjs');
29
- const { createAdminHttp } = require('../lib/localAdminHttp.cjs');
30
- const config = require('../config.cjs');
31
- const prdParser = require('../scheduler/prdParser.cjs');
32
-
33
- test('deriveSlugFromTitle lowercases, kebab-cases, and strips non-alnum runs', () => {
34
- expect(deriveSlugFromTitle('Add Foo Bar!! Baz')).toBe('add-foo-bar-baz');
35
- expect(deriveSlugFromTitle(' leading/trailing ')).toBe('leading-trailing');
36
- });
37
-
38
- test('deriveSlugFromTitle output always satisfies PRD_CREATE_SLUG_RE', () => {
39
- const slug = deriveSlugFromTitle('Some Title 123');
40
- expect(PRD_CREATE_SLUG_RE.test(slug)).toBeTruthy();
41
- });
42
-
43
- test('readStandards reads the real standards.md and returns non-empty text', async () => {
44
- const text = await readStandards();
45
- expect(text.includes('Execution discipline')).toBeTruthy();
46
- });
47
-
48
- test('buildPrdBody emits required frontmatter keys and body sections in order', async () => {
49
- const body = buildPrdBody({
50
- title: 'Do the thing',
51
- cwd: '~/Projects/session-manager',
52
- estimateMinutes: 15,
53
- goal: 'Build the thing.',
54
- acceptanceCriteria: ['thing exists', 'tests pass'],
55
- implementationNotes: 'See file.cjs:10.',
56
- outOfScope: ['not this'],
57
- });
58
-
59
- expect(body.startsWith('---\n')).toBeTruthy();
60
- expect(body).toMatch(/title: Do the thing/);
61
- expect(body).toMatch(/cwd: ~\/Projects\/session-manager/);
62
- expect(body).toMatch(/estimateMinutes: 15/);
63
-
64
- const goalIdx = body.indexOf('# Goal');
65
- const acIdx = body.indexOf('# Acceptance criteria');
66
- const implIdx = body.indexOf('# Implementation notes');
67
- const oosIdx = body.indexOf('# Out of scope');
68
- const standardsIdx = body.indexOf('## Engineering standards');
69
- // sections must appear in Goal -> AC -> Implementation notes -> Out of scope -> Engineering standards order
70
- expect(goalIdx > 0 && goalIdx < acIdx && acIdx < implIdx && implIdx < oosIdx && oosIdx < standardsIdx).toBeTruthy();
71
-
72
- expect(body).toMatch(/- \[ \] thing exists/);
73
- expect(body).toMatch(/- \[ \] tests pass/);
74
- expect(body).toMatch(/- not this/);
75
- // must point at STANDARDS_PATH rather than inline the standards.md content
76
- expect(body.includes(STANDARDS_PATH)).toBeTruthy();
77
- expect(body).toMatch(/Your system prompt carries the ordered run contract/);
78
- // the pointer block mentions "Execution discipline" by name, but must not
79
- // inline the full standards.md prose (e.g. its Performance-section rules)
80
- expect(body.includes('Lay out hot data contiguously')).toBeFalsy();
81
- });
82
-
83
- test('buildPrdBody emits agentType: dev-lead by default when omitted (PRD 1114)', () => {
84
- const body = buildPrdBody({
85
- title: 't', cwd: '~/x', estimateMinutes: 5, goal: 'g',
86
- acceptanceCriteria: ['a'], implementationNotes: 'n',
87
- });
88
- expect(body).toMatch(/^agentType: dev-lead$/m);
89
- });
90
-
91
- test('buildPrdBody emits the supplied agentType instead of the default', () => {
92
- const body = buildPrdBody({
93
- title: 't', cwd: '~/x', estimateMinutes: 5, goal: 'g',
94
- acceptanceCriteria: ['a'], implementationNotes: 'n',
95
- agentType: 'architect',
96
- });
97
- expect(body).toMatch(/^agentType: architect$/m);
98
- });
99
-
100
- test('buildPrdBody omits parallelGroup frontmatter key when not supplied', () => {
101
- const body = buildPrdBody({
102
- title: 't', cwd: '~/x', estimateMinutes: 5, goal: 'g',
103
- acceptanceCriteria: ['a'], implementationNotes: 'n',
104
- });
105
- expect(!/parallelGroup:/.test(body)).toBeTruthy();
106
- });
107
-
108
- test('buildPrdBody writes sourcePromptId into frontmatter when supplied (PRD 749 traceability)', () => {
109
- const body = buildPrdBody({
110
- title: 't', cwd: '~/x', estimateMinutes: 5, goal: 'g',
111
- acceptanceCriteria: ['a'], implementationNotes: 'n',
112
- sourcePromptId: 'ticket-abc-123',
113
- });
114
- expect(body).toMatch(/sourcePromptId: ticket-abc-123/);
115
- });
116
-
117
- test('buildPrdBody omits sourcePromptId frontmatter key when not supplied', () => {
118
- const body = buildPrdBody({
119
- title: 't', cwd: '~/x', estimateMinutes: 5, goal: 'g',
120
- acceptanceCriteria: ['a'], implementationNotes: 'n',
121
- });
122
- expect(!/sourcePromptId:/.test(body)).toBeTruthy();
123
- });
124
-
125
- test('readStandards result is byte-identical to the on-disk file (single source of truth)', async () => {
126
- const onDisk = fs.readFileSync(
127
- path.join(__dirname, '..', '..', '..', 'plugins', 'session-manager-dev', 'skills', 'develop', 'standards.md'),
128
- 'utf8',
129
- );
130
- expect(await readStandards()).toBe(onDisk);
131
- });
132
-
133
- // ──────────────────────────────────────────── create-prd admin route
134
-
135
- function request(port, { method = 'GET', path: reqPath, token, body }) {
136
- return new Promise((resolve, reject) => {
137
- const headers = {};
138
- if (token !== undefined) headers.Authorization = `Bearer ${token}`;
139
- let payload;
140
- if (body !== undefined) {
141
- payload = JSON.stringify(body);
142
- headers['Content-Type'] = 'application/json';
143
- headers['Content-Length'] = Buffer.byteLength(payload);
144
- }
145
- const req = http.request({ hostname: '127.0.0.1', port, method, path: reqPath, headers }, (res) => {
146
- const chunks = [];
147
- res.on('data', (c) => chunks.push(c));
148
- res.on('end', () => {
149
- const text = Buffer.concat(chunks).toString('utf8');
150
- let json = null;
151
- try { json = JSON.parse(text); } catch { /* leave null for non-JSON bodies */ }
152
- resolve({ status: res.statusCode, json, text });
153
- });
154
- });
155
- req.on('error', reject);
156
- if (payload) req.write(payload);
157
- req.end();
158
- });
159
- }
160
-
161
- // config.cjs's validateWrite only allows writes under a registered non-home
162
- // allowedRoot's `.claude/` subtree (mirrors the real ROOT layout, which is
163
- // $HOME/.claude/session-manager/...) — so the temp prdsDir must live under
164
- // `<root>/.claude/...`, not directly under the temp root itself.
165
- async function mkTmpPrdsDir() {
166
- const root = await fsp.mkdtemp(path.join(os.tmpdir(), 'sm-admin-create-prd-'));
167
- config.addAllowedRoot(root);
168
- const prdsDir = path.join(root, '.claude', 'prds');
169
- await fsp.mkdir(prdsDir, { recursive: true });
170
- return prdsDir;
171
- }
172
-
173
- // Mirrors scheduler.cjs's real `remote.allocateParallelGroup` / `remote.writePrd`
174
- // (same underlying prdParser.allocateParallelGroup + config.writeTextAtomic
175
- // calls) but pointed at a throwaway temp dir instead of the user's real
176
- // scheduled-plans/prds — so these tests never touch $HOME/.claude.
177
- function makeFakeRemoteWithPrdsDir(prdsDir) {
178
- return {
179
- async allocateParallelGroup() {
180
- return prdParser.allocateParallelGroup(prdsDir);
181
- },
182
- async readPrd(slug) {
183
- try {
184
- const text = await fsp.readFile(path.join(prdsDir, `${slug}.md`), 'utf8');
185
- return { ok: true, text };
186
- } catch (e) {
187
- return { ok: false, error: e?.message };
188
- }
189
- },
190
- async writePrd(slug, body) {
191
- try {
192
- const filePath = path.join(prdsDir, `${slug}.md`);
193
- await config.writeTextAtomic(filePath, body, { writer: 'scheduler' });
194
- return { ok: true };
195
- } catch (e) {
196
- return { ok: false, error: e?.message };
197
- }
198
- },
199
- // Minimal stand-in for scheduler.cjs's real remote.listPrds: every
200
- // `<slug>.md` under the fake project's prdsDir, as a `{ slug }` row —
201
- // enough for prdCreate.cjs's dependsOn-existence validation, which only
202
- // ever reads `.slug` off each entry.
203
- async listPrds() {
204
- const entries = await fsp.readdir(prdsDir);
205
- const prds = entries
206
- .filter((f) => f.endsWith('.md'))
207
- .map((f) => ({ slug: f.slice(0, -3) }));
208
- return { prds, total: prds.length, limit: prds.length, offset: 0, hasMore: false };
209
- },
210
- };
211
- }
212
-
213
- /** Pre-seed the fake project's prdsDir with an existing (empty-body) PRD file at `slug`. */
214
- async function seedExistingPrd(prdsDir, slug) {
215
- await fsp.writeFile(path.join(prdsDir, `${slug}.md`), '---\ntitle: seed\n---\n# Goal\n');
216
- }
217
-
218
- function validCreateBody(overrides = {}) {
219
- return {
220
- title: 'Add widget frobnication',
221
- cwd: os.homedir(),
222
- estimateMinutes: 15,
223
- goal: 'Add frobnication to the widget subsystem.',
224
- acceptanceCriteria: ['widget frobnicates on click', 'timeout 300 npm run typecheck passes'],
225
- implementationNotes: 'See src/widget.cjs:10.',
226
- outOfScope: ['not touching gadgets'],
227
- ...overrides,
228
- };
229
- }
230
-
231
- async function startWithRemote(remote) {
232
- // Force the e2e-suffixed token path so this suite never races the real
233
- // ~/.claude/session-manager/admin-api.json against other test files (or a
234
- // live production instance) that also call createAdminHttp() concurrently.
235
- const prevE2e = process.env.SM_E2E;
236
- process.env.SM_E2E = '1';
237
- const admin = createAdminHttp();
238
- registerAdminRoute(admin, remote);
239
- const { port, token } = await admin.start();
240
- if (prevE2e === undefined) delete process.env.SM_E2E; else process.env.SM_E2E = prevE2e;
241
- return { admin, port, token };
242
- }
243
-
244
- test('POST /admin/scheduler/create-prd without token returns 401', async () => {
245
- const prdsDir = await mkTmpPrdsDir();
246
- const remote = makeFakeRemoteWithPrdsDir(prdsDir);
247
- const { admin, port } = await startWithRemote(remote);
248
- try {
249
- const res = await request(port, {
250
- method: 'POST', path: '/admin/scheduler/create-prd', body: validCreateBody(),
251
- });
252
- expect(res.status).toBe(401);
253
- } finally {
254
- await admin.stop();
255
- }
256
- });
257
-
258
- test('POST /admin/scheduler/create-prd with a valid payload writes a file with frontmatter + standards appended, returns {nn, filename, enqueued:false}', async () => {
259
- const prdsDir = await mkTmpPrdsDir();
260
- const remote = makeFakeRemoteWithPrdsDir(prdsDir);
261
- const { admin, port, token } = await startWithRemote(remote);
262
- try {
263
- const res = await request(port, {
264
- method: 'POST', path: '/admin/scheduler/create-prd', token, body: validCreateBody(),
265
- });
266
- expect(res.status).toBe(200);
267
- // Never a 'status' field, and never the literal 'queued' — this route
268
- // only writes the PRD file; the queue row is derived by the next
269
- // reconcile() pass, not created here. See prdCreate.cjs's createPrd.
270
- expect(res.json.status).toBeUndefined();
271
- expect(res.json.enqueued).toBe(false);
272
- expect(typeof res.json.note).toBe('string');
273
- expect(typeof res.json.nn).toBe('number');
274
- expect(res.json.filename).toMatch(/^\d+-add-widget-frobnication\.md$/);
275
-
276
- const written = await fsp.readFile(path.join(prdsDir, res.json.filename), 'utf8');
277
- expect(written).toMatch(/^---\n/);
278
- expect(written).toMatch(/title: Add widget frobnication/);
279
- expect(written).toMatch(/cwd: .+/);
280
- expect(written).toMatch(/estimateMinutes: 15/);
281
- expect(written).toMatch(/# Goal/);
282
- expect(written).toMatch(/# Acceptance criteria/);
283
- expect(written).toMatch(/- \[ \] widget frobnicates on click/);
284
- expect(written).toMatch(/# Implementation notes/);
285
- expect(written).toMatch(/# Out of scope/);
286
- expect(written).toMatch(/## Engineering standards/);
287
- // must point at STANDARDS_PATH rather than inline standards.md
288
- expect(written.includes(STANDARDS_PATH)).toBeTruthy();
289
- expect(written.includes('Lay out hot data contiguously')).toBeFalsy();
290
- } finally {
291
- await admin.stop();
292
- }
293
- });
294
-
295
- test('POST /admin/scheduler/create-prd with malformed frontmatter (missing acceptanceCriteria) is rejected and writes no file', async () => {
296
- const prdsDir = await mkTmpPrdsDir();
297
- const remote = makeFakeRemoteWithPrdsDir(prdsDir);
298
- const { admin, port, token } = await startWithRemote(remote);
299
- try {
300
- const body = validCreateBody();
301
- delete body.acceptanceCriteria;
302
- const res = await request(port, {
303
- method: 'POST', path: '/admin/scheduler/create-prd', token, body,
304
- });
305
- expect(res.status).toBe(400);
306
- expect(res.json.ok).toBe(false);
307
- const entries = await fsp.readdir(prdsDir);
308
- expect(entries.filter((f) => f.endsWith('.md')).length).toBe(0);
309
- } finally {
310
- await admin.stop();
311
- }
312
- });
313
-
314
- test('POST /admin/scheduler/create-prd with cwd outside allowed roots is rejected and writes no file', async () => {
315
- const prdsDir = await mkTmpPrdsDir();
316
- const remote = makeFakeRemoteWithPrdsDir(prdsDir);
317
- const { admin, port, token } = await startWithRemote(remote);
318
- try {
319
- const res = await request(port, {
320
- method: 'POST', path: '/admin/scheduler/create-prd', token,
321
- body: validCreateBody({ cwd: '/etc/passwd-adjacent-outside-home' }),
322
- });
323
- expect(res.status).toBe(400);
324
- expect(res.json.ok).toBe(false);
325
- const entries = await fsp.readdir(prdsDir);
326
- expect(entries.filter((f) => f.endsWith('.md')).length).toBe(0);
327
- } finally {
328
- await admin.stop();
329
- }
330
- });
331
-
332
- test('POST /admin/scheduler/create-prd rejects a title containing a newline (frontmatter-injection guard) and writes no file', async () => {
333
- const prdsDir = await mkTmpPrdsDir();
334
- const remote = makeFakeRemoteWithPrdsDir(prdsDir);
335
- const { admin, port, token } = await startWithRemote(remote);
336
- try {
337
- const res = await request(port, {
338
- method: 'POST', path: '/admin/scheduler/create-prd', token,
339
- body: validCreateBody({ title: 'Legit title\n---\nEvil: injected' }),
340
- });
341
- expect(res.status).toBe(400);
342
- expect(res.json.ok).toBe(false);
343
- const entries = await fsp.readdir(prdsDir);
344
- expect(entries.filter((f) => f.endsWith('.md')).length).toBe(0);
345
- } finally {
346
- await admin.stop();
347
- }
348
- });
349
-
350
- test('PRD 832: an explicit parallelGroup colliding with an existing file is ignored — a fresh unique NN is allocated and nothing is clobbered', async () => {
351
- const prdsDir = await mkTmpPrdsDir();
352
- const remote = makeFakeRemoteWithPrdsDir(prdsDir);
353
- const { admin, port, token } = await startWithRemote(remote);
354
- try {
355
- await fsp.writeFile(path.join(prdsDir, '777-my-explicit-slug.md'), 'ORIGINAL CONTENT\n');
356
- const res = await request(port, {
357
- method: 'POST', path: '/admin/scheduler/create-prd', token,
358
- body: validCreateBody({ slug: 'my-explicit-slug', parallelGroup: 777 }),
359
- });
360
- expect(res.status).toBe(200);
361
- // Allocator scans past the existing 777 — unique per project, never reused.
362
- expect(res.json.nn).toBe(778);
363
- expect(res.json.filename).toBe('778-my-explicit-slug.md');
364
- const stillThere = await fsp.readFile(path.join(prdsDir, '777-my-explicit-slug.md'), 'utf8');
365
- expect(stillThere).toBe('ORIGINAL CONTENT\n');
366
- } finally {
367
- await admin.stop();
368
- }
369
- });
370
-
371
- test('PRD 832: explicit parallelGroup input is deprecated and ignored — NN always comes from the allocator', async () => {
372
- const prdsDir = await mkTmpPrdsDir();
373
- const remote = makeFakeRemoteWithPrdsDir(prdsDir);
374
- const { admin, port, token } = await startWithRemote(remote);
375
- try {
376
- const res = await request(port, {
377
- method: 'POST', path: '/admin/scheduler/create-prd', token,
378
- body: validCreateBody({ slug: 'my-explicit-slug', parallelGroup: 777 }),
379
- });
380
- expect(res.status).toBe(200);
381
- expect(res.json.nn).toBe(1);
382
- expect(res.json.filename).toBe('1-my-explicit-slug.md');
383
- expect(fs.existsSync(path.join(prdsDir, '1-my-explicit-slug.md'))).toBeTruthy();
384
- } finally {
385
- await admin.stop();
386
- }
387
- });
388
-
389
- // ──────────────────────────────────────────── createPrd() (chat:create-prd IPC handler, PRD 749 follow-up)
390
- //
391
- // createPrd() is the function ipcMain.handle('chat:create-prd', ...) in
392
- // index.cjs calls directly (in-process, no HTTP) — same function
393
- // registerAdminRoute's HTTP handler calls above. These tests exercise it
394
- // directly, mirroring the handler's own call shape: createPrd(input, remote).
395
-
396
- test('createPrd rejects a cwd outside allowedRoots before any write, returning {ok:false, status:400}', async () => {
397
- const prdsDir = await mkTmpPrdsDir();
398
- const remote = makeFakeRemoteWithPrdsDir(prdsDir);
399
- const writePrdSpy = vi.fn(remote.writePrd);
400
- remote.writePrd = writePrdSpy;
401
-
402
- const result = await createPrd(validCreateBody({ cwd: '/etc/passwd-adjacent-outside-home' }), remote);
403
-
404
- expect(result.ok).toBe(false);
405
- expect(result.status).toBe(400);
406
- expect(writePrdSpy).not.toHaveBeenCalled();
407
- const entries = await fsp.readdir(prdsDir);
408
- expect(entries.filter((f) => f.endsWith('.md')).length).toBe(0);
409
- });
410
-
411
- test('createPrd with a valid payload allocates a group, writes through remote.writePrd, and returns {ok:true, nn, filename}', async () => {
412
- const prdsDir = await mkTmpPrdsDir();
413
- const remote = makeFakeRemoteWithPrdsDir(prdsDir);
414
-
415
- const result = await createPrd(validCreateBody({ sourcePromptId: 'ticket-abc', sourceTabId: 'tab-1' }), remote);
416
-
417
- expect(result.ok).toBe(true);
418
- expect(typeof result.nn).toBe('number');
419
- expect(result.filename).toMatch(/^\d+-add-widget-frobnication\.md$/);
420
- // PRD 1403: sizing warnings ride along in the success response, advisory only.
421
- expect(Array.isArray(result.warnings)).toBe(true);
422
-
423
- const written = await fsp.readFile(path.join(prdsDir, result.filename), 'utf8');
424
- expect(written).toMatch(/^---\n/);
425
- expect(written).toMatch(/title: Add widget frobnication/);
426
- expect(written).toMatch(/sourcePromptId: ticket-abc/);
427
- expect(written).toMatch(/sourceTabId: tab-1/);
428
- expect(written).toMatch(/# Goal/);
429
- });
430
-
431
- test('createPrd returns {ok:false, status:409} without clobbering when the destination already exists', async () => {
432
- const prdsDir = await mkTmpPrdsDir();
433
- // The existence guard survives PRD 832: whatever NN the allocator picks,
434
- // a pre-existing file at that exact destination must 409, never clobber.
435
- const remote = makeFakeRemoteWithPrdsDir(prdsDir);
436
- const realRead = remote.readPrd.bind(remote);
437
- remote.readPrd = async (slug) => (slug.endsWith('my-explicit-slug') ? { ok: true, text: 'ORIGINAL' } : realRead(slug));
438
- const writes = [];
439
- const realWrite = remote.writePrd.bind(remote);
440
- remote.writePrd = async (...a) => { writes.push(a[0]); return realWrite(...a); };
441
-
442
- const result = await createPrd(validCreateBody({ slug: 'my-explicit-slug' }), remote);
443
-
444
- expect(result.ok).toBe(false);
445
- expect(result.status).toBe(409);
446
- expect(writes).toEqual([]);
447
- });
448
-
449
- // ──────────────────────────────────────────── PRD 825: Epic PRD dir write grant + `~` cwd normalization
450
-
451
- test('validateWrite allows writes under <root>/session-manager-operations/scheduler/epics/<id>/prds/', async () => {
452
- const root = await fsp.mkdtemp(path.join(os.tmpdir(), 'sm-scheduler-epics-'));
453
- config.addAllowedRoot(root);
454
- const prdsDir = path.join(root, 'session-manager-operations', 'scheduler', 'epics', 'epic-abc123', 'prds');
455
- await fsp.mkdir(prdsDir, { recursive: true });
456
- const filePath = path.join(prdsDir, '1-do-thing.md');
457
-
458
- // Writes into scheduler/ must declare the owning surface (single-writer law).
459
- await config.writeTextAtomic(filePath, 'PRD body\n', { writer: 'scheduler' });
460
-
461
- expect(fs.existsSync(filePath)).toBeTruthy();
462
- expect(await fsp.readFile(filePath, 'utf8')).toBe('PRD body\n');
463
- });
464
-
465
- test('createPrd normalizes a `~`-prefixed cwd via expandHome before calling remote', async () => {
466
- const receivedCwds = [];
467
- const remote = {
468
- async allocateParallelGroup(cwd) { receivedCwds.push(cwd); return 42; },
469
- async readPrd(slug, cwd) { receivedCwds.push(cwd); return { ok: false }; },
470
- async writePrd(slug, body, cwd) { receivedCwds.push(cwd); return { ok: true, bytesWritten: body.length }; },
471
- };
472
-
473
- const result = await createPrd(validCreateBody({ cwd: '~' }), remote);
474
-
475
- expect(result.ok).toBe(true);
476
- expect(receivedCwds.length).toBeGreaterThan(0);
477
- for (const cwd of receivedCwds) {
478
- expect(cwd.startsWith('~')).toBe(false);
479
- expect(cwd).toBe(os.homedir());
480
- }
481
- });
482
-
483
- // ──────────────────────────────────────────── PRD 862: register cwd as an
484
- // allowed write root at createPrd() time, not only at pty.spawn() time
485
- //
486
- // Before this fix, config.cjs's allowedRoots (the write-boundary allowlist)
487
- // only ever grew via pty.cjs's addAllowedRoot call inside pty.spawn(). A
488
- // chat-only Epic (headless `claude -p --resume`, no Terminal PTY ever
489
- // spawned for its project cwd) reached createPrd() -> remote.writePrd() ->
490
- // config.writeTextAtomic() -> config.validateWrite() and failed with "Write
491
- // outside allowed write boundaries", purely because no PTY had registered
492
- // that cwd yet in this process's lifetime. This test reproduces that exact
493
- // scenario: a brand-new project root that has NEVER had config.addAllowedRoot
494
- // called for it (mirroring pty.spawn() never having run), writing through the
495
- // real config.writeTextAtomic into the real session-manager-operations/
496
- // scheduler/ subtree (not the mocked-remote temp dirs the other tests here
497
- // use, which pre-register their root via addAllowedRoot in mkTmpPrdsDir).
498
- test('createPrd registers cwd as an allowed write root itself, so a chat-only Epic (no Terminal PTY ever spawned for this cwd) can still write its PRD', async () => {
499
- // Must live under $HOME: validatePath's read boundary is home-dir-wide
500
- // regardless of allowedRoots registration (matching every real project
501
- // cwd, which checkInsideHome forces inside $HOME) — the bug this test
502
- // reproduces is specifically about the *write* boundary (validateWrite),
503
- // not the read one.
504
- const root = await fsp.mkdtemp(path.join(os.homedir(), '.sm-chat-only-epic-'));
505
- // Deliberately do NOT call config.addAllowedRoot(root) here — pty.spawn()
506
- // is the only other caller of addAllowedRoot, and it never runs for a
507
- // chat-only Epic. createPrd() itself must perform this registration.
508
- const prdsEpicDir = path.join(root, 'session-manager-operations', 'scheduler', 'epics', 'chat-only-epic', 'prds');
509
-
510
- try {
511
- const remote = {
512
- async allocateParallelGroup() { return 1; },
513
- async readPrd() { return { ok: false }; },
514
- async writePrd(slug, body) {
515
- await fsp.mkdir(prdsEpicDir, { recursive: true });
516
- const filePath = path.join(prdsEpicDir, `${slug}.md`);
517
- // Real config.cjs write path — this is what threw
518
- // "Write outside allowed write boundaries" before the fix.
519
- await config.writeTextAtomic(filePath, body, { writer: 'scheduler' });
520
- return { ok: true };
521
- },
522
- };
523
-
524
- const result = await createPrd(validCreateBody({ cwd: root }), remote);
525
-
526
- expect(result.ok).toBe(true);
527
- const written = await fsp.readFile(path.join(prdsEpicDir, result.filename), 'utf8');
528
- expect(written).toMatch(/title: Add widget frobnication/);
529
- } finally {
530
- await fsp.rm(root, { recursive: true, force: true });
531
- }
532
- });
533
-
534
- test('POST /admin/scheduler/create-prd with sourcePromptId writes it into the created PRD frontmatter', async () => {
535
- const prdsDir = await mkTmpPrdsDir();
536
- const remote = makeFakeRemoteWithPrdsDir(prdsDir);
537
- const { admin, port, token } = await startWithRemote(remote);
538
- try {
539
- const res = await request(port, {
540
- method: 'POST', path: '/admin/scheduler/create-prd', token,
541
- body: validCreateBody({ slug: 'from-a-ticket', sourcePromptId: 'ticket-xyz-789' }),
542
- });
543
- expect(res.status).toBe(200);
544
- const written = fs.readFileSync(path.join(prdsDir, `${res.json.nn}-from-a-ticket.md`), 'utf8');
545
- expect(written).toMatch(/sourcePromptId: ticket-xyz-789/);
546
- } finally {
547
- await admin.stop();
548
- }
549
- });
550
-
551
- test('POST /admin/scheduler/create-prd with tag writes it into the created PRD frontmatter (PRD 1041)', async () => {
552
- const prdsDir = await mkTmpPrdsDir();
553
- const remote = makeFakeRemoteWithPrdsDir(prdsDir);
554
- const { admin, port, token } = await startWithRemote(remote);
555
- try {
556
- const res = await request(port, {
557
- method: 'POST', path: '/admin/scheduler/create-prd', token,
558
- body: validCreateBody({ slug: 'with-a-tag', tag: 'bug' }),
559
- });
560
- expect(res.status).toBe(200);
561
- const written = fs.readFileSync(path.join(prdsDir, `${res.json.nn}-with-a-tag.md`), 'utf8');
562
- expect(written).toMatch(/^tag: bug$/m);
563
- } finally {
564
- await admin.stop();
565
- }
566
- });
567
-
568
- // ──────────────────────────────────────────── PRD 1114: agentType (WHO
569
- // executes a PRD) — throw on write for an unresolvable persona, default to
570
- // dev-lead when omitted.
571
-
572
- test('createPrd rejects an unknown agentType at write time, naming available personas, and writes no file', async () => {
573
- const prdsDir = await mkTmpPrdsDir();
574
- const remote = makeFakeRemoteWithPrdsDir(prdsDir);
575
- const writePrdSpy = vi.fn(remote.writePrd);
576
- remote.writePrd = writePrdSpy;
577
-
578
- const result = await createPrd(
579
- validCreateBody({ agentType: 'zzz-definitely-not-a-real-persona-12345' }),
580
- remote,
581
- );
582
-
583
- expect(result.ok).toBe(false);
584
- expect(result.status).toBe(400);
585
- expect(result.error).toMatch(/does not resolve to a readable persona file/);
586
- expect(result.error).toMatch(/Available personas/);
587
- expect(writePrdSpy).not.toHaveBeenCalled();
588
- const entries = await fsp.readdir(prdsDir);
589
- expect(entries.filter((f) => f.endsWith('.md')).length).toBe(0);
590
- });
591
-
592
- test('createPrd accepts a caller-supplied agentType that resolves to a project-overlay persona file', async () => {
593
- const prdsDir = await mkTmpPrdsDir();
594
- const cwd = path.dirname(path.dirname(prdsDir)); // mkTmpPrdsDir's root (prdsDir = root/.claude/prds)
595
- const agentsDir = path.join(cwd, '.claude', 'agents');
596
- await fsp.mkdir(agentsDir, { recursive: true });
597
- await fsp.writeFile(path.join(agentsDir, 'my-persona.md'), '---\nname: my-persona\n---\nbody\n', 'utf8');
598
- const remote = makeFakeRemoteWithPrdsDir(prdsDir);
599
-
600
- const result = await createPrd(validCreateBody({ cwd, slug: 'with-agent-type', agentType: 'my-persona' }), remote);
601
-
602
- expect(result.ok).toBe(true);
603
- const written = await fsp.readFile(path.join(prdsDir, result.filename), 'utf8');
604
- expect(written).toMatch(/^agentType: my-persona$/m);
605
- });
606
-
607
- test('createPrd defaults agentType to dev-lead when omitted, without any persona-existence check', async () => {
608
- const prdsDir = await mkTmpPrdsDir();
609
- const remote = makeFakeRemoteWithPrdsDir(prdsDir);
610
-
611
- const result = await createPrd(validCreateBody({ slug: 'no-agent-type-supplied' }), remote);
612
-
613
- expect(result.ok).toBe(true);
614
- const written = await fsp.readFile(path.join(prdsDir, result.filename), 'utf8');
615
- expect(written).toMatch(/^agentType: dev-lead$/m);
616
- });
617
-
618
- // ──────────────────────────────────────────── originClaudeSessionId fallback
619
- // (2026-08-01 fix: a PRD queued via the scheduler_create_prd MCP tool from
620
- // inside an Epic's own chat session, without an explicit sourcePromptId,
621
- // must still join that Epic rather than mint an unrelated sibling one.)
622
-
623
- async function writeActiveIndexFixture(cwd, sessions) {
624
- const dir = path.join(cwd, 'session-manager-operations', 'prompt-sessions');
625
- await fsp.mkdir(dir, { recursive: true });
626
- await fsp.writeFile(path.join(dir, 'active-index.json'), JSON.stringify({ sessions, events: {} }, null, 2));
627
- }
628
-
629
- test('resolveSourcePromptIdFromClaudeSession finds the Epic whose claudeSessionId matches', async () => {
630
- const root = await fsp.mkdtemp(path.join(os.tmpdir(), 'sm-resolve-epic-'));
631
- try {
632
- await writeActiveIndexFixture(root, {
633
- 'psess-abc123': { id: 'psess-abc123', claudeSessionId: 'claude-sess-1', status: 'active' },
634
- });
635
- expect(resolveSourcePromptIdFromClaudeSession(root, 'claude-sess-1')).toBe('psess-abc123');
636
- expect(resolveSourcePromptIdFromClaudeSession(root, 'claude-sess-unknown')).toBeNull();
637
- expect(resolveSourcePromptIdFromClaudeSession(root, null)).toBeNull();
638
- } finally {
639
- await fsp.rm(root, { recursive: true, force: true });
640
- }
641
- });
642
-
643
- // Real-homedir-rooted like the chat-only-epic test above (see its comment at
644
- // line ~449): validatePath's read boundary is home-dir-wide, and this
645
- // exercises createPrd's real config.addAllowedRoot/writeTextAtomic path. Safe
646
- // because the random suffix is never a registered project the live scheduler
647
- // scans for PRDs, and it's removed in `finally`.
648
- test('createPrd resolves sourcePromptId from originClaudeSessionId when the caller omitted sourcePromptId', async () => {
649
- const root = await fsp.mkdtemp(path.join(os.homedir(), '.sm-origin-session-epic-'));
650
- config.addAllowedRoot(root);
651
- try {
652
- await writeActiveIndexFixture(root, {
653
- 'psess-always-select-tab': { id: 'psess-always-select-tab', claudeSessionId: 'claude-sess-epic-1', status: 'active' },
654
- });
655
- const prdsDir = path.join(root, 'session-manager-operations', 'scheduler', 'epics', 'psess-always-select-tab', 'prds');
656
- const remote = makeFakeRemoteWithPrdsDir(prdsDir);
657
-
658
- const result = await createPrd(
659
- validCreateBody({ cwd: root, slug: 'joins-the-epic', originClaudeSessionId: 'claude-sess-epic-1' }),
660
- remote,
661
- );
662
-
663
- expect(result.ok).toBe(true);
664
- const written = await fsp.readFile(path.join(prdsDir, result.filename), 'utf8');
665
- expect(written).toMatch(/sourcePromptId: psess-always-select-tab/);
666
- } finally {
667
- await fsp.rm(root, { recursive: true, force: true });
668
- }
669
- });
670
-
671
- // Same real-homedir rationale as the two tests above.
672
- // ──────────────────────────────────────────── worktree-cwd Epic-lookup
673
- // hazard (PRD: projectRootResolve.cjs) — reproduces the live starry-night-
674
- // ships shape: main's active-index.json has the Epic, a linked worktree's
675
- // own (git-tracked, frozen-at-branch-time) copy does not. Passing the
676
- // worktree's pwd as `cwd` must still resolve to main and join the Epic there.
677
-
678
- function git(args, cwd) {
679
- return execFileSync('git', args, { cwd, encoding: 'utf8' });
680
- }
681
-
682
- function initRepo(dir) {
683
- fs.mkdirSync(dir, { recursive: true });
684
- git(['init', '-q'], dir);
685
- git(['config', 'user.email', 'test@example.com'], dir);
686
- git(['config', 'user.name', 'Test'], dir);
687
- fs.writeFileSync(path.join(dir, 'README.md'), 'hello\n', 'utf8');
688
- git(['add', '-A'], dir);
689
- git(['commit', '-q', '-m', 'initial'], dir);
690
- }
691
-
692
- test('createPrd resolves a worktree cwd to the main tree and joins the Epic whose row only exists in main\'s active-index (live starry-night-ships shape)', async () => {
693
- const tmpRoot = await fsp.mkdtemp(path.join(os.tmpdir(), 'sm-worktree-cwd-hazard-'));
694
- const mainRepo = path.join(tmpRoot, 'main');
695
- const worktreeDir = path.join(tmpRoot, 'worktree', 'epic-1');
696
- config.addAllowedRoot(mainRepo);
697
- try {
698
- initRepo(mainRepo);
699
- fs.mkdirSync(path.dirname(worktreeDir), { recursive: true });
700
- git(['worktree', 'add', '-b', 'sm-epic/epic-1', worktreeDir, 'HEAD'], mainRepo);
701
-
702
- // Main's index has the Epic; the worktree's own copy (as checked out at
703
- // branch time) does not — the exact live-repro shape.
704
- await writeActiveIndexFixture(mainRepo, {
705
- 'epic-1': { id: 'epic-1', claudeSessionId: 'claude-sess-epic-1', status: 'active' },
706
- });
707
-
708
- const prdsDir = path.join(mainRepo, 'session-manager-operations', 'scheduler', 'epics', 'epic-1', 'prds');
709
- const remote = makeFakeRemoteWithPrdsDir(prdsDir);
710
- const receivedCwds = [];
711
- const realAllocate = remote.allocateParallelGroup.bind(remote);
712
- remote.allocateParallelGroup = async (cwd) => { receivedCwds.push(cwd); return realAllocate(cwd); };
713
-
714
- const result = await createPrd(
715
- validCreateBody({ cwd: worktreeDir, slug: 'joins-from-worktree', sourcePromptId: 'epic-1' }),
716
- remote,
717
- );
718
-
719
- expect(result.ok).toBe(true);
720
- // Resolved and passed to the remote as the MAIN tree, never the worktree.
721
- expect(receivedCwds.every((c) => c === mainRepo)).toBe(true);
722
- const written = await fsp.readFile(path.join(prdsDir, result.filename), 'utf8');
723
- expect(written).toMatch(/sourcePromptId: epic-1/);
724
- expect(written).toMatch(new RegExp(`cwd: ${mainRepo.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`));
725
- } finally {
726
- await fsp.rm(tmpRoot, { recursive: true, force: true });
727
- }
728
- });
729
-
730
- test('createPrd omits cwd entirely and resolves the project purely from sourcePromptId (cwd optional inside an Epic session)', async () => {
731
- const root = await fsp.mkdtemp(path.join(os.homedir(), '.sm-cwd-optional-epic-'));
732
- config.addAllowedRoot(root);
733
- // resolveProjectContext's cross-project scan (used when cwd is omitted
734
- // entirely) is built on activeSessions.allProjectCwds — a scan of REAL
735
- // ~/.claude/projects transcripts. In production this is always populated
736
- // (the calling session itself opened `root` as a project), so seed a
737
- // matching transcript here rather than the fixture root being invisible to
738
- // the scan. Cleaned up in `finally`.
739
- const transcriptDir = path.join(os.homedir(), '.claude', 'projects', `sm-cwd-optional-test-${Date.now()}`);
740
- try {
741
- await writeActiveIndexFixture(root, {
742
- 'epic-cwd-optional': { id: 'epic-cwd-optional', claudeSessionId: 'claude-sess-cwd-optional', status: 'active' },
743
- });
744
- await fsp.mkdir(transcriptDir, { recursive: true });
745
- await fsp.writeFile(path.join(transcriptDir, 'a.jsonl'), `${JSON.stringify({ cwd: root })}\n`, 'utf8');
746
-
747
- const prdsDir = path.join(root, 'session-manager-operations', 'scheduler', 'epics', 'epic-cwd-optional', 'prds');
748
- const remote = makeFakeRemoteWithPrdsDir(prdsDir);
749
-
750
- const body = validCreateBody({ slug: 'no-cwd-supplied', sourcePromptId: 'epic-cwd-optional' });
751
- delete body.cwd;
752
-
753
- const result = await createPrd(body, remote);
754
-
755
- expect(result.ok).toBe(true);
756
- const written = await fsp.readFile(path.join(prdsDir, result.filename), 'utf8');
757
- expect(written).toMatch(/sourcePromptId: epic-cwd-optional/);
758
- } finally {
759
- await fsp.rm(transcriptDir, { recursive: true, force: true });
760
- await fsp.rm(root, { recursive: true, force: true });
761
- }
762
- });
763
-
764
- test('createPrd with neither cwd nor a resolvable Epic fails with a clear cwd-required error', async () => {
765
- const remote = {
766
- async allocateParallelGroup() { throw new Error('must not be called'); },
767
- async readPrd() { throw new Error('must not be called'); },
768
- async writePrd() { throw new Error('must not be called'); },
769
- };
770
- const body = validCreateBody();
771
- delete body.cwd;
772
-
773
- const result = await createPrd(body, remote);
774
-
775
- expect(result.ok).toBe(false);
776
- expect(result.status).toBe(400);
777
- expect(result.error).toMatch(/cwd is required/);
778
- });
779
-
780
- // ──────────────────────────────────────────── PRD 1113: fix-chain depth guard
781
- // at the authoring boundary. lib/prdCreate.cjs's createPrd() is the shared
782
- // write path behind both scheduler_create_prd (MCP) and chat:create-prd
783
- // (IPC) — enforcing here covers both callers with one check.
784
-
785
- test('createPrd refuses a depth-2 fix-chain slug (fix-of-a-fix) with a distinct, human-escalation error and writes no file', async () => {
786
- const prdsDir = await mkTmpPrdsDir();
787
- const remote = makeFakeRemoteWithPrdsDir(prdsDir);
788
- const writePrdSpy = vi.fn(remote.writePrd);
789
- remote.writePrd = writePrdSpy;
790
-
791
- const result = await createPrd(validCreateBody({ slug: 'fix-fix-widget-frobnication' }), remote);
792
-
793
- expect(result.ok).toBe(false);
794
- expect(result.status).toBe(409);
795
- // Distinct from the provenance/validation error text elsewhere in this
796
- // file ('cwd rejected', 'invalid PRD payload', 'already exists') and
797
- // explicitly states escalation, not another attempt, is correct.
798
- expect(result.error).toMatch(/fix-chain depth cap/i);
799
- expect(result.error).toMatch(/not the correct response/i);
800
- expect(result.error).toMatch(/human/i);
801
- expect(writePrdSpy).not.toHaveBeenCalled();
802
- const entries = await fsp.readdir(prdsDir);
803
- expect(entries.filter((f) => f.endsWith('.md')).length).toBe(0);
804
- });
805
-
806
- test('createPrd refuses a depth-3 fix-chain slug the same way as depth-2', async () => {
807
- const prdsDir = await mkTmpPrdsDir();
808
- const remote = makeFakeRemoteWithPrdsDir(prdsDir);
809
-
810
- const result = await createPrd(validCreateBody({ slug: 'fix-fix-fix-widget-frobnication' }), remote);
811
-
812
- expect(result.ok).toBe(false);
813
- expect(result.status).toBe(409);
814
- const entries = await fsp.readdir(prdsDir);
815
- expect(entries.filter((f) => f.endsWith('.md')).length).toBe(0);
816
- });
817
-
818
- test('createPrd refuses a depth-1 fix slug too (PRD 1131) — that shape is reserved for spawnInvestigation-authored fix plans, not this API', async () => {
819
- const prdsDir = await mkTmpPrdsDir();
820
- const remote = makeFakeRemoteWithPrdsDir(prdsDir);
821
- const writePrdSpy = vi.fn(remote.writePrd);
822
- remote.writePrd = writePrdSpy;
823
-
824
- const result = await createPrd(validCreateBody({ slug: 'fix-widget-frobnication' }), remote);
825
-
826
- expect(result.ok).toBe(false);
827
- expect(result.status).toBe(400);
828
- expect(result.error).toMatch(/reserved NN-fix-\.\.\. fix-plan naming convention/i);
829
- expect(writePrdSpy).not.toHaveBeenCalled();
830
- const entries = await fsp.readdir(prdsDir);
831
- expect(entries.filter((f) => f.endsWith('.md')).length).toBe(0);
832
- });
833
-
834
- test('createPrd is unaffected by the fix-chain guard for an ordinary slug with no fix chain at all', async () => {
835
- const prdsDir = await mkTmpPrdsDir();
836
- const remote = makeFakeRemoteWithPrdsDir(prdsDir);
837
-
838
- const result = await createPrd(validCreateBody(), remote);
839
-
840
- expect(result.ok).toBe(true);
841
- const written = await fsp.readFile(path.join(prdsDir, result.filename), 'utf8');
842
- expect(written).toMatch(/title: Add widget frobnication/);
843
- });
844
-
845
- test('createPrd refuses a slug starting with "fix-" even when a later segment merely reads "fix" (isFixPlanSlug matches on the leading segment alone, unlike fixChainDepthOf)', async () => {
846
- const prdsDir = await mkTmpPrdsDir();
847
- const remote = makeFakeRemoteWithPrdsDir(prdsDir);
848
-
849
- const result = await createPrd(validCreateBody({ slug: 'fix-loop-extraction-and-round-trip' }), remote);
850
-
851
- expect(result.ok).toBe(false);
852
- expect(result.status).toBe(400);
853
- expect(result.error).toMatch(/reserved NN-fix-\.\.\. fix-plan naming convention/i);
854
- });
855
-
856
- test('createPrd leaves sourcePromptId unset when originClaudeSessionId matches no known Epic (plain SessionTab chat)', async () => {
857
- const root = await fsp.mkdtemp(path.join(os.homedir(), '.sm-origin-session-no-match-'));
858
- config.addAllowedRoot(root);
859
- try {
860
- await writeActiveIndexFixture(root, {});
861
- const prdsDir = path.join(root, 'session-manager-operations', 'scheduler', 'prds');
862
- const remote = makeFakeRemoteWithPrdsDir(prdsDir);
863
-
864
- const result = await createPrd(
865
- validCreateBody({ cwd: root, slug: 'no-epic-match', originClaudeSessionId: 'claude-sess-not-an-epic' }),
866
- remote,
867
- );
868
-
869
- expect(result.ok).toBe(true);
870
- const written = await fsp.readFile(path.join(prdsDir, result.filename), 'utf8');
871
- expect(!/sourcePromptId:/.test(written)).toBeTruthy();
872
- } finally {
873
- await fsp.rm(root, { recursive: true, force: true });
874
- }
875
- });
876
-
877
- // ──────────────────────────────────────────── PRD 1121: reject double-prefixed
878
- // slugs, validate dependsOn against existing PRDs at write time
879
-
880
- test('createPrd refuses a caller-supplied slug already starting with a numeric prefix', async () => {
881
- const prdsDir = await mkTmpPrdsDir();
882
- const remote = makeFakeRemoteWithPrdsDir(prdsDir);
883
-
884
- const result = await createPrd(validCreateBody({ slug: '254-perf-x' }), remote);
885
-
886
- expect(result.ok).toBe(false);
887
- expect(result.status).toBe(400);
888
- expect(result.error).toMatch(/254-/);
889
- expect(result.error).toMatch(/bare name/);
890
- const entries = await fsp.readdir(prdsDir);
891
- expect(entries.filter((f) => f.endsWith('.md')).length).toBe(0);
892
- });
893
-
894
- test('createPrd still accepts a bare (non-prefixed) caller-supplied slug', async () => {
895
- const prdsDir = await mkTmpPrdsDir();
896
- const remote = makeFakeRemoteWithPrdsDir(prdsDir);
897
-
898
- const result = await createPrd(validCreateBody({ slug: 'perf-x' }), remote);
899
-
900
- expect(result.ok).toBe(true);
901
- expect(result.filename).toMatch(/^\d+-perf-x\.md$/);
902
- });
903
-
904
- test('createPrd accepts dependsOn naming an existing bare slug', async () => {
905
- const prdsDir = await mkTmpPrdsDir();
906
- await seedExistingPrd(prdsDir, '3-widget-base');
907
- const remote = makeFakeRemoteWithPrdsDir(prdsDir);
908
-
909
- const result = await createPrd(validCreateBody({ slug: 'widget-follow-up', dependsOn: ['widget-base'] }), remote);
910
-
911
- expect(result.ok).toBe(true);
912
- const written = await fsp.readFile(path.join(prdsDir, result.filename), 'utf8');
913
- expect(written).toMatch(/^dependsOn: \[widget-base\]$/m);
914
- });
915
-
916
- test('createPrd accepts dependsOn naming an existing full NN-prefixed slug', async () => {
917
- const prdsDir = await mkTmpPrdsDir();
918
- await seedExistingPrd(prdsDir, '3-widget-base');
919
- const remote = makeFakeRemoteWithPrdsDir(prdsDir);
920
-
921
- const result = await createPrd(validCreateBody({ slug: 'widget-follow-up-2', dependsOn: ['3-widget-base'] }), remote);
922
-
923
- expect(result.ok).toBe(true);
924
- });
925
-
926
- test('createPrd refuses dependsOn naming nothing, listing near-matches in the message, and writes no file', async () => {
927
- const prdsDir = await mkTmpPrdsDir();
928
- await seedExistingPrd(prdsDir, '3-widget-base');
929
- await seedExistingPrd(prdsDir, '4-widget-basics');
930
- const remote = makeFakeRemoteWithPrdsDir(prdsDir);
931
-
932
- const result = await createPrd(validCreateBody({ slug: 'widget-follow-up-3', dependsOn: ['wigdet-bsae'] }), remote);
933
-
934
- expect(result.ok).toBe(false);
935
- expect(result.status).toBe(400);
936
- expect(result.error).toMatch(/wigdet-bsae/);
937
- expect(result.error).toMatch(/widget-base/);
938
- const entries = await fsp.readdir(prdsDir);
939
- expect(entries.filter((f) => f.endsWith('.md') && f.includes('widget-follow-up-3')).length).toBe(0);
940
- });
941
-
942
- test('createPrd treats an empty dependsOn array as a no-op (no listPrds call, no rejection)', async () => {
943
- const prdsDir = await mkTmpPrdsDir();
944
- const remote = makeFakeRemoteWithPrdsDir(prdsDir);
945
- const listPrdsSpy = vi.fn(remote.listPrds);
946
- remote.listPrds = listPrdsSpy;
947
-
948
- const result = await createPrd(validCreateBody({ slug: 'no-deps-here', dependsOn: [] }), remote);
949
-
950
- expect(result.ok).toBe(true);
951
- expect(listPrdsSpy).not.toHaveBeenCalled();
952
- });
953
-
954
- test('createPrd treats an absent dependsOn as a no-op (no listPrds call, no rejection)', async () => {
955
- const prdsDir = await mkTmpPrdsDir();
956
- const remote = makeFakeRemoteWithPrdsDir(prdsDir);
957
- const listPrdsSpy = vi.fn(remote.listPrds);
958
- remote.listPrds = listPrdsSpy;
959
-
960
- const result = await createPrd(validCreateBody({ slug: 'no-deps-field' }), remote);
961
-
962
- expect(result.ok).toBe(true);
963
- expect(listPrdsSpy).not.toHaveBeenCalled();
964
- });
965
-
966
- test('createPrd warns and skips dependsOn validation (never fails the write) when listPrds throws', async () => {
967
- const prdsDir = await mkTmpPrdsDir();
968
- const remote = makeFakeRemoteWithPrdsDir(prdsDir);
969
- remote.listPrds = async () => { throw new Error('simulated I/O failure'); };
970
- const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
971
-
972
- try {
973
- const result = await createPrd(validCreateBody({ slug: 'deps-during-outage', dependsOn: ['anything-at-all'] }), remote);
974
- expect(result.ok).toBe(true);
975
- expect(warnSpy).toHaveBeenCalled();
976
- } finally {
977
- warnSpy.mockRestore();
978
- }
979
- });