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
@@ -126,9 +126,19 @@ function reportToTelemetry({ cwd, scope, level, tabId, epicId, tags, message })
126
126
  } catch { /* telemetry must never break the caller — this is the sole error funnel */ }
127
127
  }
128
128
 
129
+ // realpath of the deepest existing ancestor + the not-yet-existing tail, so a cwd that was
130
+ // never created still compares in the same spelling as a symlinked root (macOS /var -> /private/var).
129
131
  function realOrResolved(p) {
130
132
  const abs = path.resolve(p);
131
- try { return fs.realpathSync(abs); } catch { return abs; }
133
+ let head = abs;
134
+ const tail = [];
135
+ for (;;) {
136
+ try { return path.join(fs.realpathSync(head), ...tail.reverse()); } catch { /* walk up */ }
137
+ const up = path.dirname(head);
138
+ if (up === head) return abs;
139
+ tail.push(path.basename(head));
140
+ head = up;
141
+ }
132
142
  }
133
143
 
134
144
  function isInside(child, parent) {
@@ -48,18 +48,14 @@ const OWNERS = Object.freeze({
48
48
  'scheduler': 'scheduler',
49
49
  // Project Home owns the synthesized Brief (generate + hand-edit).
50
50
  'project-brief': 'project-home',
51
- // Project Home owns project-pages/home.html, written only via the
52
- // project_home_write MCP tool -> /admin/project-home/write route
53
- // (config.cjs's writeTextAtomic). See
54
- // session-manager-operations/project-pages/README.md.
51
+ // Project Home owns project-pages/home.html and project-pages/demo-video/
52
+ // index.html, written only via the project_home_write ->
53
+ // /admin/project-home/write and project_demo_video_write ->
54
+ // /admin/project-home/demo-video/write routes (config.cjs's
55
+ // writeTextAtomic). See session-manager-operations/project-pages/README.md.
55
56
  'project-pages': 'project-home',
56
57
  // Structured per-tab error log lines (JSONL), tagged for tracing/analysis.
57
58
  'logs': 'logs',
58
- // Host on Bilko.run tab's deterministic bundle prep (dist/index.html +
59
- // dist/manifest.json only — publish-state.json and anything the
60
- // bilko-host-publisher Epic authors beyond dist/ is agent-Write-tool
61
- // output, same unenforceable-by-construction class as project-pages/output.
62
- 'bilko-host': 'bilko-host',
63
59
  // Memory Clusters owns its own regenerable per-project cache (PRD 1389) —
64
60
  // memoryAggregate.cjs's clustering result, rebuilt on any explicit
65
61
  // refresh:true. See memory-clusters/README.md.
@@ -0,0 +1,32 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * pidAlive.cjs — single source of truth for "is this pid alive" liveness.
5
+ *
6
+ * Four independent copies of this exact check had drifted apart:
7
+ * reaperHelpers.cjs's claudePidAlive (bug: caught EPERM and returned false,
8
+ * never crediting a pid it couldn't signal but which demonstrably exists),
9
+ * watchdogHelpers.cjs's isPidAlive, reservationExpiry.cjs's defaultPidAlive,
10
+ * and instanceLock.cjs's pidAlive (these three already treated EPERM as
11
+ * alive, but each reimplemented the same three lines). This module is now
12
+ * the one place the rule lives; the others re-export thin aliases so every
13
+ * existing import keeps working.
14
+ *
15
+ * Rule: an invalid pid (not an integer, or <= 1 — pid 1 is init/launchd,
16
+ * never a real target) is never alive. `process.kill(pid, 0)` (no signal
17
+ * sent) succeeding means the pid exists and we can signal it → alive. EPERM
18
+ * (pid exists, owned by another user — we just can't signal it) also counts
19
+ * as alive: the process demonstrably exists. Anything else (ESRCH — no such
20
+ * process) → dead.
21
+ */
22
+ function pidAlive(pid) {
23
+ if (!Number.isInteger(pid) || pid <= 1) return false;
24
+ try {
25
+ process.kill(pid, 0);
26
+ return true;
27
+ } catch (e) {
28
+ return e?.code === 'EPERM';
29
+ }
30
+ }
31
+
32
+ module.exports = { pidAlive };
@@ -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' };
@@ -522,8 +620,17 @@ async function createPrd(input, remote) {
522
620
  note: enqueued
523
621
  ? 'PRD file written and adopted into the queue as a pending row'
524
622
  : 'PRD file written; the queue row is derived by the next scheduler reconcile pass, not created here',
525
- // Advisory only (PRD 1403) — never blocks the write. See prdSizing.cjs.
526
- warnings: sizingWarnings(input),
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)],
527
634
  };
528
635
  }
529
636
 
@@ -569,6 +676,8 @@ function registerAdminRoute(adminHttp, remote) {
569
676
  epicId: result.epicId ?? null,
570
677
  enqueued: result.enqueued,
571
678
  note: result.note,
679
+ gate: result.gate,
680
+ files: result.files,
572
681
  warnings: result.warnings,
573
682
  });
574
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