claude-code-session-manager 0.110.0 → 0.112.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 (283) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/dist/assets/{AgentLibrary-Cm6MrOnF.js → AgentLibrary-RwPvEgqm.js} +2 -2
  3. package/dist/assets/{Button-CGVncJ6a.js → Button-36j3PzYv.js} +1 -1
  4. package/dist/assets/{DataModel-C3hlMoa8.js → DataModel-DUhANo4l.js} +1 -1
  5. package/dist/assets/{History-CTyfGNKn.js → History-BeZzNjmv.js} +2 -2
  6. package/dist/assets/{Hooks-BXxCBibx.js → Hooks-iQ8845Hp.js} +3 -3
  7. package/dist/assets/{Library-aCs7zxAg.js → Library-DPE1h-1L.js} +6 -6
  8. package/dist/assets/{ListDetail-B9Hqk1_f.js → ListDetail-PStOuQAo.js} +1 -1
  9. package/dist/assets/{MarkdownEditor-Djfgp32k.js → MarkdownEditor-DPw-N2-m.js} +1 -1
  10. package/dist/assets/{McpServers-8HQC6QgR.js → McpServers-CUyRTNep.js} +2 -2
  11. package/dist/assets/{Memory-c4SVE3l2.js → Memory-bZ2xjBGy.js} +3 -3
  12. package/dist/assets/{Panel-BEQTwE4z.js → Panel-_VZ_uXNg.js} +1 -1
  13. package/dist/assets/{Permissions-BaYzgZtc.js → Permissions-PPRmfNbK.js} +3 -3
  14. package/dist/assets/{Plugins-DnXsVau7.js → Plugins-BFcvfvlx.js} +2 -2
  15. package/dist/assets/{ProvenanceBadge-CT2vWps1.js → ProvenanceBadge-B1WUknak.js} +1 -1
  16. package/dist/assets/RemoteMachines-D5V0lzgW.js +3 -0
  17. package/dist/assets/{SaveBar-DwcHt3iR.js → SaveBar-Dw64FVdM.js} +1 -1
  18. package/dist/assets/{Scheduler-D6Gcuo0M.js → Scheduler-C8s_pJ3I.js} +9 -9
  19. package/dist/assets/{ScopeSwitcher-CgvWDc9Y.js → ScopeSwitcher-D-nKDMAE.js} +1 -1
  20. package/dist/assets/{Settings-CJoogVAB.js → Settings-CQSrR4mO.js} +4 -4
  21. package/dist/assets/{SkillReferenceGraph-4fq01kxq.js → SkillReferenceGraph-BbZFMHim.js} +1 -1
  22. package/dist/assets/{Skills-BU4ZX4h4.js → Skills-Bzyrh6EQ.js} +1 -1
  23. package/dist/assets/SystemPrompt-DP6i3Dx0.js +1 -0
  24. package/dist/assets/TagLibrary-iip0C6Bl.js +1 -0
  25. package/dist/assets/{TiptapBody-DVyBsfsT.js → TiptapBody-Cr-hxX8L.js} +1 -1
  26. package/dist/assets/Toggle-7WpPzcZH.js +1 -0
  27. package/dist/assets/{ViewTabs-BLHjKUv-.js → ViewTabs-9lQCRwWJ.js} +1 -1
  28. package/dist/assets/{historyTotals-BXDd3EzP.js → historyTotals-BrFxw5J1.js} +1 -1
  29. package/dist/assets/{index-CuiU3I-8.js → index-CTBpf-26.js} +250 -249
  30. package/dist/assets/{settingsSchema-DD4bc5cT.js → settingsSchema-Dh5Kl7VO.js} +1 -1
  31. package/dist/assets/{useAgentsDirWatch-DMQ_XZtr.js → useAgentsDirWatch-CMpx0d04.js} +1 -1
  32. package/dist/assets/{useRemotePeers-C9NfteSD.js → useRemotePeers-CI2axd-L.js} +1 -1
  33. package/dist/index.html +1 -1
  34. package/package.json +3 -2
  35. package/plugins/CLAUDE.md +5 -8
  36. package/plugins/session-manager-dev/.claude-plugin/plugin.json +3 -6
  37. package/plugins/session-manager-dev/skills/builder/1-classify-and-bump/SKILL.md +2 -2
  38. package/plugins/session-manager-dev/skills/builder/SKILL.md +2 -3
  39. package/plugins/session-manager-dev/skills/develop/SKILL.md +1 -1
  40. package/plugins/session-manager-dev/skills/develop/standards.md +3 -3
  41. package/scripts/README.md +3 -3
  42. package/scripts/audit-ops-hygiene.cjs +4 -4
  43. package/scripts/hooks/guard-destructive-git.cjs +4 -4
  44. package/scripts/hooks/guard-inline-implementation.cjs +1 -1
  45. package/scripts/hooks/guard-prd-writes.cjs +2 -2
  46. package/scripts/hooks/lib/guard-destructive-git-policy.cjs +1 -1
  47. package/scripts/mint-epic.cjs +2 -2
  48. package/scripts/scheduler-mcp-server.cjs +4 -4
  49. package/src/main/agentLibrary.cjs +5 -5
  50. package/src/main/agentsIpc.cjs +76 -0
  51. package/src/main/build-info.json +4 -4
  52. package/src/main/chatRunner.cjs +12 -12
  53. package/src/main/config.cjs +2 -2
  54. package/src/main/crashDiagnostics.cjs +2 -2
  55. package/src/main/docEdit.cjs +5 -5
  56. package/src/main/files.cjs +2 -2
  57. package/src/main/git.cjs +1 -1
  58. package/src/main/health.cjs +20 -18
  59. package/src/main/heapSnapshot.cjs +1 -1
  60. package/src/main/historyAggregator.cjs +2 -2
  61. package/src/main/index.cjs +44 -132
  62. package/src/main/ipcSchemas.cjs +10 -10
  63. package/src/main/lib/CLAUDE.md +48 -0
  64. package/src/main/lib/activeSessions.cjs +1 -1
  65. package/src/main/lib/classifyPromptTicket.cjs +2 -2
  66. package/src/main/lib/{agentModelResolve.cjs → claude/agentModelResolve.cjs} +7 -7
  67. package/src/main/lib/{claudeBin.cjs → claude/claudeBin.cjs} +4 -4
  68. package/src/main/lib/{effectiveModelInfo.cjs → claude/effectiveModelInfo.cjs} +9 -9
  69. package/src/main/lib/{modelCatalog.cjs → claude/modelCatalog.cjs} +3 -3
  70. package/src/main/lib/{rateLimitDetect.cjs → claude/rateLimitDetect.cjs} +1 -1
  71. package/src/main/lib/{rateLimitWindow.cjs → claude/rateLimitWindow.cjs} +1 -1
  72. package/src/main/lib/{runClaudeP.cjs → claude/runClaudeP.cjs} +1 -1
  73. package/src/main/lib/credentials.cjs +3 -3
  74. package/src/main/lib/cwdClassify.cjs +1 -1
  75. package/src/main/lib/{activeIndexMerge.cjs → epic/activeIndexMerge.cjs} +3 -3
  76. package/src/main/lib/{activeIndexRebuild.cjs → epic/activeIndexRebuild.cjs} +1 -1
  77. package/src/main/lib/{delegationReadiness.cjs → epic/delegationReadiness.cjs} +21 -29
  78. package/src/main/lib/{epicContextDigest.cjs → epic/epicContextDigest.cjs} +1 -1
  79. package/src/main/lib/{epicDelegationStats.cjs → epic/epicDelegationStats.cjs} +2 -2
  80. package/src/main/lib/{epicMint.cjs → epic/epicMint.cjs} +11 -11
  81. package/src/main/lib/{epicSpawnPlan.cjs → epic/epicSpawnPlan.cjs} +3 -3
  82. package/src/main/lib/{epicStatusMirror.cjs → epic/epicStatusMirror.cjs} +2 -2
  83. package/src/main/lib/{epicTranscriptDiagnostic.cjs → epic/epicTranscriptDiagnostic.cjs} +2 -2
  84. package/src/main/lib/{epicTranscriptPath.cjs → epic/epicTranscriptPath.cjs} +2 -2
  85. package/src/main/lib/{epicValidationHook.cjs → epic/epicValidationHook.cjs} +3 -3
  86. package/src/main/lib/{epicWorktreeBoot.cjs → epic/epicWorktreeBoot.cjs} +1 -1
  87. package/src/main/lib/{epicWorktreeMerge.cjs → epic/epicWorktreeMerge.cjs} +3 -3
  88. package/src/main/lib/{epicWorktreeMint.cjs → epic/epicWorktreeMint.cjs} +3 -3
  89. package/src/main/lib/{epicWorktreeProjectConfig.cjs → epic/epicWorktreeProjectConfig.cjs} +4 -4
  90. package/src/main/lib/{promptSessionsCreateEpic.cjs → epic/promptSessionsCreateEpic.cjs} +4 -4
  91. package/src/main/lib/{crossProjectFeedback.cjs → feedback-remote/crossProjectFeedback.cjs} +7 -7
  92. package/src/main/lib/{customerFeedbackClient.cjs → feedback-remote/customerFeedbackClient.cjs} +3 -3
  93. package/src/main/lib/{customerFeedbackInbox.cjs → feedback-remote/customerFeedbackInbox.cjs} +2 -2
  94. package/src/main/lib/{customerFeedbackIpc.cjs → feedback-remote/customerFeedbackIpc.cjs} +1 -1
  95. package/src/main/lib/{localAdminHttp.cjs → feedback-remote/localAdminHttp.cjs} +2 -2
  96. package/src/main/lib/{macroAdminRoutes.cjs → feedback-remote/macroAdminRoutes.cjs} +1 -1
  97. package/src/main/lib/{projectHomeAdminRoutes.cjs → feedback-remote/projectHomeAdminRoutes.cjs} +4 -4
  98. package/src/main/lib/{relayClientAuth.cjs → feedback-remote/relayClientAuth.cjs} +2 -2
  99. package/src/main/lib/{remoteClientSessions.cjs → feedback-remote/remoteClientSessions.cjs} +1 -1
  100. package/src/main/lib/{gitWorktree.cjs → git/gitWorktree.cjs} +7 -7
  101. package/src/main/lib/historyRollup.cjs +1 -1
  102. package/src/main/lib/importReferences.cjs +1 -1
  103. package/src/main/lib/machineProfile.cjs +1 -1
  104. package/src/main/lib/openExternalApp.cjs +1 -1
  105. package/src/main/lib/opsOwnership.cjs +3 -3
  106. package/src/main/lib/{agentPersonaSchema.cjs → persona-macro/agentPersonaSchema.cjs} +2 -2
  107. package/src/main/lib/{builtinMacros.cjs → persona-macro/builtinMacros.cjs} +2 -2
  108. package/src/main/lib/{macroLibrary.cjs → persona-macro/macroLibrary.cjs} +5 -5
  109. package/src/main/lib/{personaMerge.cjs → persona-macro/personaMerge.cjs} +4 -4
  110. package/src/main/lib/{shippedPersonaSeeds.cjs → persona-macro/shippedPersonaSeeds.cjs} +11 -0
  111. package/src/main/lib/{prdAdminRoutes.cjs → prd/prdAdminRoutes.cjs} +7 -7
  112. package/src/main/lib/{prdAgentType.cjs → prd/prdAgentType.cjs} +3 -3
  113. package/src/main/lib/{prdCreate.cjs → prd/prdCreate.cjs} +12 -12
  114. package/src/main/lib/{prdDeclaredPaths.cjs → prd/prdDeclaredPaths.cjs} +1 -1
  115. package/src/main/lib/{prdGateFiles.cjs → prd/prdGateFiles.cjs} +1 -1
  116. package/src/main/lib/{prdLocations.cjs → prd/prdLocations.cjs} +2 -2
  117. package/src/main/lib/{prdMigration.cjs → prd/prdMigration.cjs} +3 -3
  118. package/src/main/lib/prereqs.cjs +2 -2
  119. package/src/main/lib/{instanceLock.cjs → process/instanceLock.cjs} +4 -11
  120. package/src/main/lib/{pidAlive.cjs → process/pidAlive.cjs} +1 -1
  121. package/src/main/lib/{procName.cjs → process/procName.cjs} +2 -2
  122. package/src/main/lib/{timeoutShim.cjs → process/timeoutShim.cjs} +1 -1
  123. package/src/main/lib/projectRootResolve.cjs +1 -1
  124. package/src/main/lib/promptSessionSchema.cjs +1 -1
  125. package/src/main/lib/{definitionOfDone.cjs → scheduler/definitionOfDone.cjs} +6 -12
  126. package/src/main/lib/{fixChainDepth.cjs → scheduler/fixChainDepth.cjs} +1 -1
  127. package/src/main/lib/{jobDirtFilter.cjs → scheduler/jobDirtFilter.cjs} +1 -1
  128. package/src/main/lib/{jobSupervisorRecord.cjs → scheduler/jobSupervisorRecord.cjs} +1 -1
  129. package/src/main/lib/{launchFailure.cjs → scheduler/launchFailure.cjs} +2 -2
  130. package/src/main/lib/{queueStore.cjs → scheduler/queueStore.cjs} +6 -6
  131. package/src/main/lib/{quietMachineLease.cjs → scheduler/quietMachineLease.cjs} +1 -1
  132. package/src/main/lib/{rcaReport.cjs → scheduler/rcaReport.cjs} +4 -4
  133. package/src/main/lib/{reaperHelpers.cjs → scheduler/reaperHelpers.cjs} +3 -3
  134. package/src/main/lib/{reservationExpiry.cjs → scheduler/reservationExpiry.cjs} +4 -11
  135. package/src/main/lib/{reviewNotice.cjs → scheduler/reviewNotice.cjs} +1 -1
  136. package/src/main/lib/{scheduleJobTransitions.cjs → scheduler/scheduleJobTransitions.cjs} +3 -3
  137. package/src/main/lib/{schedulerBatch.cjs → scheduler/schedulerBatch.cjs} +2 -2
  138. package/src/main/lib/{schedulerConfig.cjs → scheduler/schedulerConfig.cjs} +1 -1
  139. package/src/main/lib/{schedulerRuntimeState.cjs → scheduler/schedulerRuntimeState.cjs} +1 -1
  140. package/src/main/lib/{sessionSlots.cjs → scheduler/sessionSlots.cjs} +2 -2
  141. package/src/main/lib/{upgradeDrain.cjs → scheduler/upgradeDrain.cjs} +8 -11
  142. package/src/main/lib/{watchdogHelpers.cjs → scheduler/watchdogHelpers.cjs} +5 -19
  143. package/src/main/lib/{auditLog.cjs → telemetry/auditLog.cjs} +1 -1
  144. package/src/main/lib/{opsErrorLog.cjs → telemetry/opsErrorLog.cjs} +4 -4
  145. package/src/main/lib/{telemetryBacklog.cjs → telemetry/telemetryBacklog.cjs} +4 -4
  146. package/src/main/lib/{telemetryBoot.cjs → telemetry/telemetryBoot.cjs} +1 -1
  147. package/src/main/lib/{telemetryClient.cjs → telemetry/telemetryClient.cjs} +4 -4
  148. package/src/main/lib/{telemetrySettings.cjs → telemetry/telemetrySettings.cjs} +1 -1
  149. package/src/main/logs.cjs +1 -1
  150. package/src/main/mcpStatus.cjs +2 -2
  151. package/src/main/memoryAggregate.cjs +3 -3
  152. package/src/main/memoryTool.cjs +1 -1
  153. package/src/main/otel.cjs +2 -2
  154. package/src/main/pluginInstall.cjs +29 -3
  155. package/src/main/projectBrief.cjs +6 -6
  156. package/src/main/projectPages.cjs +1 -1
  157. package/src/main/promptSessionEvents.cjs +2 -2
  158. package/src/main/pty.cjs +7 -7
  159. package/src/main/queueOps.cjs +6 -6
  160. package/src/main/remoteClient.cjs +4 -4
  161. package/src/main/remoteClientIpc.cjs +1 -1
  162. package/src/main/runVerify.cjs +1 -1
  163. package/src/main/scheduler/CLAUDE.md +26 -0
  164. package/src/main/scheduler/budget.cjs +576 -0
  165. package/src/main/scheduler/gitTreeGuard.cjs +672 -0
  166. package/src/main/scheduler/prdParser.cjs +4 -4
  167. package/src/main/scheduler/prdPaths.cjs +487 -0
  168. package/src/main/scheduler/reviewLadder.cjs +1011 -0
  169. package/src/main/scheduler/starvation.cjs +390 -0
  170. package/src/main/scheduler.cjs +289 -3311
  171. package/src/main/seedAgentPersonas.cjs +5 -5
  172. package/src/main/seedDevPlugin.cjs +70 -3
  173. package/src/main/seedSchedulerMcp.cjs +2 -2
  174. package/src/main/supervisor.cjs +5 -5
  175. package/src/main/telemetryIpc.cjs +51 -0
  176. package/src/main/transcripts.cjs +3 -3
  177. package/src/main/updaterIpc.cjs +1 -1
  178. package/src/main/usage.cjs +1 -1
  179. package/src/main/watchers.cjs +1 -1
  180. package/src/main/webRemote.cjs +7 -7
  181. package/src/preload/api.d.ts +8 -8
  182. package/src/preload/index.cjs +3 -3
  183. package/src/seed/agents/demo-video-builder.md +62 -19
  184. package/dist/assets/RemoteMachines-DHJZfdJC.js +0 -3
  185. package/dist/assets/SystemPrompt-BCEyMSEq.js +0 -1
  186. package/dist/assets/TagLibrary-m3It8Oal.js +0 -1
  187. package/dist/assets/Toggle-In_AExPV.js +0 -1
  188. package/plugins/session-manager-dev/skills/blog-for-project-feature/0-select/SKILL.md +0 -39
  189. package/plugins/session-manager-dev/skills/blog-for-project-feature/1-research/SKILL.md +0 -37
  190. package/plugins/session-manager-dev/skills/blog-for-project-feature/2-storyboard/SKILL.md +0 -64
  191. package/plugins/session-manager-dev/skills/blog-for-project-feature/3-capture/SKILL.md +0 -52
  192. package/plugins/session-manager-dev/skills/blog-for-project-feature/4-draft-copy/SKILL.md +0 -37
  193. package/plugins/session-manager-dev/skills/blog-for-project-feature/5-illustrate/SKILL.md +0 -50
  194. package/plugins/session-manager-dev/skills/blog-for-project-feature/6-compose/SKILL.md +0 -51
  195. package/plugins/session-manager-dev/skills/blog-for-project-feature/7-review/SKILL.md +0 -54
  196. package/plugins/session-manager-dev/skills/blog-for-project-feature/SKILL.md +0 -144
  197. package/plugins/session-manager-dev/skills/blog-for-project-feature/feature.config.yaml +0 -90
  198. package/plugins/session-manager-dev/skills/discover-features/SKILL.md +0 -95
  199. package/plugins/session-manager-dev/skills/explain-to-me/SKILL.md +0 -205
  200. package/plugins/session-manager-dev/skills/explain-to-me/assets/style-reference.html +0 -163
  201. package/plugins/session-manager-dev/skills/find-opportunity/SKILL.md +0 -83
  202. package/plugins/session-manager-dev/skills/issue-address/0-select/0-fetch-pool/SKILL.md +0 -38
  203. package/plugins/session-manager-dev/skills/issue-address/0-select/1-reject-fixed/SKILL.md +0 -64
  204. package/plugins/session-manager-dev/skills/issue-address/0-select/2-reject-claimed/SKILL.md +0 -71
  205. package/plugins/session-manager-dev/skills/issue-address/0-select/3-rank-impact/SKILL.md +0 -39
  206. package/plugins/session-manager-dev/skills/issue-address/0-select/SKILL.md +0 -75
  207. package/plugins/session-manager-dev/skills/issue-address/1-confirm-open/SKILL.md +0 -39
  208. package/plugins/session-manager-dev/skills/issue-address/2-claim/SKILL.md +0 -57
  209. package/plugins/session-manager-dev/skills/issue-address/3-reproduce/SKILL.md +0 -40
  210. package/plugins/session-manager-dev/skills/issue-address/4-fix/SKILL.md +0 -33
  211. package/plugins/session-manager-dev/skills/issue-address/5-verify/SKILL.md +0 -40
  212. package/plugins/session-manager-dev/skills/issue-address/SKILL.md +0 -120
  213. package/plugins/session-manager-dev/skills/local-project-health/SKILL.md +0 -58
  214. package/plugins/session-manager-dev/skills/memory-sanitation/SKILL.md +0 -147
  215. package/plugins/session-manager-dev/skills/ops-sweep/SKILL.md +0 -137
  216. package/plugins/session-manager-dev/skills/pr-review-sweep/0-fetch/SKILL.md +0 -41
  217. package/plugins/session-manager-dev/skills/pr-review-sweep/1-classify/SKILL.md +0 -56
  218. package/plugins/session-manager-dev/skills/pr-review-sweep/2-check-fixed/SKILL.md +0 -49
  219. package/plugins/session-manager-dev/skills/pr-review-sweep/3-queue/SKILL.md +0 -57
  220. package/plugins/session-manager-dev/skills/pr-review-sweep/4-land-and-resolve/SKILL.md +0 -60
  221. package/plugins/session-manager-dev/skills/pr-review-sweep/SKILL.md +0 -124
  222. package/plugins/session-manager-dev/skills/pr-signal/SKILL.md +0 -67
  223. package/plugins/session-manager-dev/skills/project-status/SKILL.md +0 -243
  224. package/plugins/session-manager-dev/skills/requesting-code-review/SKILL.md +0 -98
  225. package/plugins/session-manager-dev/skills/requesting-code-review/code-reviewer.md +0 -146
  226. /package/src/main/lib/{agentEffortResolve.cjs → claude/agentEffortResolve.cjs} +0 -0
  227. /package/src/main/lib/{claudeCliCaps.cjs → claude/claudeCliCaps.cjs} +0 -0
  228. /package/src/main/lib/{usageCircuit.cjs → claude/usageCircuit.cjs} +0 -0
  229. /package/src/main/lib/{epicSpawnCwd.cjs → epic/epicSpawnCwd.cjs} +0 -0
  230. /package/src/main/lib/{customerFeedbackAdminRoutes.cjs → feedback-remote/customerFeedbackAdminRoutes.cjs} +0 -0
  231. /package/src/main/lib/{remoteE2eCrypto.cjs → feedback-remote/remoteE2eCrypto.cjs} +0 -0
  232. /package/src/main/lib/{branchSweep.cjs → git/branchSweep.cjs} +0 -0
  233. /package/src/main/lib/{gitExec.cjs → git/gitExec.cjs} +0 -0
  234. /package/src/main/lib/{jobWorktree.cjs → git/jobWorktree.cjs} +0 -0
  235. /package/src/main/lib/{jobWorktreeBootLive.cjs → git/jobWorktreeBootLive.cjs} +0 -0
  236. /package/src/main/lib/{jobWorktreeTerminalOrphanLive.cjs → git/jobWorktreeTerminalOrphanLive.cjs} +0 -0
  237. /package/src/main/lib/{personaImportHealth.cjs → persona-macro/personaImportHealth.cjs} +0 -0
  238. /package/src/main/lib/{depSlugResolve.cjs → prd/depSlugResolve.cjs} +0 -0
  239. /package/src/main/lib/{planValidator.cjs → prd/planValidator.cjs} +0 -0
  240. /package/src/main/lib/{prdAuthoringSeed.cjs → prd/prdAuthoringSeed.cjs} +0 -0
  241. /package/src/main/lib/{prdDisposition.cjs → prd/prdDisposition.cjs} +0 -0
  242. /package/src/main/lib/{prdFrontmatter.cjs → prd/prdFrontmatter.cjs} +0 -0
  243. /package/src/main/lib/{prdSizing.cjs → prd/prdSizing.cjs} +0 -0
  244. /package/src/main/lib/{workTypeLibrary.cjs → prd/workTypeLibrary.cjs} +0 -0
  245. /package/src/main/lib/{childWithLog.cjs → process/childWithLog.cjs} +0 -0
  246. /package/src/main/lib/{cleanEnv.cjs → process/cleanEnv.cjs} +0 -0
  247. /package/src/main/lib/{defaultShell.cjs → process/defaultShell.cjs} +0 -0
  248. /package/src/main/lib/{killTree.cjs → process/killTree.cjs} +0 -0
  249. /package/src/main/lib/{loginShellPath.cjs → process/loginShellPath.cjs} +0 -0
  250. /package/src/main/lib/{procIdentity.cjs → process/procIdentity.cjs} +0 -0
  251. /package/src/main/lib/{singleInstanceGuard.cjs → process/singleInstanceGuard.cjs} +0 -0
  252. /package/src/main/lib/{smProcNames.cjs → process/smProcNames.cjs} +0 -0
  253. /package/src/main/lib/{timeoutShimScript.cjs → process/timeoutShimScript.cjs} +0 -0
  254. /package/src/main/lib/{winSpawn.cjs → process/winSpawn.cjs} +0 -0
  255. /package/src/main/lib/{adoptedRunSupervisor.cjs → scheduler/adoptedRunSupervisor.cjs} +0 -0
  256. /package/src/main/lib/{dispatchLoop.cjs → scheduler/dispatchLoop.cjs} +0 -0
  257. /package/src/main/lib/{dodDrainHook.cjs → scheduler/dodDrainHook.cjs} +0 -0
  258. /package/src/main/lib/{fixPlanSlug.cjs → scheduler/fixPlanSlug.cjs} +0 -0
  259. /package/src/main/lib/{gateAuthority.cjs → scheduler/gateAuthority.cjs} +0 -0
  260. /package/src/main/lib/{landedSinceRun.cjs → scheduler/landedSinceRun.cjs} +0 -0
  261. /package/src/main/lib/{loadGate.cjs → scheduler/loadGate.cjs} +0 -0
  262. /package/src/main/lib/{needsReviewLedger.cjs → scheduler/needsReviewLedger.cjs} +0 -0
  263. /package/src/main/lib/{queueHealth.cjs → scheduler/queueHealth.cjs} +0 -0
  264. /package/src/main/lib/{queueHistory.cjs → scheduler/queueHistory.cjs} +0 -0
  265. /package/src/main/lib/{runLogRetention.cjs → scheduler/runLogRetention.cjs} +0 -0
  266. /package/src/main/lib/{scheduleJobSchema.cjs → scheduler/scheduleJobSchema.cjs} +0 -0
  267. /package/src/main/lib/{schedulerPaths.cjs → scheduler/schedulerPaths.cjs} +0 -0
  268. /package/src/main/lib/{terminalRunOutcome.cjs → scheduler/terminalRunOutcome.cjs} +0 -0
  269. /package/src/main/lib/{validationSentinels.cjs → scheduler/validationSentinels.cjs} +0 -0
  270. /package/src/main/lib/{crashTelemetry.cjs → telemetry/crashTelemetry.cjs} +0 -0
  271. /package/src/main/lib/{otlpTraceExporter.cjs → telemetry/otlpTraceExporter.cjs} +0 -0
  272. /package/src/main/lib/{telemetryConsent.cjs → telemetry/telemetryConsent.cjs} +0 -0
  273. /package/src/main/lib/{telemetryCounters.cjs → telemetry/telemetryCounters.cjs} +0 -0
  274. /package/src/main/lib/{atomicFs.cjs → util/atomicFs.cjs} +0 -0
  275. /package/src/main/lib/{encodeCwd.cjs → util/encodeCwd.cjs} +0 -0
  276. /package/src/main/lib/{expandHome.cjs → util/expandHome.cjs} +0 -0
  277. /package/src/main/lib/{extractJson.cjs → util/extractJson.cjs} +0 -0
  278. /package/src/main/lib/{fileTail.cjs → util/fileTail.cjs} +0 -0
  279. /package/src/main/lib/{headTailBuffer.cjs → util/headTailBuffer.cjs} +0 -0
  280. /package/src/main/lib/{kebabCase.cjs → util/kebabCase.cjs} +0 -0
  281. /package/src/main/lib/{loopDelay.cjs → util/loopDelay.cjs} +0 -0
  282. /package/src/main/lib/{lruCache.cjs → util/lruCache.cjs} +0 -0
  283. /package/src/main/lib/{withTimeout.cjs → util/withTimeout.cjs} +0 -0
@@ -0,0 +1,576 @@
1
+ /**
2
+ * scheduler/budget.cjs — pure stall / stranded-row / job-overrun / wall-clock-budget
3
+ * selectors, failure-streak/backoff predicates and the dependsOn history-satisfaction
4
+ * lookup. Extracted verbatim from scheduler.cjs; the watchdogs and queue
5
+ * mutations stay there and call in.
6
+ */
7
+ 'use strict';
8
+
9
+ const fsp = require('node:fs/promises');
10
+ const path = require('node:path');
11
+ const { claudePidAlive } = require('../lib/scheduler/reaperHelpers.cjs');
12
+ const queueHistory = require('../lib/scheduler/queueHistory.cjs');
13
+ const { listArchivedPrdDirs } = require('../lib/prd/prdLocations.cjs');
14
+ const { LEGAL_TRANSITIONS } = require('../lib/scheduler/scheduleJobTransitions.cjs');
15
+ const { DEFAULT_PROJECT_CWD, DEP_HISTORY_FAIL_OPEN } = require('../lib/scheduler/schedulerBatch.cjs');
16
+ const {
17
+ JOB_OVERRUN_FACTOR: JOB_OVERRUN_FACTOR_DEFAULT,
18
+ JOB_OVERRUN_FLOOR_MS: JOB_OVERRUN_FLOOR_MS_DEFAULT,
19
+ JOB_BUDGET_FACTOR: JOB_BUDGET_FACTOR_DEFAULT,
20
+ JOB_BUDGET_FLOOR_MS: JOB_BUDGET_FLOOR_MS_DEFAULT,
21
+ JOB_BUDGET_CEILING_MS: JOB_BUDGET_CEILING_MS_DEFAULT,
22
+ } = require('../lib/scheduler/schedulerConfig.cjs');
23
+
24
+ // Shared by every SM_*-env-overridable numeric constant below (bare factors
25
+ // use unitMs=1; minute-denominated knobs use unitMs=60_000) — one parse rule
26
+ // instead of one hand-copied ternary per constant.
27
+ function numEnvOverride(envVar, unitMs, fallback) {
28
+ const raw = process.env[envVar];
29
+ return raw ? Number(raw) * unitMs : fallback;
30
+ }
31
+ const JOB_OVERRUN_FACTOR = numEnvOverride('SM_JOB_OVERRUN_FACTOR', 1, JOB_OVERRUN_FACTOR_DEFAULT);
32
+ const JOB_OVERRUN_FLOOR_MS = numEnvOverride('SM_JOB_OVERRUN_FLOOR_MINUTES', 60_000, JOB_OVERRUN_FLOOR_MS_DEFAULT);
33
+ // Same three numbers as JOB_OVERRUN_FACTOR/JOB_OVERRUN_FLOOR_MS today (3x,
34
+ // 45min) is coincidental, not structural — this triad ACTS (kills) where
35
+ // JOB_OVERRUN_* only ever escalates (see JOB_OVERRUN_FACTOR's own header);
36
+ // tune them independently, don't re-couple on a future pass just because the
37
+ // defaults happen to match right now.
38
+ const JOB_BUDGET_FACTOR = numEnvOverride('SM_JOB_BUDGET_FACTOR', 1, JOB_BUDGET_FACTOR_DEFAULT);
39
+ const JOB_BUDGET_FLOOR_MS = numEnvOverride('SM_JOB_BUDGET_FLOOR_MINUTES', 60_000, JOB_BUDGET_FLOOR_MS_DEFAULT);
40
+ const JOB_BUDGET_CEILING_MS = numEnvOverride('SM_JOB_BUDGET_CEILING_MINUTES', 60_000, JOB_BUDGET_CEILING_MS_DEFAULT);
41
+
42
+ /**
43
+ * computeStallSummary(state) → { stalled, total, running, pending, byProject }
44
+ *
45
+ * Pure, no IO. `state` is a merged queue-store read ({ jobs, invalidJobs,
46
+ * paused }). The engine (reconcile/reaper/auto-fix/reverify) already
47
+ * operates machine-wide via queueStore's stateCwds() — this function is
48
+ * MONITORING, and monitoring must not collapse per-project reality into one
49
+ * boolean. `stalled` (top-level) is the pre-existing machine-wide roll-up:
50
+ * the queue holds work — valid rows OR rows quarantined for an invalid
51
+ * status — but nothing anywhere is running or pending and the scheduler
52
+ * isn't paused. The 2026-08-07 incident sat exactly in this state for 4+
53
+ * hours: 2 jobs, 0 running, 0 pending, and the only visible symptom was a
54
+ * heartbeat `counts` object that had silently minted a `queued` bucket
55
+ * instead of reporting anything actionable.
56
+ *
57
+ * `byProject[cwd].stalled` is the PER-PROJECT verdict added for the
58
+ * "burrow went dark while other projects were busy" gap: a project can hold
59
+ * jobs (including ones parked `quarantined`) with 0 running and 0 pending
60
+ * while the machine-wide `stalled` above reads false because a different
61
+ * project has running/pending work. Each project's own status counts
62
+ * (`byProject[cwd][status]`) already summed to a total before this — the
63
+ * fix is only the boolean, not the counting.
64
+ */
65
+ function computeStallSummary(state) {
66
+ const jobs = Array.isArray(state?.jobs) ? state.jobs : [];
67
+ const invalidJobs = Array.isArray(state?.invalidJobs) ? state.invalidJobs : [];
68
+ let running = 0;
69
+ let pending = 0;
70
+ const byProject = {};
71
+ for (const j of jobs) {
72
+ if (j.status === 'running') running += 1;
73
+ if (j.status === 'pending') pending += 1;
74
+ const key = j.cwd || '(unknown)';
75
+ byProject[key] = byProject[key] || {};
76
+ byProject[key][j.status] = (byProject[key][j.status] || 0) + 1;
77
+ }
78
+ for (const inv of invalidJobs) {
79
+ const key = inv.row?.cwd || '(unknown)';
80
+ byProject[key] = byProject[key] || {};
81
+ byProject[key].invalid = (byProject[key].invalid || 0) + 1;
82
+ }
83
+ const total = jobs.length + invalidJobs.length;
84
+ // A drained queue (every row completed/skipped) is idle, not stalled — a
85
+ // stall needs at least one parked problem row (failed/needs_review/
86
+ // quarantined/invalid) that is waiting on someone.
87
+ const isProblemStatus = (st) => st !== 'completed' && st !== 'skipped';
88
+ const stalled = total > 0 && running === 0 && pending === 0 && !state?.paused
89
+ && (invalidJobs.length > 0 || jobs.some((j) => isProblemStatus(j.status)));
90
+ for (const key of Object.keys(byProject)) {
91
+ const counts = byProject[key];
92
+ const projRunning = counts.running || 0;
93
+ const projPending = counts.pending || 0;
94
+ const projTotal = Object.keys(counts)
95
+ .filter((k) => k !== 'stalled')
96
+ .reduce((sum, k) => sum + counts[k], 0);
97
+ const projProblem = Object.keys(counts)
98
+ .some((k) => k !== 'stalled' && k !== 'completed' && k !== 'skipped' && counts[k] > 0);
99
+ counts.stalled = projTotal > 0 && projRunning === 0 && projPending === 0 && !state?.paused && projProblem;
100
+ }
101
+ return { stalled, total, running, pending, byProject };
102
+ }
103
+
104
+ /**
105
+ * findStaleQuarantinedJobs(jobs, now, thresholdMs) → [{ slug, cwd, ageMs }]
106
+ *
107
+ * Pure, no IO. A 'quarantined' row (no createdVia provenance) can otherwise
108
+ * sit forever with nothing looking at it — quarantine used to clear only via
109
+ * a human adopting or archiving it; autoResolveQuarantine below now gives it
110
+ * a bounded automatic exit too. This function stays the escalation/warn half
111
+ * of that gate: any quarantined row whose recorded quarantine timestamp
112
+ * (statusHistory's `to === 'quarantined'` entry — stamped at creation, or
113
+ * backfilled from the PRD file's mtime by reconcile() for rows quarantined
114
+ * before that stamp existed) is older than `thresholdMs` is reported so the
115
+ * caller can warn-log and surface it distinctly. A row with no recoverable
116
+ * timestamp is skipped rather than guessed at.
117
+ */
118
+ function findStaleQuarantinedJobs(jobs, now, thresholdMs) {
119
+ const stale = [];
120
+ for (const j of jobs ?? []) {
121
+ if (j.status !== 'quarantined') continue;
122
+ const entry = (j.statusHistory || []).find((h) => h.to === 'quarantined');
123
+ if (!entry) continue;
124
+ const since = Date.parse(entry.at);
125
+ if (Number.isNaN(since)) continue;
126
+ const ageMs = now - since;
127
+ if (ageMs >= thresholdMs) stale.push({ slug: j.slug, cwd: j.cwd ?? null, ageMs });
128
+ }
129
+ return stale;
130
+ }
131
+
132
+ /**
133
+ * findOverrunningJobs(jobs, now, { factor, floorMs }) → [{ slug, cwd, estimateMinutes, ranMs, ratio }]
134
+ *
135
+ * Pure, no IO. A RUNNING job whose elapsed time exceeds
136
+ * `max(estimateMinutes * factor, floorMs)` is overrunning its own PRD's
137
+ * declared estimate.
138
+ *
139
+ * This closes the gap between the two existing kill paths, which a
140
+ * long-running-but-chatty job slips straight through:
141
+ * - MAX_JOB_DURATION_MS (4h) is a deadman — a 20-minute PRD at 3h is 9x
142
+ * over estimate and still an hour away from it.
143
+ * - IDLE_OUTPUT_KILL_MS (20m) only fires when the log mtime STALLS; an
144
+ * agent stuck in a productive-looking loop keeps writing and never trips it.
145
+ * `estimateMinutes` was parsed, stored and displayed but never once compared
146
+ * against actual runtime, so the only thing standing between a runaway job
147
+ * and the 4h ceiling was a human noticing. (2026-08-08: a PRD ran 3h+ and was
148
+ * caught only because the operator opened a second session to look.)
149
+ *
150
+ * Escalation only — see JOB_OVERRUN_FACTOR's note on why this must not kill.
151
+ * A job with no usable estimate is skipped rather than guessed at.
152
+ */
153
+ function findOverrunningJobs(jobs, now, { factor, floorMs } = {}) {
154
+ const f = typeof factor === 'number' && factor > 0 ? factor : JOB_OVERRUN_FACTOR;
155
+ const floor = typeof floorMs === 'number' && floorMs >= 0 ? floorMs : JOB_OVERRUN_FLOOR_MS;
156
+ const out = [];
157
+ for (const j of jobs ?? []) {
158
+ if (j.status !== 'running') continue;
159
+ const est = Number(j.estimateMinutes);
160
+ if (!Number.isFinite(est) || est <= 0) continue; // no estimate to overrun
161
+ const startedAt = Date.parse(j.startedAt ?? '');
162
+ if (Number.isNaN(startedAt)) continue;
163
+ const ranMs = now - startedAt;
164
+ if (ranMs <= 0) continue;
165
+ const thresholdMs = Math.max(est * 60_000 * f, floor);
166
+ if (ranMs < thresholdMs) continue;
167
+ out.push({
168
+ slug: j.slug,
169
+ cwd: j.cwd ?? null,
170
+ estimateMinutes: est,
171
+ ranMs,
172
+ ratio: ranMs / (est * 60_000),
173
+ });
174
+ }
175
+ return out;
176
+ }
177
+
178
+ /**
179
+ * computeJobBudgetMs(estimateMinutes, { factor, floorMs, ceilingMs }) → number
180
+ *
181
+ * Pure. `budgetMs = clamp(estimateMinutes * factor, floorMs, ceilingMs)` — see
182
+ * JOB_BUDGET_FACTOR's header comment (schedulerConfig.cjs) for the measured
183
+ * p50/p90/max this is calibrated against. A missing/zero/non-finite estimate
184
+ * is treated as 0, which the floor clamp then dominates — "jobs with a
185
+ * missing estimate get the floor" falls straight out of the clamp, no
186
+ * special-casing needed.
187
+ */
188
+ function computeJobBudgetMs(estimateMinutes, { factor, floorMs, ceilingMs } = {}) {
189
+ const f = typeof factor === 'number' && factor > 0 ? factor : JOB_BUDGET_FACTOR;
190
+ const floor = typeof floorMs === 'number' && floorMs >= 0 ? floorMs : JOB_BUDGET_FLOOR_MS;
191
+ const ceiling = typeof ceilingMs === 'number' && ceilingMs > 0 ? ceilingMs : JOB_BUDGET_CEILING_MS;
192
+ const est = Number(estimateMinutes);
193
+ const minutes = Number.isFinite(est) && est > 0 ? est : 0;
194
+ return Math.min(Math.max(minutes * f * 60_000, floor), ceiling);
195
+ }
196
+
197
+ /**
198
+ * classifyBudgetKill(res, landedCommitEvidence) → { status, reason, landedCommit } | null
199
+ *
200
+ * Pure. `res` is executeJob's resolved outcome — only fires when
201
+ * `res.killedByWatchdog === 'budget'` (stamped by the budget watchdog inside
202
+ * executeJob, never inferred from exit code/duration alone, so it can never
203
+ * collide with an ordinary idle-tail/deadman/external kill). ALWAYS routes to
204
+ * needs_review — never 'failed' (spawnJob's ordinary non-zero-exit default)
205
+ * and never silently 'completed' (executeJob's onExit excludes
206
+ * killedByWatchdog === 'budget' from the result=success → exit 0 mapping
207
+ * idle-tail/deadman get) — so a budget kill is a visible, actionable park,
208
+ * never a retry (classifyFailureOutcome/selectAutoFixTargets only ever see
209
+ * 'failed'/ordinary needs_review rows, not this one — see
210
+ * selectAutoFixTargets' own budget_exceeded exclusion) and never a discard:
211
+ * `landedCommitEvidence`, when the caller resolved one via the SAME
212
+ * commit-guard evidence check the plain sigterm/exit paths already use, is
213
+ * threaded onto the row as `landedCommit` so a job that HAD already
214
+ * committed before overrunning is still adjudicated on its git evidence.
215
+ */
216
+ function classifyBudgetKill(res, landedCommitEvidence) {
217
+ if (!res || res.killedByWatchdog !== 'budget') return null;
218
+ return {
219
+ status: 'needs_review',
220
+ reason: res.budgetKillReason || `wall-clock budget exceeded (exit ${res.exitCode})`,
221
+ landedCommit: landedCommitEvidence || null,
222
+ };
223
+ }
224
+
225
+ /**
226
+ * isJobBudgetExempt(job) → boolean
227
+ *
228
+ * Pure. `quietMachine: true` PRDs (their whole point is running alone,
229
+ * un-contended, for a timing-sensitive measurement) and any PRD with an
230
+ * explicit `budgetExempt: true` opt-out have no wall-clock kill ceiling.
231
+ */
232
+ function isJobBudgetExempt(job) {
233
+ return job?.quietMachine === true || job?.budgetExempt === true;
234
+ }
235
+
236
+ /** Pure predicate the budget watchdog's shouldFire calls — single source of
237
+ * truth for "has this job run past its own budget" so it's unit-testable
238
+ * without spinning up real timers. */
239
+ function shouldKillForBudget(elapsedMs, budgetMs) {
240
+ return elapsedMs >= budgetMs;
241
+ }
242
+
243
+ /**
244
+ * resolveBudgetKillOutcome({ killedByWatchdog, killedBySignal, durationMs, jobBudgetMs, estimateMinutes })
245
+ * → { killedByWatchdog: 'budget'|null, budgetKillReason: string|null }
246
+ *
247
+ * Pure. `ctx.killedByWatchdog` is stamped by the budget watchdog's action()
248
+ * the instant its periodic shouldFire() observes elapsedMs >= jobBudgetMs —
249
+ * but that setInterval tick and the child's real 'exit' event both run on
250
+ * the SAME single-threaded event loop, so it's possible to observe the
251
+ * budget threshold crossed and call ctx.killTree() in the same window the
252
+ * agent happens to exit cleanly (exit 0) or fails on its own for an
253
+ * unrelated reason — ctx.killTree() against an already-exited pid is a
254
+ * silent no-op (ESRCH, caught), but the flag would still read 'budget'
255
+ * unless gated here. Only trusted when the exit SHAPE actually looks like a
256
+ * signal kill (killedBySignal — mirrors the exact same check onExit already
257
+ * uses for its own mappedToSuccess exclusion), so a clean exit=0 or an
258
+ * ordinary unrelated non-zero failure racing the watchdog's tick is never
259
+ * misclassified as a budget kill downstream.
260
+ */
261
+ function resolveBudgetKillOutcome({ killedByWatchdog, killedBySignal, durationMs, jobBudgetMs, estimateMinutes }) {
262
+ if (killedByWatchdog !== 'budget' || !killedBySignal) {
263
+ return { killedByWatchdog: killedByWatchdog === 'budget' ? null : (killedByWatchdog ?? null), budgetKillReason: null };
264
+ }
265
+ return {
266
+ killedByWatchdog: 'budget',
267
+ budgetKillReason: `wall-clock budget exceeded: ran ${Math.round(durationMs / 60_000)}m against a ${Math.round(jobBudgetMs / 60_000)}m budget (estimateMinutes=${estimateMinutes ?? 0})`,
268
+ };
269
+ }
270
+
271
+ /**
272
+ * findStrandedInvestigations(jobs, now, maxMs, isAlive = claudePidAlive)
273
+ * → [{ slug, cwd, ageMs, restoreStatus }]
274
+ *
275
+ * Pure (besides the warn-log side effect on the two unprovable-age cases
276
+ * below), no other IO. spawnInvestigation's own restore of a job's
277
+ * pre-investigation status runs entirely inside the process that spawned the
278
+ * probe (its withChildAndLog onExit handler, or the synchronous-throw catch
279
+ * path) — so a job left 'investigating' when the app itself dies or restarts
280
+ * has NOTHING left to restore it. The comment at spawnInvestigation's onExit
281
+ * asserts "'investigating' must never be the job's resting state"; this is
282
+ * the sweep that makes that true across a restart, not just within one.
283
+ *
284
+ * A row qualifies only when ALL of:
285
+ * - status is 'investigating'
286
+ * - its most recent transition INTO 'investigating' (statusHistory's last
287
+ * `to === 'investigating'` entry — a job can be investigated more than
288
+ * once across its life, e.g. a retried auto-fix) is older than `maxMs`
289
+ * - it has no live probe process behind it (checked via runtime.pid, set by
290
+ * spawnInvestigation once its child spawns and cleared on every restore
291
+ * path, the same shape reapDeadRunningJobs already uses for 'running' rows)
292
+ *
293
+ * `restoreStatus` is that transition entry's `from` — the exact value
294
+ * spawnInvestigation itself would have restored to (`failedJob.status ||
295
+ * 'failed'`), which for a row that already finished and recorded
296
+ * finishedAt+exitCode (the burrow-834 shape) is whatever terminal status was
297
+ * computed for that outcome BEFORE the probe was spawned — this sweep never
298
+ * re-derives it from exitCode, only replays the already-recorded decision.
299
+ *
300
+ * A row with no recoverable transition timestamp cannot have its age proven,
301
+ * so it is warn-logged and left alone rather than guessed at — same posture
302
+ * as findStaleQuarantinedJobs/findOverrunningJobs.
303
+ *
304
+ * `restoreStatus` is validated against LEGAL_TRANSITIONS['investigating']
305
+ * before being returned — `statusHistory`'s `from` should only ever be
306
+ * 'failed' or 'needs_review' (the only two states LEGAL_TRANSITIONS allows
307
+ * into 'investigating'), but a corrupted/unexpected value must not be handed
308
+ * straight to transitionJob: an illegal target is refused outright (row stays
309
+ * stuck at 'investigating', re-detected as stranded every sweep with no path
310
+ * out), so an out-of-set `from` falls back to 'failed' here instead.
311
+ */
312
+ const INVESTIGATING_RESTORE_TARGETS = new Set(LEGAL_TRANSITIONS.investigating);
313
+ function findStrandedInvestigations(jobs, now, maxMs, isAlive = claudePidAlive) {
314
+ const out = [];
315
+ for (const j of jobs ?? []) {
316
+ if (j.status !== 'investigating') continue;
317
+ const entries = (j.statusHistory || []).filter((h) => h.to === 'investigating');
318
+ const entry = entries[entries.length - 1];
319
+ if (!entry) {
320
+ console.warn(`[scheduler] findStrandedInvestigations: ${j.slug} is 'investigating' with no statusHistory entry recording the transition — cannot prove age, leaving alone`);
321
+ continue;
322
+ }
323
+ const since = Date.parse(entry.at ?? '');
324
+ if (Number.isNaN(since)) {
325
+ console.warn(`[scheduler] findStrandedInvestigations: ${j.slug} has an unparseable investigating-transition timestamp (${entry.at}) — cannot prove age, leaving alone`);
326
+ continue;
327
+ }
328
+ const ageMs = now - since;
329
+ if (ageMs < maxMs) continue; // a live probe must not be yanked out from under itself
330
+ const pid = j.runtime?.pid;
331
+ if (pid && isAlive(pid)) continue; // probe genuinely still running — not stranded
332
+ const restoreStatus = INVESTIGATING_RESTORE_TARGETS.has(entry.from) ? entry.from : 'failed';
333
+ out.push({ slug: j.slug, cwd: j.cwd ?? null, ageMs, restoreStatus });
334
+ }
335
+ return out;
336
+ }
337
+
338
+ /**
339
+ * computeDepHistorySatisfaction(state) → Map<cwd, Set<string>|symbol>
340
+ *
341
+ * PRD 1122's once-per-tick dependsOn history/archive lookup: for every
342
+ * distinct project cwd with jobs this tick, builds the set of dep slugs that
343
+ * have no live queue row but are nonetheless known-satisfied — a completed
344
+ * record in that project's own `state/history.jsonl` shard
345
+ * (queueHistory.completedSlugsForCwd, scoped per-project so a same-named PRD
346
+ * in an unrelated project can never satisfy a dep here), or a `.md` file
347
+ * under any of that project's `prds-archived/` dirs (listArchivedPrdDirs —
348
+ * covers both the retired flat layout and every Epic's own sibling archive).
349
+ * findBlockingDep (schedulerBatch.cjs) treats a dep slug as blocking
350
+ * whenever it has no live row AND is absent from this set, so a typo or a
351
+ * double-prefixed slug (the exact 2026-09-06 starry-night-ships incident)
352
+ * HOLDS its dependent instead of silently dispatching it.
353
+ *
354
+ * Fails OPEN per project, never queue-wide: a history-shard or archive-scan
355
+ * read error for one cwd degrades that cwd's value to
356
+ * `DEP_HISTORY_FAIL_OPEN` (findBlockingDep then treats every rowless dep in
357
+ * that project as satisfied, exactly today's pre-1122 behaviour) with a
358
+ * logged warning — it never throws out of this function and never blocks
359
+ * every OTHER project's dispatch for one project's bad fs state.
360
+ *
361
+ * Computed ONCE here, before pickNextBatch runs, and threaded down as pure
362
+ * data (quietOpts.satisfiedSlugsByCwd) — schedulerBatch.cjs itself does no
363
+ * I/O, so this is the only fs read this gate costs per tick, not one per job
364
+ * per dep.
365
+ */
366
+ async function computeDepHistorySatisfaction(state) {
367
+ const byCwd = new Map();
368
+ const cwds = new Set((state?.jobs || []).map((j) => j.cwd || DEFAULT_PROJECT_CWD));
369
+ for (const cwd of cwds) {
370
+ const satisfied = new Set();
371
+ try {
372
+ for (const slug of await queueHistory.completedSlugsForCwd(cwd)) satisfied.add(slug);
373
+ for (const dir of listArchivedPrdDirs(cwd)) {
374
+ let entries;
375
+ try { entries = await fsp.readdir(dir, { withFileTypes: true }); } catch { continue; }
376
+ for (const ent of entries) {
377
+ if (ent.isFile() && ent.name.endsWith('.md')) { satisfied.add(ent.name.slice(0, -3)); continue; }
378
+ // Option (b) of PRD 1286: a manual archive (queueOps.archiveOne, the
379
+ // schedule:archive-prd route and scheduler_archive_prd MCP tool) files the PRD
380
+ // under prds-archived/<ISO-ts>/<slug>.md — one level DEEPER than the auto-archive
381
+ // layout. Reading only the top level made every manually-archived slug invisible
382
+ // here, so once its row aged out its dependents held forever as 'unresolved'.
383
+ // A dep whose PRD file was archived is satisfied; a dep with NO file, row or
384
+ // history record (never ran, or a typo) still holds — nothing is dropped.
385
+ if (!ent.isDirectory()) continue;
386
+ let inner;
387
+ try { inner = await fsp.readdir(path.join(dir, ent.name)); } catch { continue; }
388
+ for (const name of inner) {
389
+ if (name.endsWith('.md')) satisfied.add(name.slice(0, -3));
390
+ }
391
+ }
392
+ }
393
+ } catch (e) {
394
+ console.warn(`[scheduler] depHistorySatisfaction: history/archive lookup failed for ${cwd} (${e?.message}) — falling back to fail-open dep resolution for this project this tick`);
395
+ byCwd.set(cwd, DEP_HISTORY_FAIL_OPEN);
396
+ continue;
397
+ }
398
+ byCwd.set(cwd, satisfied);
399
+ }
400
+ return byCwd;
401
+ }
402
+
403
+ // Bounded automatic exit for a quarantined row (this PRD): up to
404
+ // QUARANTINE_RESOLVE_CAP auto-resolve attempts, each gated on having sat
405
+ // `quarantined` for QUARANTINE_ESCALATE_MS, before autoResolveQuarantine
406
+ // below settles the row to 'skipped' rather than leaving it as a dead end
407
+ // only a human `scheduler_reset_job`/adopt action could ever clear. A single
408
+ // attempt is enough in practice — the outcome is terminal — but the counter
409
+ // still guards against two overlapping ticks both trying to resolve the
410
+ // same row.
411
+ const QUARANTINE_RESOLVE_CAP = 1;
412
+
413
+ /**
414
+ * Kill-switch gate for the quarantine auto-resolve pass below
415
+ * (SM_QUARANTINE_AUTORESOLVE_DISABLE=1), same shape as
416
+ * failedAutoResetDisabled/needsReviewAutoResolveDisabled.
417
+ */
418
+ function quarantineAutoResolveDisabled() {
419
+ return process.env.SM_QUARANTINE_AUTORESOLVE_DISABLE === '1';
420
+ }
421
+
422
+ /**
423
+ * selectQuarantineAutoResolveTargets(jobs, now, thresholdMs) →
424
+ * [{ slug, cwd, ageMs }]
425
+ *
426
+ * Pure selector — no IO. Same age computation as findStaleQuarantinedJobs
427
+ * above, bounded additionally by quarantineResolveAttempts so a row already
428
+ * auto-resolved (or mid-resolve on a race) is never re-selected. Deliberately
429
+ * does NOT check createdVia here — that requires a disk read of the PRD
430
+ * file, and doing it at selection time would let this pass act on a
431
+ * snapshot that's gone stale by the time the mutate() pass actually runs.
432
+ * autoResolveQuarantine below re-reads createdVia fresh, immediately before
433
+ * transitioning, inside the same mutate() callback that applies this
434
+ * selector's targets — see that function's own header for why.
435
+ */
436
+ function selectQuarantineAutoResolveTargets(jobs, now, thresholdMs) {
437
+ const targets = [];
438
+ for (const j of jobs ?? []) {
439
+ if (j.status !== 'quarantined') continue;
440
+ if ((j.quarantineResolveAttempts ?? 0) >= QUARANTINE_RESOLVE_CAP) continue;
441
+ const entry = (j.statusHistory || []).find((h) => h.to === 'quarantined');
442
+ if (!entry) continue;
443
+ const since = Date.parse(entry.at);
444
+ if (Number.isNaN(since)) continue;
445
+ const ageMs = now - since;
446
+ if (ageMs < thresholdMs) continue;
447
+ targets.push({ slug: j.slug, cwd: j.cwd ?? null, ageMs });
448
+ }
449
+ return targets;
450
+ }
451
+
452
+ // Ceiling on pollLoop's exponential poll backoff (both the 'transient'/'config'
453
+ // branch and the 'meter_rate_limited' branch below share this cap — a single
454
+ // constant so the two never drift to different ceilings).
455
+ const BACKOFF_MAX_MS = 480_000; // 8 minutes
456
+ // Threshold past which a growing consecutiveFailures streak stops being normal
457
+ // jitter and becomes worth a human's attention. health.cjs imports this so the
458
+ // WARN and the `npm run health` non-GREEN trip at the exact same count.
459
+ const FAILURE_STREAK_WARN_THRESHOLD = 5;
460
+ // How often a PERSISTING failure streak re-escalates (audit event +
461
+ // opsErrorLog line) after the initial WARN, and the health.cjs YELLOW->RED
462
+ // threshold for how long the usageCircuit has been open — one constant so
463
+ // the log cadence and the health-color flip never drift apart.
464
+ const FAILURE_STREAK_ESCALATION_MS = 30 * 60_000; // 30 minutes
465
+
466
+ /** Pure: exponential backoff with a cap, shared by every pollLoop failure branch. Exported for unit testing. */
467
+ function nextBackoffMs(prevBackoffMs) {
468
+ return prevBackoffMs ? Math.min(prevBackoffMs * 2, BACKOFF_MAX_MS) : 30_000;
469
+ }
470
+
471
+ /**
472
+ * Pure: should this failure count trigger the one-time streak WARN? Exported
473
+ * for unit testing. `alreadyWarned` is the current streak's warned flag —
474
+ * true for every failure after the threshold-crossing one, so this returns
475
+ * true exactly once per streak.
476
+ */
477
+ function shouldWarnFailureStreak(consecutiveFailures, alreadyWarned, threshold = FAILURE_STREAK_WARN_THRESHOLD) {
478
+ return consecutiveFailures >= threshold && !alreadyWarned;
479
+ }
480
+
481
+ /**
482
+ * Pure: `failureStreakWarned === true` must always carry a numeric
483
+ * `failureStreakWarnedAt` (a state file may hold one without the other), so
484
+ * the escalation message never renders "after nullm". Backfills `nowMs`.
485
+ */
486
+ function restoreFailureStreakWarnedAt(warned, warnedAt, nowMs) {
487
+ if (!warned) return typeof warnedAt === 'number' ? warnedAt : null;
488
+ return typeof warnedAt === 'number' ? warnedAt : nowMs;
489
+ }
490
+
491
+ /** Pure: whole minutes a warned streak has persisted; never null/NaN. */
492
+ function persistedStreakMinutes(warnedAt, nowMs) {
493
+ return typeof warnedAt === 'number' ? Math.round((nowMs - warnedAt) / 60_000) : 0;
494
+ }
495
+
496
+ /**
497
+ * Pure: does a PERSISTING failure streak warrant another escalation (audit
498
+ * event + opsErrorLog line) at `nowMs`? Exported for unit testing. Only
499
+ * relevant once the streak has already crossed `warnThreshold` (the initial
500
+ * WARN); `lastEscalatedAtMs` null means no escalation has fired yet this
501
+ * streak, so the first one is due immediately. Re-arms automatically once a
502
+ * streak clears (the caller resets `lastEscalatedAtMs` to null alongside
503
+ * `failureStreakWarned`), so a later independent streak escalates again.
504
+ */
505
+ function shouldEscalateFailureStreak(consecutiveFailures, lastEscalatedAtMs, nowMs, thresholdMs = FAILURE_STREAK_ESCALATION_MS, warnThreshold = FAILURE_STREAK_WARN_THRESHOLD) {
506
+ if (consecutiveFailures < warnThreshold) return false;
507
+ if (!lastEscalatedAtMs) return true;
508
+ return nowMs - lastEscalatedAtMs >= thresholdMs;
509
+ }
510
+
511
+ /**
512
+ * Leftover quarantine (PRD 1128). Resume-first recovery gets exactly one
513
+ * `--resume` attempt (selectResumeRecoveryTarget above); when that attempt
514
+ * ALSO parks needs_review with 'uncommitted_changes', the leftovers are
515
+ * about to sit dirty in the SHARED tree forever — git then refuses any later
516
+ * worktree merge for this cwd that would overwrite them, turning one parked
517
+ * job into a project-wide stall (216-jupiter-sand-kazekage, 2026-09-06).
518
+ * Pure/no I/O, mirroring selectResumeRecoveryTarget so the eligibility rule
519
+ * is unit-testable directly.
520
+ *
521
+ * Bounded to exactly one attempt via job.leftoverQuarantineAttempted, stamped
522
+ * synchronously by the caller in the SAME mutate as this decision (never
523
+ * here) — see spawnJob's finalize and reverifyNeedsReview's periodic pass.
524
+ *
525
+ * Kill-switch: SM_LEFTOVER_QUARANTINE_DISABLE=1 restores today's behaviour
526
+ * exactly (always returns null), mirroring SM_RESUME_RECOVERY_DISABLE.
527
+ */
528
+ function selectLeftoverQuarantineTarget(job) {
529
+ if (process.env.SM_LEFTOVER_QUARANTINE_DISABLE === '1') return null;
530
+ if (!job || job.status !== 'needs_review') return null;
531
+ if (job.verifierVerdict !== 'uncommitted_changes') return null;
532
+ if (job.resumeRecoveryAttempted !== true) return null;
533
+ if (job.leftoverQuarantineAttempted === true) return null;
534
+ const uncommittedPaths = Array.isArray(job.uncommittedPaths)
535
+ ? job.uncommittedPaths.filter((p) => typeof p === 'string' && p.length > 0)
536
+ : [];
537
+ if (!uncommittedPaths.length) return null;
538
+ // The single most important constraint: never touch a path that was
539
+ // ALREADY dirty at this run's own dispatch time (preRunDirtyPaths) — that
540
+ // is foreign WIP (a human's or a sibling's), not this job's own leftover.
541
+ const preRunDirty = new Set(Array.isArray(job.preRunDirtyPaths) ? job.preRunDirtyPaths : []);
542
+ const paths = uncommittedPaths.filter((p) => !preRunDirty.has(p));
543
+ if (!paths.length) return null;
544
+ return { slug: job.slug, cwd: job.cwd, paths };
545
+ }
546
+
547
+ module.exports = {
548
+ BACKOFF_MAX_MS,
549
+ FAILURE_STREAK_WARN_THRESHOLD,
550
+ FAILURE_STREAK_ESCALATION_MS,
551
+ nextBackoffMs,
552
+ shouldWarnFailureStreak,
553
+ restoreFailureStreakWarnedAt,
554
+ persistedStreakMinutes,
555
+ shouldEscalateFailureStreak,
556
+ selectLeftoverQuarantineTarget,
557
+
558
+ QUARANTINE_RESOLVE_CAP,
559
+ quarantineAutoResolveDisabled,
560
+ selectQuarantineAutoResolveTargets,
561
+ JOB_OVERRUN_FACTOR,
562
+ JOB_OVERRUN_FLOOR_MS,
563
+ JOB_BUDGET_FACTOR,
564
+ JOB_BUDGET_FLOOR_MS,
565
+ JOB_BUDGET_CEILING_MS,
566
+ computeStallSummary,
567
+ findStaleQuarantinedJobs,
568
+ findOverrunningJobs,
569
+ computeJobBudgetMs,
570
+ classifyBudgetKill,
571
+ isJobBudgetExempt,
572
+ shouldKillForBudget,
573
+ resolveBudgetKillOutcome,
574
+ findStrandedInvestigations,
575
+ computeDepHistorySatisfaction,
576
+ };