claude-code-session-manager 0.98.0 → 0.101.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 (441) 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-CXaH2GED.js +1 -0
  5. package/dist/assets/{History-Bfdoywlh.js → History-Cerii-HA.js} +2 -2
  6. package/dist/assets/{Hooks-CKDJjTJV.js → Hooks-aXMb5LDy.js} +3 -3
  7. package/dist/assets/{Library-7akfgy67.js → Library-WrJFKEOJ.js} +1 -1
  8. package/dist/assets/{MarkdownEditor-DDesiqet.js → MarkdownEditor-D1BMCXS7.js} +1 -1
  9. package/dist/assets/{McpServers-D4lZtXof.js → McpServers-Cf0davLd.js} +2 -2
  10. package/dist/assets/{Memory-CeI8G1w5.js → Memory-Dn6NHWPe.js} +6 -6
  11. package/dist/assets/Permissions-CTVuFPjg.js +3 -0
  12. package/dist/assets/{Plugins-P4eCXy_S.js → Plugins-C2TyF8Mn.js} +2 -2
  13. package/dist/assets/ProvenanceBadge-F_jwHSwE.js +1 -0
  14. package/dist/assets/{SaveBar-l9jnEMES.js → SaveBar-DA1KVJST.js} +1 -1
  15. package/dist/assets/Scheduler-Bm6Q_hlu.js +16 -0
  16. package/dist/assets/{ScopeSwitcher-CjJi8ST5.js → ScopeSwitcher-BygZlJca.js} +1 -1
  17. package/dist/assets/Settings-BYCiv5KV.js +3 -0
  18. package/dist/assets/{SkillReferenceGraph-CjbO01Sg.js → SkillReferenceGraph-CcLnl9np.js} +1 -1
  19. package/dist/assets/{Skills-BxZ9_fCR.js → Skills-ChffsdTD.js} +2 -2
  20. package/dist/assets/{SystemPrompt-D8vfFb8b.js → SystemPrompt-dNkfHE_M.js} +1 -1
  21. package/dist/assets/{TagLibrary-2vzUo7YL.js → TagLibrary-jVlEuB0m.js} +1 -1
  22. package/dist/assets/{TiptapBody-B8ZfbK90.js → TiptapBody-6_cdwcP0.js} +1 -1
  23. package/dist/assets/{Toggle-BbW4nGGf.js → Toggle-CVXqGpAP.js} +1 -1
  24. package/dist/assets/ViewTabs-DO1YVbpc.js +1 -0
  25. package/dist/assets/{index-CKH5Uxik.css → index-D89EUizB.css} +1 -1
  26. package/dist/assets/{index-Cd98Q1lD.js → index-DbCQPWWY.js} +747 -752
  27. package/dist/assets/{settingsSchema-IhUfucCF.js → settingsSchema-Wrvn-Yz1.js} +1 -1
  28. package/dist/index.html +2 -2
  29. package/package.json +25 -29
  30. package/plugins/CLAUDE.md +6 -6
  31. package/plugins/session-manager-dev/skills/builder/4-manual/SKILL.md +21 -19
  32. package/plugins/session-manager-dev/skills/builder/SKILL.md +3 -3
  33. package/plugins/session-manager-dev/skills/develop/SKILL.md +254 -510
  34. package/plugins/session-manager-dev/skills/develop/standards.md +17 -22
  35. package/scripts/README.md +3 -11
  36. package/scripts/audit-ops-hygiene.cjs +3 -3
  37. package/scripts/hooks/lib/guard-prd-writes-policy.cjs +1 -1
  38. package/scripts/hooks/lib/guard-self-schedule-policy.cjs +1 -1
  39. package/scripts/mint-epic.cjs +2 -3
  40. package/scripts/scheduler-mcp-server.cjs +64 -7
  41. package/src/main/agentLibrary.cjs +22 -2
  42. package/src/main/build-info.json +4 -4
  43. package/src/main/chatRunner.cjs +4 -3
  44. package/src/main/config.cjs +41 -38
  45. package/src/main/docEdit.cjs +4 -1
  46. package/src/main/files.cjs +2 -5
  47. package/src/main/health.cjs +1 -1
  48. package/src/main/index.cjs +40 -11
  49. package/src/main/ipcSchemas.cjs +35 -34
  50. package/src/main/lib/agentPersonaSchema.cjs +13 -2
  51. package/src/main/lib/atomicFs.cjs +116 -0
  52. package/src/main/lib/branchSweep.cjs +13 -12
  53. package/src/main/lib/buildTarget.cjs +3 -3
  54. package/src/main/lib/builtinMacros.cjs +39 -0
  55. package/src/main/lib/claudeCliCaps.cjs +129 -0
  56. package/src/main/lib/credentials.cjs +2 -4
  57. package/src/main/lib/crossProjectFeedback.cjs +3 -3
  58. package/src/main/lib/cwdClassify.cjs +15 -3
  59. package/src/main/lib/definitionOfDone.cjs +207 -125
  60. package/src/main/lib/dodDrainHook.cjs +11 -5
  61. package/src/main/lib/epicMint.cjs +2 -4
  62. package/src/main/lib/epicStatusMirror.cjs +8 -10
  63. package/src/main/lib/epicWorktreeProjectConfig.cjs +2 -4
  64. package/src/main/lib/gateAuthority.cjs +96 -0
  65. package/src/main/lib/gitExec.cjs +69 -0
  66. package/src/main/lib/gitWorktree.cjs +656 -92
  67. package/src/main/lib/instanceLock.cjs +5 -8
  68. package/src/main/lib/launchFailure.cjs +2 -3
  69. package/src/main/lib/macroLibrary.cjs +480 -0
  70. package/src/main/lib/mcpToolCatalog.cjs +49 -17
  71. package/src/main/lib/needsReviewLedger.cjs +1 -1
  72. package/src/main/lib/opsErrorLog.cjs +11 -1
  73. package/src/main/lib/opsOwnership.cjs +5 -9
  74. package/src/main/lib/pidAlive.cjs +32 -0
  75. package/src/main/lib/prdCreate.cjs +121 -12
  76. package/src/main/lib/prdDisposition.cjs +2 -2
  77. package/src/main/lib/prdGateFiles.cjs +284 -0
  78. package/src/main/lib/prdLocations.cjs +61 -0
  79. package/src/main/lib/prdMigration.cjs +2 -3
  80. package/src/main/lib/prdSizing.cjs +2 -1
  81. package/src/main/lib/projectHomeAdminRoutes.cjs +136 -21
  82. package/src/main/lib/promptSessionSchema.cjs +10 -2
  83. package/src/main/lib/rcaReport.cjs +4 -6
  84. package/src/main/lib/reaperHelpers.cjs +8 -1
  85. package/src/main/lib/reservationExpiry.cjs +6 -1
  86. package/src/main/lib/reviewNotice.cjs +263 -0
  87. package/src/main/lib/runClaudeP.cjs +4 -1
  88. package/src/main/lib/schedulerPaths.cjs +22 -2
  89. package/src/main/lib/sessionSlots.cjs +2 -4
  90. package/src/main/lib/shippedPersonaSeeds.cjs +226 -0
  91. package/src/main/lib/smfileHeaders.cjs +66 -0
  92. package/src/main/lib/timeoutShim.cjs +123 -0
  93. package/src/main/lib/timeoutShimScript.cjs +275 -0
  94. package/src/main/lib/upgradeDrain.cjs +4 -10
  95. package/src/main/lib/watchdogHelpers.cjs +12 -25
  96. package/src/main/lib/workTypeLibrary.cjs +7 -1
  97. package/src/main/projectPages.cjs +43 -13
  98. package/src/main/promptSessionEvents.cjs +37 -13
  99. package/src/main/scheduler.cjs +910 -198
  100. package/src/main/seedAgentPersonas.cjs +279 -24
  101. package/src/main/supervisor.cjs +4 -1
  102. package/src/main/templates/PRD_AUTHORING.md +126 -406
  103. package/src/preload/__tests__/preload-surface.test.cjs +68 -0
  104. package/src/preload/api.d.ts +53 -94
  105. package/src/preload/index.cjs +14 -9
  106. package/src/seed/agents/architect.md +2 -0
  107. package/src/seed/agents/demo-video-builder.md +84 -0
  108. package/src/seed/agents/dev-lead.md +18 -18
  109. package/src/seed/agents/project-home-builder.md +66 -35
  110. package/src/seed/agents/validator.md +10 -4
  111. package/dist/assets/DataModel-DCEgMTjH.js +0 -1
  112. package/dist/assets/HostBilko-CDVHE0hT.js +0 -1
  113. package/dist/assets/Permissions-BGleLja4.js +0 -3
  114. package/dist/assets/ProvenanceBadge-BFczzI1B.js +0 -1
  115. package/dist/assets/Scheduler-C_phpeUS.js +0 -16
  116. package/dist/assets/Settings-BIwZ3FGQ.js +0 -3
  117. package/src/main/__tests__/activeIndexMerge.test.cjs +0 -235
  118. package/src/main/__tests__/agentEffortResolve.test.cjs +0 -117
  119. package/src/main/__tests__/agentLibrary.test.cjs +0 -276
  120. package/src/main/__tests__/agentModelResolve.test.cjs +0 -311
  121. package/src/main/__tests__/agentOverlayWrite.test.cjs +0 -95
  122. package/src/main/__tests__/bilkoHost-deriveSlug.test.cjs +0 -26
  123. package/src/main/__tests__/bilkoHost-integration.test.cjs +0 -118
  124. package/src/main/__tests__/bilkoHostCore.test.cjs +0 -72
  125. package/src/main/__tests__/broadcastCoalescer.test.cjs +0 -122
  126. package/src/main/__tests__/chat-cancel-terminal.test.cjs +0 -120
  127. package/src/main/__tests__/chat-dead-channels.test.cjs +0 -63
  128. package/src/main/__tests__/chat-exit-close-race.test.cjs +0 -146
  129. package/src/main/__tests__/chat-mcp-consent-notice.test.cjs +0 -139
  130. package/src/main/__tests__/chat-preamble-anchors.test.cjs +0 -100
  131. package/src/main/__tests__/chat-queue.test.cjs +0 -97
  132. package/src/main/__tests__/chat-stop-signal.test.cjs +0 -89
  133. package/src/main/__tests__/chatRunner-epic-worktree-execcwd.test.cjs +0 -125
  134. package/src/main/__tests__/chatRunner-session-flag-retry.test.cjs +0 -252
  135. package/src/main/__tests__/classifyPromptTicket.test.cjs +0 -101
  136. package/src/main/__tests__/classifyTranscriptLine.test.cjs +0 -201
  137. package/src/main/__tests__/computeDepHistorySatisfaction.test.cjs +0 -66
  138. package/src/main/__tests__/config-readText-bounded.test.cjs +0 -84
  139. package/src/main/__tests__/configWriteBoundaryOwners.test.cjs +0 -59
  140. package/src/main/__tests__/crossProjectFeedback.test.cjs +0 -334
  141. package/src/main/__tests__/crossProjectFeedbackRoutes.test.cjs +0 -161
  142. package/src/main/__tests__/dep-orphan-archive-health.test.cjs +0 -77
  143. package/src/main/__tests__/develop-skill-failure-modes.test.cjs +0 -70
  144. package/src/main/__tests__/docEdit.test.cjs +0 -244
  145. package/src/main/__tests__/dod-batchkey.test.cjs +0 -183
  146. package/src/main/__tests__/dod-drain-hook.test.cjs +0 -302
  147. package/src/main/__tests__/dod-report.test.cjs +0 -304
  148. package/src/main/__tests__/dod-reverify.test.cjs +0 -285
  149. package/src/main/__tests__/epicContextDigest.test.cjs +0 -174
  150. package/src/main/__tests__/epicMint.test.cjs +0 -332
  151. package/src/main/__tests__/epicMintTelemetryTap.test.cjs +0 -64
  152. package/src/main/__tests__/epicStatusMirror.test.cjs +0 -110
  153. package/src/main/__tests__/epicValidationHook.test.cjs +0 -291
  154. package/src/main/__tests__/exchanges.test.cjs +0 -122
  155. package/src/main/__tests__/exchangesPromptId.test.cjs +0 -61
  156. package/src/main/__tests__/extractJson.test.cjs +0 -51
  157. package/src/main/__tests__/files-reject-credentials.test.cjs +0 -40
  158. package/src/main/__tests__/fixtures/1218-fo-01-move-scripts-lib-into-src-main-lib.log +0 -556
  159. package/src/main/__tests__/flatPrdTickSweep.test.cjs +0 -110
  160. package/src/main/__tests__/health-build-freshness.test.cjs +0 -39
  161. package/src/main/__tests__/health-claude-md-budget.test.cjs +0 -57
  162. package/src/main/__tests__/health-credentials.test.cjs +0 -81
  163. package/src/main/__tests__/health-delegation-chain.test.cjs +0 -124
  164. package/src/main/__tests__/health-per-project-stall.test.cjs +0 -84
  165. package/src/main/__tests__/health-prd-migration.test.cjs +0 -37
  166. package/src/main/__tests__/health-queue-dispatch.test.cjs +0 -135
  167. package/src/main/__tests__/health-starve-escalation.test.cjs +0 -94
  168. package/src/main/__tests__/health-tick-liveness.test.cjs +0 -179
  169. package/src/main/__tests__/health-usage-poller.test.cjs +0 -144
  170. package/src/main/__tests__/health-worktree-cap-blocked.test.cjs +0 -65
  171. package/src/main/__tests__/heapSnapshot.test.cjs +0 -121
  172. package/src/main/__tests__/historyAggregatorIntraday.test.cjs +0 -313
  173. package/src/main/__tests__/historyDashboard.test.cjs +0 -163
  174. package/src/main/__tests__/historyRollup.test.cjs +0 -333
  175. package/src/main/__tests__/intradayRefresh.test.cjs +0 -39
  176. package/src/main/__tests__/ipcSchemas-dependsOn.test.cjs +0 -39
  177. package/src/main/__tests__/kg-augment.test.cjs +0 -195
  178. package/src/main/__tests__/loadGateDetailTick.test.cjs +0 -31
  179. package/src/main/__tests__/machineProfile.test.cjs +0 -152
  180. package/src/main/__tests__/mcpStatus.test.cjs +0 -61
  181. package/src/main/__tests__/memoryAggregate.test.cjs +0 -109
  182. package/src/main/__tests__/memoryStale.test.cjs +0 -88
  183. package/src/main/__tests__/needsReviewLedger.test.cjs +0 -162
  184. package/src/main/__tests__/openExternalApp-spawn-error.test.cjs +0 -25
  185. package/src/main/__tests__/opsErrorLog.test.cjs +0 -109
  186. package/src/main/__tests__/opsErrorLogTelemetryTap.test.cjs +0 -173
  187. package/src/main/__tests__/personaMerge.test.cjs +0 -169
  188. package/src/main/__tests__/planValidator.test.cjs +0 -125
  189. package/src/main/__tests__/pollLoop-dispatch-on-failure.test.cjs +0 -176
  190. package/src/main/__tests__/prd-group-allocator.test.cjs +0 -119
  191. package/src/main/__tests__/prdAdminRouteParity.test.cjs +0 -70
  192. package/src/main/__tests__/prdAdminRoutes.test.cjs +0 -718
  193. package/src/main/__tests__/prdAgentType.test.cjs +0 -103
  194. package/src/main/__tests__/prdAuthoringSeed.test.cjs +0 -39
  195. package/src/main/__tests__/prdCreate.test.cjs +0 -979
  196. package/src/main/__tests__/prdCreateAdoption.test.cjs +0 -205
  197. package/src/main/__tests__/prdCreateDisposition.test.cjs +0 -201
  198. package/src/main/__tests__/prdCreatePlanId.test.cjs +0 -132
  199. package/src/main/__tests__/prdFrontmatterAgentType.test.cjs +0 -117
  200. package/src/main/__tests__/prdFrontmatterDependsOn.test.cjs +0 -136
  201. package/src/main/__tests__/prdFrontmatterDisposition.test.cjs +0 -125
  202. package/src/main/__tests__/prdFrontmatterQuietMachine.test.cjs +0 -108
  203. package/src/main/__tests__/prdLocations.test.cjs +0 -195
  204. package/src/main/__tests__/prdLocationsArchived.test.cjs +0 -201
  205. package/src/main/__tests__/prdMigration.test.cjs +0 -349
  206. package/src/main/__tests__/prdMigrationLegacyAdopt.test.cjs +0 -91
  207. package/src/main/__tests__/prdParserHighWater.test.cjs +0 -74
  208. package/src/main/__tests__/prdParserSourcePromptId.test.cjs +0 -65
  209. package/src/main/__tests__/prdSetDisposition.test.cjs +0 -222
  210. package/src/main/__tests__/prdSizing.test.cjs +0 -106
  211. package/src/main/__tests__/prdSourcePromptIdBackfill.test.cjs +0 -118
  212. package/src/main/__tests__/prdUpdateDependsOn.test.cjs +0 -160
  213. package/src/main/__tests__/proc-role-env.test.cjs +0 -125
  214. package/src/main/__tests__/procname-claude-spawn-sites.test.cjs +0 -304
  215. package/src/main/__tests__/procname-sm-processes.test.cjs +0 -127
  216. package/src/main/__tests__/projectHomeAdminRoutes.test.cjs +0 -177
  217. package/src/main/__tests__/projectPages.test.cjs +0 -137
  218. package/src/main/__tests__/promptSessionEvents.test.cjs +0 -234
  219. package/src/main/__tests__/promptSessionSchema.test.cjs +0 -101
  220. package/src/main/__tests__/promptSessionTranscript.test.cjs +0 -0
  221. package/src/main/__tests__/promptSessionsCreateEpicHandler.test.cjs +0 -159
  222. package/src/main/__tests__/pty-epic-worktree-spawn-cwd.test.cjs +0 -282
  223. package/src/main/__tests__/pty-session-open-telemetry.test.cjs +0 -96
  224. package/src/main/__tests__/pty-write-result.test.cjs +0 -46
  225. package/src/main/__tests__/queue-health-verdict.test.cjs +0 -170
  226. package/src/main/__tests__/queue-starvation-dispatch-driver.test.cjs +0 -286
  227. package/src/main/__tests__/queue-starvation-per-project.test.cjs +0 -147
  228. package/src/main/__tests__/queueHistory.test.cjs +0 -355
  229. package/src/main/__tests__/queueOps-interactive-ac-lint.test.cjs +0 -65
  230. package/src/main/__tests__/queueOpsArchiveDestination.test.cjs +0 -65
  231. package/src/main/__tests__/queueOpsAutoArchive.test.cjs +0 -211
  232. package/src/main/__tests__/rateLimitPollerStreak.test.cjs +0 -128
  233. package/src/main/__tests__/rcaReport.test.cjs +0 -266
  234. package/src/main/__tests__/reconcileFlatPrdSweep.test.cjs +0 -119
  235. package/src/main/__tests__/reconcileTiming.test.cjs +0 -135
  236. package/src/main/__tests__/runLogRetention.test.cjs +0 -489
  237. package/src/main/__tests__/runVerify-atomic-verdicts.test.cjs +0 -26
  238. package/src/main/__tests__/runVerify-blocked-by-foreign-wip.test.cjs +0 -58
  239. package/src/main/__tests__/runVerify-landed-commit-outranks.test.cjs +0 -191
  240. package/src/main/__tests__/runVerify-policy-denial.test.cjs +0 -89
  241. package/src/main/__tests__/runVerify-transcript-commit-evidence.test.cjs +0 -225
  242. package/src/main/__tests__/runVerify.test.cjs +0 -1784
  243. package/src/main/__tests__/scheduleJobSchema.test.cjs +0 -127
  244. package/src/main/__tests__/scheduleJobStatusDrift.test.cjs +0 -65
  245. package/src/main/__tests__/scheduleJobTransitions.test.cjs +0 -277
  246. package/src/main/__tests__/scheduleJobTransitionsGrep.test.cjs +0 -59
  247. package/src/main/__tests__/scheduleJobTransitionsTelemetryTap.test.cjs +0 -72
  248. package/src/main/__tests__/scheduler-admin-routes.test.cjs +0 -199
  249. package/src/main/__tests__/scheduler-adopted-run-supervision.test.cjs +0 -143
  250. package/src/main/__tests__/scheduler-already-satisfied-on-main.test.cjs +0 -105
  251. package/src/main/__tests__/scheduler-archive-completed-prd.test.cjs +0 -102
  252. package/src/main/__tests__/scheduler-archived-twin-guard.test.cjs +0 -155
  253. package/src/main/__tests__/scheduler-autofix-outcome.test.cjs +0 -188
  254. package/src/main/__tests__/scheduler-autofix-select.test.cjs +0 -439
  255. package/src/main/__tests__/scheduler-autopromote.test.cjs +0 -51
  256. package/src/main/__tests__/scheduler-bash-timeout-env.test.cjs +0 -101
  257. package/src/main/__tests__/scheduler-blocked-by-foreign-wip.test.cjs +0 -107
  258. package/src/main/__tests__/scheduler-boot-orphans.test.cjs +0 -153
  259. package/src/main/__tests__/scheduler-broadcast-reconcile.test.cjs +0 -121
  260. package/src/main/__tests__/scheduler-clear-queue-history.test.cjs +0 -134
  261. package/src/main/__tests__/scheduler-commit-guard-noop.test.cjs +0 -244
  262. package/src/main/__tests__/scheduler-committed-in-window.test.cjs +0 -182
  263. package/src/main/__tests__/scheduler-cross-project-batch.test.cjs +0 -43
  264. package/src/main/__tests__/scheduler-default-eligible-heal.test.cjs +0 -168
  265. package/src/main/__tests__/scheduler-dispatch-loop.test.cjs +0 -58
  266. package/src/main/__tests__/scheduler-effective-concurrency.test.cjs +0 -81
  267. package/src/main/__tests__/scheduler-epic-digest.test.cjs +0 -227
  268. package/src/main/__tests__/scheduler-failed-autoreset.test.cjs +0 -121
  269. package/src/main/__tests__/scheduler-finalize-dispatch-guards.test.cjs +0 -229
  270. package/src/main/__tests__/scheduler-find-prd-dir.test.cjs +0 -75
  271. package/src/main/__tests__/scheduler-fix-plan-path.test.cjs +0 -119
  272. package/src/main/__tests__/scheduler-force-tick-outcome.test.cjs +0 -46
  273. package/src/main/__tests__/scheduler-foreign-wip-manifest.test.cjs +0 -78
  274. package/src/main/__tests__/scheduler-gate-shadow.test.cjs +0 -119
  275. package/src/main/__tests__/scheduler-guard-verdict-autoresolve.test.cjs +0 -390
  276. package/src/main/__tests__/scheduler-heal-refusal.test.cjs +0 -61
  277. package/src/main/__tests__/scheduler-heartbeat-payload.test.cjs +0 -80
  278. package/src/main/__tests__/scheduler-inplace-salvage.test.cjs +0 -323
  279. package/src/main/__tests__/scheduler-integration-failure-stamp.test.cjs +0 -41
  280. package/src/main/__tests__/scheduler-investigation-clean-skip.test.cjs +0 -63
  281. package/src/main/__tests__/scheduler-investigation-prompt.test.cjs +0 -123
  282. package/src/main/__tests__/scheduler-job-budget.test.cjs +0 -172
  283. package/src/main/__tests__/scheduler-job-overrun.test.cjs +0 -175
  284. package/src/main/__tests__/scheduler-launch-failure.test.cjs +0 -199
  285. package/src/main/__tests__/scheduler-leftover-fields.test.cjs +0 -52
  286. package/src/main/__tests__/scheduler-leftover-quarantine.test.cjs +0 -199
  287. package/src/main/__tests__/scheduler-looks-done.test.cjs +0 -537
  288. package/src/main/__tests__/scheduler-manual-pause.test.cjs +0 -118
  289. package/src/main/__tests__/scheduler-mechanical-recovery.test.cjs +0 -245
  290. package/src/main/__tests__/scheduler-meta-code-sha.test.cjs +0 -46
  291. package/src/main/__tests__/scheduler-mutate-reentrancy.test.cjs +0 -117
  292. package/src/main/__tests__/scheduler-needs-review-autoresolve.test.cjs +0 -197
  293. package/src/main/__tests__/scheduler-never-stop.test.cjs +0 -157
  294. package/src/main/__tests__/scheduler-no-dead-end-status.test.cjs +0 -152
  295. package/src/main/__tests__/scheduler-no-orphan-run-dir.test.cjs +0 -81
  296. package/src/main/__tests__/scheduler-notify-originating-tab-transcript.test.cjs +0 -87
  297. package/src/main/__tests__/scheduler-notify-originating-tab.test.cjs +0 -343
  298. package/src/main/__tests__/scheduler-periodic-reverify-guard.test.cjs +0 -217
  299. package/src/main/__tests__/scheduler-porcelain-rename.test.cjs +0 -164
  300. package/src/main/__tests__/scheduler-prd-missing-skip.test.cjs +0 -161
  301. package/src/main/__tests__/scheduler-prd-persona-spawn.test.cjs +0 -178
  302. package/src/main/__tests__/scheduler-quarantine-autoresolve.test.cjs +0 -165
  303. package/src/main/__tests__/scheduler-quiet-machine-lease.test.cjs +0 -257
  304. package/src/main/__tests__/scheduler-rate-limit-cooldown-freshness.test.cjs +0 -123
  305. package/src/main/__tests__/scheduler-rate-limit-pause.test.cjs +0 -152
  306. package/src/main/__tests__/scheduler-rate-limit-spin-guard.test.cjs +0 -156
  307. package/src/main/__tests__/scheduler-reap-dead-running-jobs.test.cjs +0 -754
  308. package/src/main/__tests__/scheduler-reaper-helpers-basics.test.cjs +0 -87
  309. package/src/main/__tests__/scheduler-reconcile-cwd-preserve.test.cjs +0 -100
  310. package/src/main/__tests__/scheduler-reconcile-history-backfill.test.cjs +0 -105
  311. package/src/main/__tests__/scheduler-reconcile-invalid-repair.test.cjs +0 -203
  312. package/src/main/__tests__/scheduler-reconcile-quarantine.test.cjs +0 -247
  313. package/src/main/__tests__/scheduler-reset-job-fields-guard.test.cjs +0 -77
  314. package/src/main/__tests__/scheduler-resume-recovery.test.cjs +0 -254
  315. package/src/main/__tests__/scheduler-shard-quarantine.test.cjs +0 -115
  316. package/src/main/__tests__/scheduler-shared-tree-guard.test.cjs +0 -380
  317. package/src/main/__tests__/scheduler-sigterm-commit.test.cjs +0 -43
  318. package/src/main/__tests__/scheduler-stall-per-project.test.cjs +0 -126
  319. package/src/main/__tests__/scheduler-starve-escalation.test.cjs +0 -154
  320. package/src/main/__tests__/scheduler-stranded-autofix-park.test.cjs +0 -252
  321. package/src/main/__tests__/scheduler-stranded-investigation.test.cjs +0 -185
  322. package/src/main/__tests__/scheduler-stuck-failed-escalation.test.cjs +0 -136
  323. package/src/main/__tests__/scheduler-supervisor-record.test.cjs +0 -81
  324. package/src/main/__tests__/scheduler-tick-cancel-token.test.cjs +0 -54
  325. package/src/main/__tests__/scheduler-tick-wedge.test.cjs +0 -172
  326. package/src/main/__tests__/scheduler-transient-failure.test.cjs +0 -141
  327. package/src/main/__tests__/scheduler-unreadable-queue-guard.test.cjs +0 -62
  328. package/src/main/__tests__/scheduler-utilization-hold.test.cjs +0 -89
  329. package/src/main/__tests__/scheduler-verify-prd-path.test.cjs +0 -109
  330. package/src/main/__tests__/scheduler-worktree-cap-defer.test.cjs +0 -234
  331. package/src/main/__tests__/scheduler-worktree-exec-cwd.test.cjs +0 -120
  332. package/src/main/__tests__/scheduler-writeprd-epic-rollback.test.cjs +0 -105
  333. package/src/main/__tests__/schedulerBatchRootBlocker.test.cjs +0 -117
  334. package/src/main/__tests__/schedulerStateSidecarRestore.test.cjs +0 -110
  335. package/src/main/__tests__/seedAgentPersonas.test.cjs +0 -184
  336. package/src/main/__tests__/seedSchedulerMcp.test.cjs +0 -211
  337. package/src/main/__tests__/seedStatus.test.cjs +0 -100
  338. package/src/main/__tests__/seedValidatorPersona.test.cjs +0 -116
  339. package/src/main/__tests__/stop-signal-anchor.test.cjs +0 -75
  340. package/src/main/__tests__/telemetryClient.test.cjs +0 -1055
  341. package/src/main/__tests__/telemetryContract.test.cjs +0 -930
  342. package/src/main/__tests__/telemetrySettings.test.cjs +0 -210
  343. package/src/main/__tests__/transcripts-batch-flush.test.cjs +0 -249
  344. package/src/main/__tests__/transcripts-doFlush-array.test.cjs +0 -124
  345. package/src/main/__tests__/transcripts-paged-reads.test.cjs +0 -241
  346. package/src/main/__tests__/transcripts-worktree-epic-path.test.cjs +0 -154
  347. package/src/main/__tests__/transcriptsUsageFor.test.cjs +0 -206
  348. package/src/main/__tests__/uniquePrdNumbers.test.cjs +0 -153
  349. package/src/main/__tests__/usageSingleFlight.test.cjs +0 -169
  350. package/src/main/__tests__/validationSentinels.test.cjs +0 -84
  351. package/src/main/__tests__/workTypeLibrary.test.cjs +0 -89
  352. package/src/main/bilkoHost.cjs +0 -314
  353. package/src/main/bilkoHostCore.cjs +0 -89
  354. package/src/main/lib/__tests__/active-sessions.test.cjs +0 -251
  355. package/src/main/lib/__tests__/activeIndexRebuild.test.cjs +0 -179
  356. package/src/main/lib/__tests__/agentPersonaSchema.test.cjs +0 -67
  357. package/src/main/lib/__tests__/auditLog.test.cjs +0 -38
  358. package/src/main/lib/__tests__/bootSelfHeal.test.cjs +0 -107
  359. package/src/main/lib/__tests__/branchSweep.test.cjs +0 -164
  360. package/src/main/lib/__tests__/buildIdentity.test.cjs +0 -121
  361. package/src/main/lib/__tests__/buildTarget.test.cjs +0 -52
  362. package/src/main/lib/__tests__/childWithLog.test.cjs +0 -321
  363. package/src/main/lib/__tests__/coldBootPromptSessionsWrite.test.cjs +0 -87
  364. package/src/main/lib/__tests__/crashTelemetry.test.cjs +0 -103
  365. package/src/main/lib/__tests__/credentials-futile-refresh.test.cjs +0 -115
  366. package/src/main/lib/__tests__/cwdClassify.test.cjs +0 -111
  367. package/src/main/lib/__tests__/definitionOfDoneSequence.test.cjs +0 -95
  368. package/src/main/lib/__tests__/delegationReadiness.test.cjs +0 -1175
  369. package/src/main/lib/__tests__/dispatchLoop.test.cjs +0 -63
  370. package/src/main/lib/__tests__/effectiveModelInfo.test.cjs +0 -244
  371. package/src/main/lib/__tests__/ephemeralCwd.test.cjs +0 -91
  372. package/src/main/lib/__tests__/epicDelegationStats.test.cjs +0 -137
  373. package/src/main/lib/__tests__/epicSpawnCwd.test.cjs +0 -283
  374. package/src/main/lib/__tests__/epicSpawnPlan.test.cjs +0 -196
  375. package/src/main/lib/__tests__/epicTranscriptPath.test.cjs +0 -163
  376. package/src/main/lib/__tests__/epicWorktreeBoot.test.cjs +0 -136
  377. package/src/main/lib/__tests__/epicWorktreeMerge.test.cjs +0 -130
  378. package/src/main/lib/__tests__/epicWorktreeMint.test.cjs +0 -117
  379. package/src/main/lib/__tests__/epicWorktreeProjectConfig.test.cjs +0 -137
  380. package/src/main/lib/__tests__/fixChainDepth.test.cjs +0 -40
  381. package/src/main/lib/__tests__/fixtures/204-mercury-steam-horse.log.txt +0 -13
  382. package/src/main/lib/__tests__/fixtures/scheduler-machine.json.corrupt-1789147548 +0 -34
  383. package/src/main/lib/__tests__/gateFixtures.json +0 -20
  384. package/src/main/lib/__tests__/gitCacheBound.test.cjs +0 -69
  385. package/src/main/lib/__tests__/gitWorktree.test.cjs +0 -1478
  386. package/src/main/lib/__tests__/gitWorktreeSalvage.test.cjs +0 -107
  387. package/src/main/lib/__tests__/gitWorktreeSalvageDelta.test.cjs +0 -153
  388. package/src/main/lib/__tests__/guardShims.test.cjs +0 -158
  389. package/src/main/lib/__tests__/importReferences.spec.cjs +0 -56
  390. package/src/main/lib/__tests__/instanceLock.test.cjs +0 -173
  391. package/src/main/lib/__tests__/jobSupervisorRecord.test.cjs +0 -78
  392. package/src/main/lib/__tests__/jobWorktree.test.cjs +0 -199
  393. package/src/main/lib/__tests__/jobWorktreeBootLive.test.cjs +0 -82
  394. package/src/main/lib/__tests__/landedSinceRun.test.cjs +0 -133
  395. package/src/main/lib/__tests__/launchFailure.test.cjs +0 -220
  396. package/src/main/lib/__tests__/loadGate.test.cjs +0 -303
  397. package/src/main/lib/__tests__/localAdminHttp.test.cjs +0 -214
  398. package/src/main/lib/__tests__/loopDelay.test.cjs +0 -68
  399. package/src/main/lib/__tests__/mcpToolCatalog.test.cjs +0 -107
  400. package/src/main/lib/__tests__/modelCatalog.test.cjs +0 -202
  401. package/src/main/lib/__tests__/opsOwnership.test.cjs +0 -113
  402. package/src/main/lib/__tests__/opsRootAbsoluteCwd.test.cjs +0 -328
  403. package/src/main/lib/__tests__/opsRootNestedWrite.test.cjs +0 -51
  404. package/src/main/lib/__tests__/opsRootResolve.test.cjs +0 -149
  405. package/src/main/lib/__tests__/prdDeclaredPaths.test.cjs +0 -82
  406. package/src/main/lib/__tests__/prdDisposition.test.cjs +0 -224
  407. package/src/main/lib/__tests__/procIdentity.test.cjs +0 -119
  408. package/src/main/lib/__tests__/procName.test.cjs +0 -92
  409. package/src/main/lib/__tests__/projectBriefCore.test.cjs +0 -216
  410. package/src/main/lib/__tests__/projectRootResolve.test.cjs +0 -148
  411. package/src/main/lib/__tests__/queueHealth.test.cjs +0 -58
  412. package/src/main/lib/__tests__/queueStoreAtomicWrite.test.cjs +0 -88
  413. package/src/main/lib/__tests__/queueStoreMachineStateRecovery.test.cjs +0 -190
  414. package/src/main/lib/__tests__/quietMachineLease.test.cjs +0 -39
  415. package/src/main/lib/__tests__/rateLimitWindow.test.cjs +0 -88
  416. package/src/main/lib/__tests__/reaperHelpers.test.cjs +0 -577
  417. package/src/main/lib/__tests__/schedulerBatchDepends.test.cjs +0 -312
  418. package/src/main/lib/__tests__/schedulerBatchFairness.test.cjs +0 -213
  419. package/src/main/lib/__tests__/schedulerBatchLaunchHold.test.cjs +0 -125
  420. package/src/main/lib/__tests__/schedulerBatchProjectCap.test.cjs +0 -127
  421. package/src/main/lib/__tests__/schedulerBatchQuietMachine.test.cjs +0 -109
  422. package/src/main/lib/__tests__/schedulerMcpServerHeadlessRefusal.test.cjs +0 -71
  423. package/src/main/lib/__tests__/schedulerMcpServerHelp.test.cjs +0 -216
  424. package/src/main/lib/__tests__/schedulerMcpServerProjectHome.test.cjs +0 -183
  425. package/src/main/lib/__tests__/schedulerPaths.test.cjs +0 -226
  426. package/src/main/lib/__tests__/schedulerPathsWorktree.test.cjs +0 -133
  427. package/src/main/lib/__tests__/schedulerRuntimeState.test.cjs +0 -56
  428. package/src/main/lib/__tests__/sessionSlots.test.cjs +0 -144
  429. package/src/main/lib/__tests__/telemetryBacklog.test.cjs +0 -626
  430. package/src/main/lib/__tests__/telemetryBoot.test.cjs +0 -134
  431. package/src/main/lib/__tests__/telemetryConsent.test.cjs +0 -136
  432. package/src/main/lib/__tests__/telemetryCounters.test.cjs +0 -57
  433. package/src/main/lib/__tests__/telemetryCountersMetadataColumn.test.cjs +0 -98
  434. package/src/main/lib/__tests__/terminalRunOutcome.test.cjs +0 -200
  435. package/src/main/lib/__tests__/toolUseClassify.test.cjs +0 -53
  436. package/src/main/lib/__tests__/updateCheck.test.cjs +0 -63
  437. package/src/main/lib/__tests__/upgradeDrain.test.cjs +0 -130
  438. package/src/main/lib/__tests__/usageCircuit.test.cjs +0 -354
  439. package/src/main/lib/__tests__/watchdog-helpers.test.cjs +0 -441
  440. package/src/main/lib/__tests__/watchdog-relaunch.test.cjs +0 -266
  441. 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
- });