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
@@ -11,8 +11,8 @@
11
11
  * anymore — both just point the headless executor at STANDARDS_PATH with an
12
12
  * instruction to read it before starting. There's nothing to go stale: the
13
13
  * executor always reads the live file at run time, same one-concept-one-
14
- * implementation reasoning that keeps the two PRD-creation paths in sync
15
- * (see SKILL.md).
14
+ * implementation reasoning that keeps the two PRD-creation paths (admin
15
+ * HTTP route, chat:create-prd IPC) in sync.
16
16
  */
17
17
  'use strict';
18
18
 
@@ -32,6 +32,8 @@ const { resolveDepSlug, findNearMatches } = require('./depSlugResolve.cjs');
32
32
  const { isFixPlanSlug } = require('./fixPlanSlug.cjs');
33
33
  const { isIncomplete, resolveChainTerminals, isValidPlanId, mintPlanId, resolveInheritedPlanId } = require('./prdDisposition.cjs');
34
34
  const { sizingWarnings } = require('./prdSizing.cjs');
35
+ const { validateGate, validateFiles, renderFilesSection, renderGateSection } = require('./prdGateFiles.cjs');
36
+ const { resolveGate, parseChain, isNoneGate } = require('./definitionOfDone.cjs');
35
37
 
36
38
  // A caller-supplied slug that already starts with its own `NN-` (e.g.
37
39
  // "254-perf-x") used to silently become the double-prefixed row
@@ -68,18 +70,19 @@ function deriveSlugFromTitle(title) {
68
70
 
69
71
  /**
70
72
  * Build the full PRD markdown body (frontmatter + required sections + a
71
- * pointer at the engineering standards file), matching the structure
72
- * `/develop`'s SKILL.md documents: frontmatter, then Goal / Acceptance
73
- * criteria / Implementation notes / Out of scope / Engineering standards,
74
- * in order.
73
+ * pointer at the engineering standards file), matching the body shape
74
+ * PRD_AUTHORING.md §13 documents: frontmatter, then Goal / Acceptance
75
+ * criteria / Files / Implementation notes / Out of scope / Gate /
76
+ * Engineering standards, in order.
75
77
  */
76
78
  function buildPrdBody(input) {
77
79
  const {
78
80
  title, cwd, estimateMinutes, goal, acceptanceCriteria,
79
81
  implementationNotes, outOfScope, sourcePromptId, sourceTabId, tag, agentType, dependsOn, quietMachine, disposition, deliverable, artifactPaths, planId,
82
+ gate, files,
80
83
  } = input;
81
84
 
82
- // No `parallelGroup` frontmatter key by convention (SKILL.md) — the NN-
85
+ // No `parallelGroup` frontmatter key (PRD_AUTHORING.md §5) — the NN-
83
86
  // filename prefix is the single source of truth for grouping; adding a
84
87
  // second one here would let the two drift out of sync.
85
88
  const fmLines = ['---', `title: ${title}`, `cwd: ${cwd}`, `estimateMinutes: ${estimateMinutes}`];
@@ -140,13 +143,22 @@ function buildPrdBody(input) {
140
143
  'do not re-read the whole file every run.',
141
144
  ].join('\n');
142
145
 
146
+ // `# Files` sits right after Acceptance criteria; `# Gate` sits after Out
147
+ // of scope, before Engineering standards (gate-and-files-in-PRD-body unit).
148
+ // Each section appears only when its field is given. Only the MCP tool
149
+ // refuses a call without them; createPrd() (so the HTTP route and the IPC
150
+ // handler too) just warns when either is absent.
143
151
  const bodyLines = [
144
152
  '# Goal', '', goal, '',
145
153
  '# Acceptance criteria', '', acLines, '',
154
+ ];
155
+ if (files && files.length) bodyLines.push(...renderFilesSection(files));
156
+ bodyLines.push(
146
157
  '# Implementation notes', '', implementationNotes, '',
147
158
  '# Out of scope', '', oosLines, '',
148
- '## Engineering standards', '', standardsPointer, '',
149
- ];
159
+ );
160
+ if (gate && gate.length) bodyLines.push(...renderGateSection(gate));
161
+ bodyLines.push('## Engineering standards', '', standardsPointer, '');
150
162
 
151
163
  return `${fmLines.join('\n')}${bodyLines.join('\n')}`;
152
164
  }
@@ -347,6 +359,67 @@ async function createPrd(input, remote) {
347
359
  }
348
360
  }
349
361
 
362
+ // Fake-heading guard (gate-and-files-in-PRD-body unit): a caller-supplied
363
+ // free-text field that itself contains a "# Gate" or "# Files" heading line
364
+ // would, after buildPrdBody splices in the real section below, read as TWO
365
+ // Gate/Files sections — readExplicitGate takes the FIRST fence/heading it
366
+ // finds, so the fake one would silently win and the executor would see the
367
+ // wrong gate. Checked before NN allocation so a refusal burns no number.
368
+ const HEADING_LINE_RE = /^ {0,3}#{1,6}[ \t]+(gate|files)[ \t]*#*[ \t]*$/i;
369
+ function headingGuardError(fieldLabel, text) {
370
+ if (typeof text !== 'string') return null;
371
+ for (const line of text.split(/\r\n|\r|\n/)) {
372
+ if (HEADING_LINE_RE.test(line)) {
373
+ return `The ${fieldLabel} text has a "# Gate" or "# Files" heading. The API writes those sections ` +
374
+ 'from the gate and files fields. Remove the heading and call again.';
375
+ }
376
+ }
377
+ return null;
378
+ }
379
+ for (const [fieldLabel, text] of [['goal', input.goal], ['implementation notes', input.implementationNotes]]) {
380
+ const headingError = headingGuardError(fieldLabel, text);
381
+ if (headingError) return { ok: false, status: 400, error: headingError };
382
+ }
383
+ if (Array.isArray(input.acceptanceCriteria)) {
384
+ for (const entry of input.acceptanceCriteria) {
385
+ const headingError = headingGuardError('acceptance criteria', entry);
386
+ if (headingError) return { ok: false, status: 400, error: headingError };
387
+ }
388
+ }
389
+ if (Array.isArray(input.outOfScope)) {
390
+ for (const entry of input.outOfScope) {
391
+ const headingError = headingGuardError('out of scope', entry);
392
+ if (headingError) return { ok: false, status: 400, error: headingError };
393
+ }
394
+ }
395
+
396
+ // Gate + files validation (gate-and-files-in-PRD-body unit): fail-closed,
397
+ // checked BEFORE NN allocation so a refusal burns no number — same posture
398
+ // as the artifactPaths checks above. The actual shape rules live in
399
+ // prdGateFiles.cjs (pure, no I/O); this is just the accumulate-and-return glue.
400
+ const extraWarnings = [];
401
+ let gateCommands;
402
+ if (input.gate !== undefined) {
403
+ const gateValidation = validateGate(input.gate);
404
+ if (!gateValidation.ok) {
405
+ return { ok: false, status: 400, error: gateValidation.error };
406
+ }
407
+ gateCommands = gateValidation.commands;
408
+ extraWarnings.push(...gateValidation.warnings);
409
+ } else {
410
+ extraWarnings.push('No gate: the scheduler cannot re-check this PRD.');
411
+ }
412
+ let filesList;
413
+ if (input.files !== undefined) {
414
+ const filesValidation = validateFiles(input.files);
415
+ if (!filesValidation.ok) {
416
+ return { ok: false, status: 400, error: filesValidation.error };
417
+ }
418
+ filesList = filesValidation.files;
419
+ } else {
420
+ extraWarnings.push('No files list: the executor is not told which files it may change.');
421
+ }
422
+
350
423
  const nn = await remote.allocateParallelGroup(input.cwd);
351
424
  const filenameSlug = `${nn}-${slug}`;
352
425
 
@@ -463,7 +536,32 @@ async function createPrd(input, remote) {
463
536
  }
464
537
  if (!planId) planId = mintPlanId();
465
538
 
466
- const body = buildPrdBody({ ...input, planId });
539
+ const body = buildPrdBody({ ...input, planId, gate: gateCommands, files: filesList });
540
+
541
+ // Read-back check (gate-and-files-in-PRD-body unit): the whole point of a
542
+ // declared gate is that the scheduler's own reader (resolveGate/parseChain
543
+ // in definitionOfDone.cjs) gets back exactly what was declared. A stray
544
+ // ```gate fence elsewhere in the body (goal/notes/criteria/out-of-scope)
545
+ // would otherwise win — readExplicitGate takes the FIRST fence in the
546
+ // text — and the scheduler would silently re-run the wrong gate. Caught
547
+ // here, before the write, rather than discovered later from a job log.
548
+ if (gateCommands) {
549
+ const expectedSource = gateCommands.length === 1 && isNoneGate(gateCommands[0]) ? 'none' : 'explicit';
550
+ const expectedSequence = expectedSource === 'none' ? [] : gateCommands.flatMap(parseChain);
551
+ const resolved = resolveGate(body);
552
+ const matches = resolved.source === expectedSource
553
+ && JSON.stringify(resolved.sequence) === JSON.stringify(expectedSequence);
554
+ if (!matches) {
555
+ return {
556
+ ok: false,
557
+ status: 400,
558
+ error: 'The PRD text already holds a gate fence (three backticks + gate), so the scheduler would read '
559
+ + 'the wrong gate. Remove it from the goal, implementation notes, acceptance criteria, out of scope '
560
+ + 'or files, and call again.',
561
+ };
562
+ }
563
+ }
564
+
467
565
  const writeResult = await remote.writePrd(filenameSlug, body, input.cwd);
468
566
  if (!writeResult?.ok) {
469
567
  return { ok: false, status: 500, error: writeResult?.error ?? 'write failed' };
@@ -480,25 +578,59 @@ async function createPrd(input, remote) {
480
578
  sourceTabId: input.sourceTabId ?? null,
481
579
  });
482
580
 
581
+ // PRD 1446: writePrd() succeeding used to just promise "the next scheduler
582
+ // reconcile pass (~1 minute)" adopts this file — but nothing reliably ran
583
+ // that pass (the 60s pollLoop only reaches reconcile() via
584
+ // maybeLaunchWhenAvailable, which returns early while zero rows are
585
+ // pending; the 10-minute rescheduleTimer could itself stall behind a slow
586
+ // billing fetch). Trigger one reconcile immediately through the EXISTING
587
+ // broadcast-coalescer seam (remote.requestReconcile(), same one resetJob
588
+ // already uses) instead of waiting on either. Best-effort: a reconcile
589
+ // failure here must not fail a write that already landed on disk — the
590
+ // PRD file is real regardless, and the next scheduled pass still adopts
591
+ // it if this one didn't stick.
592
+ let enqueued = false;
593
+ if (typeof remote.requestReconcile === 'function') {
594
+ try {
595
+ await remote.requestReconcile();
596
+ if (typeof remote.getJob === 'function') {
597
+ enqueued = Boolean(await remote.getJob(filenameSlug));
598
+ }
599
+ } catch (e) {
600
+ console.warn(`[prdCreate] immediate requestReconcile failed (non-fatal, next scheduled pass will adopt it): ${e?.message ?? e}`);
601
+ }
602
+ }
603
+
483
604
  // No `status` field here, and never one named 'queued' — that string was
484
605
  // handed straight to callers as if it were a real ScheduleJob status, and
485
606
  // is exactly how the 1021/1022 incident's invalid `"status": "queued"`
486
607
  // rows got onto disk (this route's own response was the most likely
487
608
  // source). A PRD is a queue row's SOURCE, not the row itself: writePrd
488
- // only ever joins an Epic and writes the .md file — the queue row is
489
- // created separately, by the next reconcile() pass. `enqueued: false` is
490
- // true by construction (this function never touches queue.json), so no
491
- // caller can copy a fake job status out of this response again.
609
+ // only ever joins an Epic and writes the .md file — the queue row above
610
+ // is created separately, by reconcile(). `enqueued` reflects whether that
611
+ // reconcile pass (triggered synchronously above) actually produced the
612
+ // row before this function returned — it is not a fabricated status.
492
613
  return {
493
614
  ok: true,
494
615
  nn,
495
616
  filename: `${filenameSlug}.md`,
496
617
  prdPath: writeResult.path ?? null,
497
618
  epicId: writeResult.epicId ?? null,
498
- enqueued: false,
499
- note: 'PRD file written; the queue row is derived by the next scheduler reconcile pass, not created here',
500
- // Advisory only (PRD 1403) — never blocks the write. See prdSizing.cjs.
501
- warnings: sizingWarnings(input),
619
+ enqueued,
620
+ note: enqueued
621
+ ? 'PRD file written and adopted into the queue as a pending row'
622
+ : 'PRD file written; the queue row is derived by the next scheduler reconcile pass, not created here',
623
+ // gate-and-files-in-PRD-body unit: honest echo of what was actually
624
+ // validated and rendered, same spirit as `note` above — never a fabricated status.
625
+ gate: {
626
+ source: gateCommands ? (gateCommands.length === 1 && isNoneGate(gateCommands[0]) ? 'none' : 'explicit') : 'absent',
627
+ commands: gateCommands || [],
628
+ },
629
+ files: filesList || [],
630
+ // Advisory only (PRD 1403 sizing warnings + gate-and-files-in-PRD-body
631
+ // absent/none-gate warnings) — never blocks the write. See prdSizing.cjs
632
+ // and prdGateFiles.cjs.
633
+ warnings: [...extraWarnings, ...sizingWarnings(input)],
502
634
  };
503
635
  }
504
636
 
@@ -542,8 +674,10 @@ function registerAdminRoute(adminHttp, remote) {
542
674
  filename: result.filename,
543
675
  prdPath: result.prdPath ?? null,
544
676
  epicId: result.epicId ?? null,
545
- enqueued: false,
677
+ enqueued: result.enqueued,
546
678
  note: result.note,
679
+ gate: result.gate,
680
+ files: result.files,
547
681
  warnings: result.warnings,
548
682
  });
549
683
  });
@@ -18,7 +18,7 @@
18
18
  * an already-written PRD's disposition from the Scheduler UI. Uses
19
19
  * computeDispositionRewrite() to validate the rewrite is safe before
20
20
  * handing the new dependsOn to remote.updatePrd (which itself refuses a
21
- * non-pending/quarantined row — see scheduler.cjs's updatePrd).
21
+ * 'running' or 'completed' row — see scheduler.cjs's updatePrd).
22
22
  *
23
23
  * `rows` throughout is the same shape listPrdsInternal() already produces:
24
24
  * { slug, status, dependsOn }. dependsOn entries are resolved against `rows`
@@ -151,7 +151,7 @@ function wouldCreateCycle(rows, slug, newDependsOn) {
151
151
  * behind another chain" action). Pure — returns the new dependsOn value on
152
152
  * success; the caller (scheduler.cjs's IPC handler) is responsible for
153
153
  * actually persisting it via remote.updatePrd, which independently refuses
154
- * a non-pending/quarantined row (defense in depth, not duplicated here).
154
+ * a 'running' or 'completed' row (defense in depth, not duplicated here).
155
155
  *
156
156
  * Refuses (ok: false) when:
157
157
  * - `slug` isn't a known row.
@@ -0,0 +1,284 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * prdGateFiles.cjs — pure validate + render helpers for a PRD's `gate` and
5
+ * `files` fields (gate-and-files-in-PRD-body unit).
6
+ *
7
+ * Why: 74% of parked jobs had no gate the scheduler could read. The planner
8
+ * now declares the gate commands and the files list when it calls
9
+ * scheduler_create_prd; prdCreate.cjs's createPrd() validates them with the
10
+ * functions here, then renders them as two PRD body sections (`# Files`,
11
+ * `# Gate`). `resolveGate`/`parseChain` in definitionOfDone.cjs already read
12
+ * a fenced ```gate block back — renderGateSection's output must round-trip
13
+ * through them unchanged (checked by createPrd(), not here).
14
+ *
15
+ * No I/O, no scheduler imports — pure string-in, string/array-out.
16
+ */
17
+
18
+ const { parseChain, explainChain, isNoneGate } = require('./definitionOfDone.cjs');
19
+
20
+ const GATE_MIN_ENTRIES = 1;
21
+ const GATE_MAX_ENTRIES = 10;
22
+ const GATE_ENTRY_MAX_CHARS = 500;
23
+ const FILES_MIN_ENTRIES = 1;
24
+ const FILES_MAX_ENTRIES = 50;
25
+ const FILES_ENTRY_MAX_CHARS = 300;
26
+
27
+ const NONE_GATE_WARNING =
28
+ 'Gate is none: the scheduler cannot re-check this PRD. Use none only for docs or config with no runnable check.';
29
+
30
+ // Same whitespace/control-char rule explainChain's rule 1 uses
31
+ // (definitionOfDone.cjs), minus tab — a tab has no legitimate place in a
32
+ // repo-relative file path.
33
+ const FILES_FOREIGN_WHITESPACE_RE = /[^\S ]|[\u0000-\u0008\u000A-\u001F\u007F]/;
34
+
35
+ // Chars stripped from a gate entry's first word before comparing it to
36
+ // "none" — quotes, plus the punctuation a typo or trailing comment tends to
37
+ // add (`none.`, `"none"`, `none!`).
38
+ const NONE_PUNCT_STRIP_RE = /^[.,;:!?()[\]{}#'"]+|[.,;:!?()[\]{}#'"]+$/g;
39
+
40
+ /** Cut a string to 80 chars for an error message, marking truncation with "…". */
41
+ function cut80(s) {
42
+ return s.length > 80 ? `${s.slice(0, 80)}…` : s;
43
+ }
44
+
45
+ /** `<field> entry <n> ("<cut entry>"): <rule>` — the shared error-text shape. */
46
+ function entryError(field, index, entry, rule) {
47
+ return `${field} entry ${index} ("${cut80(entry)}"): ${rule}`;
48
+ }
49
+
50
+ /**
51
+ * True when `entry` reads as a typo'd "none" opt-out (`none.`, `"none"`,
52
+ * `none # docs`, `NONE!`) but is not the exact opt-out itself. The exact
53
+ * opt-out (isNoneGate) is never a near-miss.
54
+ */
55
+ function isNearMissNone(entry) {
56
+ if (isNoneGate(entry)) return false;
57
+ const firstWord = entry.split(/[ \t]+/)[0] ?? '';
58
+ return /^none$/i.test(firstWord.replace(NONE_PUNCT_STRIP_RE, ''));
59
+ }
60
+
61
+ /**
62
+ * Validate a PRD's `gate` field.
63
+ *
64
+ * @param {unknown} gate
65
+ * @returns {{ok:true, commands:string[], warnings:string[]}|{ok:false, error:string}}
66
+ * commands is `['none']` for the opt-out, else the trimmed entries verbatim
67
+ * (one command chain per entry) — the exact strings renderGateSection and
68
+ * createPrd's read-back check both work from.
69
+ */
70
+ function validateGate(gate) {
71
+ if (!Array.isArray(gate) || gate.length < GATE_MIN_ENTRIES || gate.length > GATE_MAX_ENTRIES) {
72
+ return {
73
+ ok: false,
74
+ error: `gate must have ${GATE_MIN_ENTRIES} to ${GATE_MAX_ENTRIES} entries (got ${Array.isArray(gate) ? gate.length : 0}).`,
75
+ };
76
+ }
77
+
78
+ const trimmed = gate.map((e) => (typeof e === 'string' ? e.trim() : e));
79
+
80
+ // Per-entry shape rules — run uniformly over every entry, including "none"
81
+ // (it trivially satisfies all of them; the none/mixed semantics are a
82
+ // separate check below). explainChain's result is kept per entry so the
83
+ // missing-timeout warning pass below never re-parses.
84
+ const explainedByEntry = [];
85
+ for (let i = 0; i < trimmed.length; i++) {
86
+ const entry = trimmed[i];
87
+ const idx = i + 1;
88
+ if (typeof entry !== 'string' || entry.length === 0) {
89
+ return { ok: false, error: `gate entry ${idx}: must be a non-empty string.` };
90
+ }
91
+ if (entry.length > GATE_ENTRY_MAX_CHARS) {
92
+ return { ok: false, error: entryError('gate', idx, entry, `must be at most ${GATE_ENTRY_MAX_CHARS} chars.`) };
93
+ }
94
+ if (/[\r\n]/.test(entry)) {
95
+ return { ok: false, error: entryError('gate', idx, entry, 'must not contain a newline.') };
96
+ }
97
+ if (entry.includes('```')) {
98
+ return { ok: false, error: entryError('gate', idx, entry, 'must not contain a triple-backtick run (breaks the fence).') };
99
+ }
100
+ if (entry.startsWith('#')) {
101
+ return { ok: false, error: entryError('gate', idx, entry, 'must not start with "#" — the fence parser drops lines starting with "#".') };
102
+ }
103
+ if (isNearMissNone(entry)) {
104
+ return {
105
+ ok: false,
106
+ error: entryError('gate', idx, entry, 'write exactly none, with nothing else, for a PRD with no runnable check.'),
107
+ };
108
+ }
109
+ const explained = explainChain(entry);
110
+ if (!explained.ok) {
111
+ return { ok: false, error: entryError('gate', idx, entry, explained.error) };
112
+ }
113
+ explainedByEntry.push(explained);
114
+ }
115
+
116
+ // "none" semantics: alone it's the opt-out; mixed with anything else it's
117
+ // an error (one PRD cannot be both "no gate" and "here is a gate").
118
+ const noneIdx = trimmed.findIndex((e) => isNoneGate(e));
119
+ if (noneIdx !== -1) {
120
+ if (trimmed.length > 1) {
121
+ return {
122
+ ok: false,
123
+ error: entryError(
124
+ 'gate', noneIdx + 1, trimmed[noneIdx],
125
+ '"none" must be the only entry when used — remove the other entries or remove "none".',
126
+ ),
127
+ };
128
+ }
129
+ return { ok: true, commands: ['none'], warnings: [NONE_GATE_WARNING] };
130
+ }
131
+
132
+ // Missing-timeout warning (never rejects) — a step without one can hang
133
+ // the run.
134
+ const warnings = [];
135
+ explainedByEntry.forEach((explained, i) => {
136
+ explained.steps.forEach((step, stepIdx) => {
137
+ if (!explained.timeoutGiven[stepIdx]) {
138
+ warnings.push(
139
+ `gate entry ${i + 1} step "${cut80(step.raw)}" does not start with "timeout <seconds>" — a step without a timeout can hang the run.`,
140
+ );
141
+ }
142
+ });
143
+ });
144
+
145
+ return { ok: true, commands: trimmed, warnings };
146
+ }
147
+
148
+ /**
149
+ * Validate a PRD's `files` field.
150
+ *
151
+ * @param {unknown} files
152
+ * @returns {{ok:true, files:string[]}|{ok:false, error:string}}
153
+ * files is deduped (exact match, order preserved) with every leading `./`
154
+ * stripped per entry.
155
+ */
156
+ function validateFiles(files) {
157
+ if (!Array.isArray(files) || files.length < FILES_MIN_ENTRIES || files.length > FILES_MAX_ENTRIES) {
158
+ return {
159
+ ok: false,
160
+ error: `files must have ${FILES_MIN_ENTRIES} to ${FILES_MAX_ENTRIES} entries (got ${Array.isArray(files) ? files.length : 0}).`,
161
+ };
162
+ }
163
+
164
+ const seen = new Set();
165
+ const out = [];
166
+ for (let i = 0; i < files.length; i++) {
167
+ const idx = i + 1;
168
+ const original = files[i];
169
+ if (typeof original !== 'string') {
170
+ return { ok: false, error: `files entry ${idx}: must be a string.` };
171
+ }
172
+ const entry = original.trim();
173
+ if (entry.length === 0) {
174
+ return { ok: false, error: `files entry ${idx}: must not be empty.` };
175
+ }
176
+ if (entry.length > FILES_ENTRY_MAX_CHARS) {
177
+ return { ok: false, error: entryError('files', idx, entry, `must be at most ${FILES_ENTRY_MAX_CHARS} chars.`) };
178
+ }
179
+ if (/[\r\n]/.test(entry)) {
180
+ return { ok: false, error: entryError('files', idx, entry, 'must not contain a newline.') };
181
+ }
182
+ if (FILES_FOREIGN_WHITESPACE_RE.test(entry)) {
183
+ return {
184
+ ok: false,
185
+ error: entryError('files', idx, entry, 'has a character that is not a plain space (for example a tab, a no-break space, or a line break). Retype it.'),
186
+ };
187
+ }
188
+ if (entry.includes('\\')) {
189
+ return { ok: false, error: entryError('files', idx, entry, 'use "/" between folders, not "\\".') };
190
+ }
191
+ if (entry.includes('`')) {
192
+ return { ok: false, error: entryError('files', idx, entry, 'must not contain a backtick.') };
193
+ }
194
+ if (entry.startsWith('-')) {
195
+ return { ok: false, error: entryError('files', idx, entry, 'must not start with "-".') };
196
+ }
197
+ if (entry.startsWith('/') || entry.startsWith('~') || /^[A-Za-z]:/.test(entry)) {
198
+ return { ok: false, error: entryError('files', idx, entry, 'must be repo-relative — absolute paths are not allowed.') };
199
+ }
200
+ if (entry.includes('*') || entry.includes('?')) {
201
+ return { ok: false, error: entryError('files', idx, entry, 'must not contain "*" or "?" (no globs).') };
202
+ }
203
+
204
+ let normalized = entry;
205
+ while (normalized.startsWith('./')) normalized = normalized.slice(2);
206
+ if (normalized.length === 0) {
207
+ return { ok: false, error: entryError('files', idx, entry, 'is empty after removing "./".') };
208
+ }
209
+ const segments = normalized.split('/');
210
+ if (segments.includes('..')) {
211
+ return { ok: false, error: entryError('files', idx, entry, 'must not contain a ".." path segment.') };
212
+ }
213
+ if (segments.includes('.')) {
214
+ return { ok: false, error: entryError('files', idx, entry, 'must not contain a "." path segment.') };
215
+ }
216
+ if (normalized.includes('//')) {
217
+ return { ok: false, error: entryError('files', idx, entry, 'must not contain an empty path segment ("//").') };
218
+ }
219
+
220
+ if (!seen.has(normalized)) {
221
+ seen.add(normalized);
222
+ out.push(normalized);
223
+ }
224
+ }
225
+
226
+ return { ok: true, files: out };
227
+ }
228
+
229
+ /**
230
+ * Render the `# Files` body section as an array of lines (joinable with
231
+ * '\n' alongside the rest of buildPrdBody's bodyLines). `files` must already
232
+ * be validated/normalized (validateFiles's output).
233
+ *
234
+ * @param {string[]} files
235
+ * @returns {string[]}
236
+ */
237
+ function renderFilesSection(files) {
238
+ return [
239
+ '# Files', '',
240
+ 'Change only these files. If the work needs another file, change it and say why in your report.',
241
+ '',
242
+ ...files.map((f) => `- ${f}`),
243
+ '',
244
+ ];
245
+ }
246
+
247
+ /**
248
+ * Render the `# Gate` body section as an array of lines. `commandsOrNone`
249
+ * must already be validated (validateGate's `commands` output) — either
250
+ * `['none']` or the ordered command-chain strings to fence verbatim.
251
+ *
252
+ * @param {string[]} commandsOrNone
253
+ * @returns {string[]}
254
+ */
255
+ function renderGateSection(commandsOrNone) {
256
+ const isNone = commandsOrNone.length === 1 && isNoneGate(commandsOrNone[0]);
257
+ if (isNone) {
258
+ return [
259
+ '# Gate', '',
260
+ 'This PRD has no runnable check. Check each acceptance criterion by reading the files.',
261
+ '',
262
+ '```gate',
263
+ 'none',
264
+ '```',
265
+ '',
266
+ ];
267
+ }
268
+ return [
269
+ '# Gate', '',
270
+ 'Run these commands last, in order. Each must exit 0.',
271
+ '',
272
+ '```gate',
273
+ ...commandsOrNone,
274
+ '```',
275
+ '',
276
+ ];
277
+ }
278
+
279
+ module.exports = {
280
+ validateGate,
281
+ validateFiles,
282
+ renderFilesSection,
283
+ renderGateSection,
284
+ };
@@ -384,6 +384,64 @@ function deriveProjectCwdFromPrdPath(filePath) {
384
384
  return null;
385
385
  }
386
386
 
387
+ // Incident (2026-09-30): an Epic can live in a non-git PARENT folder P
388
+ // (P/session-manager-operations/scheduler/epics/<epicId>/prds/*.md) while
389
+ // its PRDs' own frontmatter `cwd` points at a SUB-REPO, e.g. P/repo. P is
390
+ // never itself a tracked project cwd (activeProjectCwds/allProjectCwds only
391
+ // know about registered project roots), so no amount of
392
+ // resolvePrdsDirs()/listEpicPrdDirs() enumeration ever visits it — only
393
+ // walking UP from the job's own cwd does. Bounded to a few hops so a
394
+ // same-machine but unrelated ancestor folder that happens to also contain a
395
+ // `session-manager-operations/` tree can't be walked into indefinitely.
396
+ const MAX_EPIC_ANCESTOR_HOPS = 3;
397
+
398
+ /**
399
+ * epicId must be an exact, plain directory-name match — never a glob, never
400
+ * string-concatenated into a path. Mirrors resolveEpicPrdWriteDir's own
401
+ * guard above.
402
+ */
403
+ function isSafeEpicId(epicId) {
404
+ return typeof epicId === 'string' && epicId.length > 0
405
+ && !epicId.includes('/') && !epicId.includes('\\') && !epicId.includes('..');
406
+ }
407
+
408
+ /**
409
+ * Walk up to MAX_EPIC_ANCESTOR_HOPS parent directories ABOVE `cwd` (never
410
+ * including `cwd` itself — that's already covered by resolveEpicsRoot/
411
+ * listEpicPrdDirs) looking for an EXACT
412
+ * `<ancestor>/session-manager-operations/scheduler/epics/<epicId>/<leafSubpath...>`
413
+ * match. Built only from `path.join` segments (never string concatenation
414
+ * with the caller-supplied epicId/cwd), with a resolved-path containment
415
+ * check as defense in depth. Returns only dirs that actually exist on disk.
416
+ */
417
+ function ancestorEpicDirs(cwd, epicId, leafSubpath) {
418
+ if (!cwd || typeof cwd !== 'string' || !path.isAbsolute(cwd)) return [];
419
+ if (!isSafeEpicId(epicId)) return [];
420
+ const out = [];
421
+ let dir = path.dirname(cwd);
422
+ for (let hop = 0; hop < MAX_EPIC_ANCESTOR_HOPS; hop++) {
423
+ const candidate = path.join(dir, OPS_ROOT_DIR, ...EPICS_SUBPATH, epicId, ...leafSubpath);
424
+ const resolvedDir = path.resolve(dir);
425
+ if (path.resolve(candidate).startsWith(resolvedDir + path.sep) && fs.existsSync(candidate)) {
426
+ out.push(candidate);
427
+ }
428
+ const parent = path.dirname(dir);
429
+ if (parent === dir) break; // reached the filesystem root
430
+ dir = parent;
431
+ }
432
+ return out;
433
+ }
434
+
435
+ /** Ancestor-folder `epics/<epicId>/prds` dirs above `cwd` (see ancestorEpicDirs). */
436
+ function ancestorEpicPrdDirs(cwd, epicId) {
437
+ return ancestorEpicDirs(cwd, epicId, ['prds']);
438
+ }
439
+
440
+ /** Ancestor-folder `epics/<epicId>/prds-archived` dirs above `cwd` (see ancestorEpicDirs). */
441
+ function ancestorEpicArchivedPrdDirs(cwd, epicId) {
442
+ return ancestorEpicDirs(cwd, epicId, ['prds-archived']);
443
+ }
444
+
387
445
  module.exports = {
388
446
  resolvePrdWriteDir,
389
447
  resolvePrdsDirs,
@@ -394,6 +452,9 @@ module.exports = {
394
452
  listArchivedPrdDirs,
395
453
  deriveEpicIdFromPrdPath,
396
454
  deriveProjectCwdFromPrdPath,
455
+ ancestorEpicPrdDirs,
456
+ ancestorEpicArchivedPrdDirs,
457
+ isSafeEpicId,
397
458
  PRD_SUBPATH,
398
459
  EPICS_SUBPATH,
399
460
  };
@@ -20,6 +20,7 @@ const { splitFrontmatter, parsePrdFile, serializePrdFile } = require('./prdFront
20
20
  const { resolvePrdWriteDir, resolvePrdsDirs } = require('./prdLocations.cjs');
21
21
  const { projectQueuePath } = require('./queueStore.cjs');
22
22
  const { expandHome } = require('./expandHome.cjs');
23
+ const atomicFs = require('./atomicFs.cjs');
23
24
 
24
25
  /**
25
26
  * Move every `.md` file in legacyPrdsDir whose frontmatter `cwd` resolves to
@@ -262,9 +263,7 @@ async function legacyAdoptExistingPrds() {
262
263
  fm.createdVia = 'legacy-adopted';
263
264
  fm.issuedAt = issuedAt;
264
265
  const newRaw = serializePrdFile(fm, body);
265
- const tmp = `${filePath}.tmp-${process.pid}`;
266
- await fsp.writeFile(tmp, newRaw, 'utf8');
267
- await fsp.rename(tmp, filePath);
266
+ await atomicFs.writeTextAtomic(filePath, newRaw);
268
267
  stamped += 1;
269
268
  } catch (e) {
270
269
  if (e?.code === 'ENOENT') continue; // raced with a concurrent mover/archiver
@@ -5,7 +5,8 @@
5
5
  * jobs finishing in <=10 min, yet nothing at the authoring gate flags an oversized PRD before
6
6
  * it lands. `sizingWarnings` is the single rule set; prdCreate.cjs's createPrd() surfaces its
7
7
  * output in the create-prd response, and scheduler-mcp-server.cjs's scheduler_create_prd
8
- * handler prints it as a "Sizing warnings:" block. Warnings never block a write.
8
+ * handler prints it as part of a "Warnings:" block (merged with the gate/files warnings
9
+ * from prdGateFiles.cjs). Warnings never block a write.
9
10
  */
10
11
  'use strict';
11
12