claude-code-session-manager 0.111.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 (282) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/dist/assets/{AgentLibrary-rBO1daqM.js → AgentLibrary-RwPvEgqm.js} +2 -2
  3. package/dist/assets/{Button-DsEYRZg0.js → Button-36j3PzYv.js} +1 -1
  4. package/dist/assets/{DataModel-D8sbvvBC.js → DataModel-DUhANo4l.js} +1 -1
  5. package/dist/assets/{History-BEHLtxia.js → History-BeZzNjmv.js} +2 -2
  6. package/dist/assets/{Hooks-CKpvlXrg.js → Hooks-iQ8845Hp.js} +3 -3
  7. package/dist/assets/{Library-CeF4qh9d.js → Library-DPE1h-1L.js} +6 -6
  8. package/dist/assets/{ListDetail-DcfmxPFk.js → ListDetail-PStOuQAo.js} +1 -1
  9. package/dist/assets/{MarkdownEditor-CvK1pyto.js → MarkdownEditor-DPw-N2-m.js} +1 -1
  10. package/dist/assets/{McpServers-D-h2ihzJ.js → McpServers-CUyRTNep.js} +2 -2
  11. package/dist/assets/{Memory-BnF89x8v.js → Memory-bZ2xjBGy.js} +3 -3
  12. package/dist/assets/{Panel-CQY5zano.js → Panel-_VZ_uXNg.js} +1 -1
  13. package/dist/assets/{Permissions-DLukmmsz.js → Permissions-PPRmfNbK.js} +3 -3
  14. package/dist/assets/{Plugins-0Pf0OZ4l.js → Plugins-BFcvfvlx.js} +2 -2
  15. package/dist/assets/{ProvenanceBadge-vC7Tj-JP.js → ProvenanceBadge-B1WUknak.js} +1 -1
  16. package/dist/assets/RemoteMachines-D5V0lzgW.js +3 -0
  17. package/dist/assets/{SaveBar-DjijiUb0.js → SaveBar-Dw64FVdM.js} +1 -1
  18. package/dist/assets/{Scheduler-Cm3EXZvE.js → Scheduler-C8s_pJ3I.js} +9 -9
  19. package/dist/assets/{ScopeSwitcher-B-P-kSfq.js → ScopeSwitcher-D-nKDMAE.js} +1 -1
  20. package/dist/assets/{Settings-DYDlp2m_.js → Settings-CQSrR4mO.js} +4 -4
  21. package/dist/assets/{SkillReferenceGraph-Epv2GJmj.js → SkillReferenceGraph-BbZFMHim.js} +1 -1
  22. package/dist/assets/{Skills-DHO0N0C2.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-DAMO3DhO.js → TiptapBody-Cr-hxX8L.js} +1 -1
  26. package/dist/assets/Toggle-7WpPzcZH.js +1 -0
  27. package/dist/assets/{ViewTabs-CGX6MAhc.js → ViewTabs-9lQCRwWJ.js} +1 -1
  28. package/dist/assets/{historyTotals-vgfk8EOL.js → historyTotals-BrFxw5J1.js} +1 -1
  29. package/dist/assets/{index-CPieAFsl.js → index-CTBpf-26.js} +250 -250
  30. package/dist/assets/{settingsSchema-D0FxNrTy.js → settingsSchema-Dh5Kl7VO.js} +1 -1
  31. package/dist/assets/{useAgentsDirWatch-DCu6BNYN.js → useAgentsDirWatch-CMpx0d04.js} +1 -1
  32. package/dist/assets/{useRemotePeers-D4cdJDyh.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/{macroLibrary.cjs → persona-macro/macroLibrary.cjs} +5 -5
  108. package/src/main/lib/{personaMerge.cjs → persona-macro/personaMerge.cjs} +4 -4
  109. package/src/main/lib/{prdAdminRoutes.cjs → prd/prdAdminRoutes.cjs} +7 -7
  110. package/src/main/lib/{prdAgentType.cjs → prd/prdAgentType.cjs} +3 -3
  111. package/src/main/lib/{prdCreate.cjs → prd/prdCreate.cjs} +12 -12
  112. package/src/main/lib/{prdDeclaredPaths.cjs → prd/prdDeclaredPaths.cjs} +1 -1
  113. package/src/main/lib/{prdGateFiles.cjs → prd/prdGateFiles.cjs} +1 -1
  114. package/src/main/lib/{prdLocations.cjs → prd/prdLocations.cjs} +2 -2
  115. package/src/main/lib/{prdMigration.cjs → prd/prdMigration.cjs} +3 -3
  116. package/src/main/lib/prereqs.cjs +2 -2
  117. package/src/main/lib/{instanceLock.cjs → process/instanceLock.cjs} +4 -11
  118. package/src/main/lib/{pidAlive.cjs → process/pidAlive.cjs} +1 -1
  119. package/src/main/lib/{procName.cjs → process/procName.cjs} +2 -2
  120. package/src/main/lib/{timeoutShim.cjs → process/timeoutShim.cjs} +1 -1
  121. package/src/main/lib/projectRootResolve.cjs +1 -1
  122. package/src/main/lib/promptSessionSchema.cjs +1 -1
  123. package/src/main/lib/{definitionOfDone.cjs → scheduler/definitionOfDone.cjs} +6 -12
  124. package/src/main/lib/{fixChainDepth.cjs → scheduler/fixChainDepth.cjs} +1 -1
  125. package/src/main/lib/{jobDirtFilter.cjs → scheduler/jobDirtFilter.cjs} +1 -1
  126. package/src/main/lib/{jobSupervisorRecord.cjs → scheduler/jobSupervisorRecord.cjs} +1 -1
  127. package/src/main/lib/{launchFailure.cjs → scheduler/launchFailure.cjs} +2 -2
  128. package/src/main/lib/{queueStore.cjs → scheduler/queueStore.cjs} +6 -6
  129. package/src/main/lib/{quietMachineLease.cjs → scheduler/quietMachineLease.cjs} +1 -1
  130. package/src/main/lib/{rcaReport.cjs → scheduler/rcaReport.cjs} +4 -4
  131. package/src/main/lib/{reaperHelpers.cjs → scheduler/reaperHelpers.cjs} +3 -3
  132. package/src/main/lib/{reservationExpiry.cjs → scheduler/reservationExpiry.cjs} +4 -11
  133. package/src/main/lib/{reviewNotice.cjs → scheduler/reviewNotice.cjs} +1 -1
  134. package/src/main/lib/{scheduleJobTransitions.cjs → scheduler/scheduleJobTransitions.cjs} +3 -3
  135. package/src/main/lib/{schedulerBatch.cjs → scheduler/schedulerBatch.cjs} +2 -2
  136. package/src/main/lib/{schedulerConfig.cjs → scheduler/schedulerConfig.cjs} +1 -1
  137. package/src/main/lib/{schedulerRuntimeState.cjs → scheduler/schedulerRuntimeState.cjs} +1 -1
  138. package/src/main/lib/{sessionSlots.cjs → scheduler/sessionSlots.cjs} +2 -2
  139. package/src/main/lib/{upgradeDrain.cjs → scheduler/upgradeDrain.cjs} +8 -11
  140. package/src/main/lib/{watchdogHelpers.cjs → scheduler/watchdogHelpers.cjs} +5 -19
  141. package/src/main/lib/{auditLog.cjs → telemetry/auditLog.cjs} +1 -1
  142. package/src/main/lib/{opsErrorLog.cjs → telemetry/opsErrorLog.cjs} +4 -4
  143. package/src/main/lib/{telemetryBacklog.cjs → telemetry/telemetryBacklog.cjs} +4 -4
  144. package/src/main/lib/{telemetryBoot.cjs → telemetry/telemetryBoot.cjs} +1 -1
  145. package/src/main/lib/{telemetryClient.cjs → telemetry/telemetryClient.cjs} +4 -4
  146. package/src/main/lib/{telemetrySettings.cjs → telemetry/telemetrySettings.cjs} +1 -1
  147. package/src/main/logs.cjs +1 -1
  148. package/src/main/mcpStatus.cjs +2 -2
  149. package/src/main/memoryAggregate.cjs +3 -3
  150. package/src/main/memoryTool.cjs +1 -1
  151. package/src/main/otel.cjs +2 -2
  152. package/src/main/pluginInstall.cjs +29 -3
  153. package/src/main/projectBrief.cjs +6 -6
  154. package/src/main/projectPages.cjs +1 -1
  155. package/src/main/promptSessionEvents.cjs +2 -2
  156. package/src/main/pty.cjs +7 -7
  157. package/src/main/queueOps.cjs +6 -6
  158. package/src/main/remoteClient.cjs +4 -4
  159. package/src/main/remoteClientIpc.cjs +1 -1
  160. package/src/main/runVerify.cjs +1 -1
  161. package/src/main/scheduler/CLAUDE.md +26 -0
  162. package/src/main/scheduler/budget.cjs +576 -0
  163. package/src/main/scheduler/gitTreeGuard.cjs +672 -0
  164. package/src/main/scheduler/prdParser.cjs +4 -4
  165. package/src/main/scheduler/prdPaths.cjs +487 -0
  166. package/src/main/scheduler/reviewLadder.cjs +1011 -0
  167. package/src/main/scheduler/starvation.cjs +390 -0
  168. package/src/main/scheduler.cjs +289 -3311
  169. package/src/main/seedAgentPersonas.cjs +5 -5
  170. package/src/main/seedDevPlugin.cjs +70 -3
  171. package/src/main/seedSchedulerMcp.cjs +2 -2
  172. package/src/main/supervisor.cjs +5 -5
  173. package/src/main/telemetryIpc.cjs +51 -0
  174. package/src/main/transcripts.cjs +3 -3
  175. package/src/main/updaterIpc.cjs +1 -1
  176. package/src/main/usage.cjs +1 -1
  177. package/src/main/watchers.cjs +1 -1
  178. package/src/main/webRemote.cjs +7 -7
  179. package/src/preload/api.d.ts +8 -8
  180. package/src/preload/index.cjs +3 -3
  181. package/dist/assets/RemoteMachines-CWnKbFHc.js +0 -3
  182. package/dist/assets/SystemPrompt-DxeNofL9.js +0 -1
  183. package/dist/assets/TagLibrary-QOAs7ISO.js +0 -1
  184. package/dist/assets/Toggle-Ba_f7q_m.js +0 -1
  185. package/plugins/session-manager-dev/skills/blog-for-project-feature/0-select/SKILL.md +0 -39
  186. package/plugins/session-manager-dev/skills/blog-for-project-feature/1-research/SKILL.md +0 -37
  187. package/plugins/session-manager-dev/skills/blog-for-project-feature/2-storyboard/SKILL.md +0 -64
  188. package/plugins/session-manager-dev/skills/blog-for-project-feature/3-capture/SKILL.md +0 -52
  189. package/plugins/session-manager-dev/skills/blog-for-project-feature/4-draft-copy/SKILL.md +0 -37
  190. package/plugins/session-manager-dev/skills/blog-for-project-feature/5-illustrate/SKILL.md +0 -50
  191. package/plugins/session-manager-dev/skills/blog-for-project-feature/6-compose/SKILL.md +0 -51
  192. package/plugins/session-manager-dev/skills/blog-for-project-feature/7-review/SKILL.md +0 -54
  193. package/plugins/session-manager-dev/skills/blog-for-project-feature/SKILL.md +0 -144
  194. package/plugins/session-manager-dev/skills/blog-for-project-feature/feature.config.yaml +0 -90
  195. package/plugins/session-manager-dev/skills/discover-features/SKILL.md +0 -95
  196. package/plugins/session-manager-dev/skills/explain-to-me/SKILL.md +0 -205
  197. package/plugins/session-manager-dev/skills/explain-to-me/assets/style-reference.html +0 -163
  198. package/plugins/session-manager-dev/skills/find-opportunity/SKILL.md +0 -83
  199. package/plugins/session-manager-dev/skills/issue-address/0-select/0-fetch-pool/SKILL.md +0 -38
  200. package/plugins/session-manager-dev/skills/issue-address/0-select/1-reject-fixed/SKILL.md +0 -64
  201. package/plugins/session-manager-dev/skills/issue-address/0-select/2-reject-claimed/SKILL.md +0 -71
  202. package/plugins/session-manager-dev/skills/issue-address/0-select/3-rank-impact/SKILL.md +0 -39
  203. package/plugins/session-manager-dev/skills/issue-address/0-select/SKILL.md +0 -75
  204. package/plugins/session-manager-dev/skills/issue-address/1-confirm-open/SKILL.md +0 -39
  205. package/plugins/session-manager-dev/skills/issue-address/2-claim/SKILL.md +0 -57
  206. package/plugins/session-manager-dev/skills/issue-address/3-reproduce/SKILL.md +0 -40
  207. package/plugins/session-manager-dev/skills/issue-address/4-fix/SKILL.md +0 -33
  208. package/plugins/session-manager-dev/skills/issue-address/5-verify/SKILL.md +0 -40
  209. package/plugins/session-manager-dev/skills/issue-address/SKILL.md +0 -120
  210. package/plugins/session-manager-dev/skills/local-project-health/SKILL.md +0 -58
  211. package/plugins/session-manager-dev/skills/memory-sanitation/SKILL.md +0 -147
  212. package/plugins/session-manager-dev/skills/ops-sweep/SKILL.md +0 -137
  213. package/plugins/session-manager-dev/skills/pr-review-sweep/0-fetch/SKILL.md +0 -41
  214. package/plugins/session-manager-dev/skills/pr-review-sweep/1-classify/SKILL.md +0 -56
  215. package/plugins/session-manager-dev/skills/pr-review-sweep/2-check-fixed/SKILL.md +0 -49
  216. package/plugins/session-manager-dev/skills/pr-review-sweep/3-queue/SKILL.md +0 -57
  217. package/plugins/session-manager-dev/skills/pr-review-sweep/4-land-and-resolve/SKILL.md +0 -60
  218. package/plugins/session-manager-dev/skills/pr-review-sweep/SKILL.md +0 -124
  219. package/plugins/session-manager-dev/skills/pr-signal/SKILL.md +0 -67
  220. package/plugins/session-manager-dev/skills/project-status/SKILL.md +0 -243
  221. package/plugins/session-manager-dev/skills/requesting-code-review/SKILL.md +0 -98
  222. package/plugins/session-manager-dev/skills/requesting-code-review/code-reviewer.md +0 -146
  223. /package/src/main/lib/{agentEffortResolve.cjs → claude/agentEffortResolve.cjs} +0 -0
  224. /package/src/main/lib/{claudeCliCaps.cjs → claude/claudeCliCaps.cjs} +0 -0
  225. /package/src/main/lib/{usageCircuit.cjs → claude/usageCircuit.cjs} +0 -0
  226. /package/src/main/lib/{epicSpawnCwd.cjs → epic/epicSpawnCwd.cjs} +0 -0
  227. /package/src/main/lib/{customerFeedbackAdminRoutes.cjs → feedback-remote/customerFeedbackAdminRoutes.cjs} +0 -0
  228. /package/src/main/lib/{remoteE2eCrypto.cjs → feedback-remote/remoteE2eCrypto.cjs} +0 -0
  229. /package/src/main/lib/{branchSweep.cjs → git/branchSweep.cjs} +0 -0
  230. /package/src/main/lib/{gitExec.cjs → git/gitExec.cjs} +0 -0
  231. /package/src/main/lib/{jobWorktree.cjs → git/jobWorktree.cjs} +0 -0
  232. /package/src/main/lib/{jobWorktreeBootLive.cjs → git/jobWorktreeBootLive.cjs} +0 -0
  233. /package/src/main/lib/{jobWorktreeTerminalOrphanLive.cjs → git/jobWorktreeTerminalOrphanLive.cjs} +0 -0
  234. /package/src/main/lib/{builtinMacros.cjs → persona-macro/builtinMacros.cjs} +0 -0
  235. /package/src/main/lib/{personaImportHealth.cjs → persona-macro/personaImportHealth.cjs} +0 -0
  236. /package/src/main/lib/{shippedPersonaSeeds.cjs → persona-macro/shippedPersonaSeeds.cjs} +0 -0
  237. /package/src/main/lib/{depSlugResolve.cjs → prd/depSlugResolve.cjs} +0 -0
  238. /package/src/main/lib/{planValidator.cjs → prd/planValidator.cjs} +0 -0
  239. /package/src/main/lib/{prdAuthoringSeed.cjs → prd/prdAuthoringSeed.cjs} +0 -0
  240. /package/src/main/lib/{prdDisposition.cjs → prd/prdDisposition.cjs} +0 -0
  241. /package/src/main/lib/{prdFrontmatter.cjs → prd/prdFrontmatter.cjs} +0 -0
  242. /package/src/main/lib/{prdSizing.cjs → prd/prdSizing.cjs} +0 -0
  243. /package/src/main/lib/{workTypeLibrary.cjs → prd/workTypeLibrary.cjs} +0 -0
  244. /package/src/main/lib/{childWithLog.cjs → process/childWithLog.cjs} +0 -0
  245. /package/src/main/lib/{cleanEnv.cjs → process/cleanEnv.cjs} +0 -0
  246. /package/src/main/lib/{defaultShell.cjs → process/defaultShell.cjs} +0 -0
  247. /package/src/main/lib/{killTree.cjs → process/killTree.cjs} +0 -0
  248. /package/src/main/lib/{loginShellPath.cjs → process/loginShellPath.cjs} +0 -0
  249. /package/src/main/lib/{procIdentity.cjs → process/procIdentity.cjs} +0 -0
  250. /package/src/main/lib/{singleInstanceGuard.cjs → process/singleInstanceGuard.cjs} +0 -0
  251. /package/src/main/lib/{smProcNames.cjs → process/smProcNames.cjs} +0 -0
  252. /package/src/main/lib/{timeoutShimScript.cjs → process/timeoutShimScript.cjs} +0 -0
  253. /package/src/main/lib/{winSpawn.cjs → process/winSpawn.cjs} +0 -0
  254. /package/src/main/lib/{adoptedRunSupervisor.cjs → scheduler/adoptedRunSupervisor.cjs} +0 -0
  255. /package/src/main/lib/{dispatchLoop.cjs → scheduler/dispatchLoop.cjs} +0 -0
  256. /package/src/main/lib/{dodDrainHook.cjs → scheduler/dodDrainHook.cjs} +0 -0
  257. /package/src/main/lib/{fixPlanSlug.cjs → scheduler/fixPlanSlug.cjs} +0 -0
  258. /package/src/main/lib/{gateAuthority.cjs → scheduler/gateAuthority.cjs} +0 -0
  259. /package/src/main/lib/{landedSinceRun.cjs → scheduler/landedSinceRun.cjs} +0 -0
  260. /package/src/main/lib/{loadGate.cjs → scheduler/loadGate.cjs} +0 -0
  261. /package/src/main/lib/{needsReviewLedger.cjs → scheduler/needsReviewLedger.cjs} +0 -0
  262. /package/src/main/lib/{queueHealth.cjs → scheduler/queueHealth.cjs} +0 -0
  263. /package/src/main/lib/{queueHistory.cjs → scheduler/queueHistory.cjs} +0 -0
  264. /package/src/main/lib/{runLogRetention.cjs → scheduler/runLogRetention.cjs} +0 -0
  265. /package/src/main/lib/{scheduleJobSchema.cjs → scheduler/scheduleJobSchema.cjs} +0 -0
  266. /package/src/main/lib/{schedulerPaths.cjs → scheduler/schedulerPaths.cjs} +0 -0
  267. /package/src/main/lib/{terminalRunOutcome.cjs → scheduler/terminalRunOutcome.cjs} +0 -0
  268. /package/src/main/lib/{validationSentinels.cjs → scheduler/validationSentinels.cjs} +0 -0
  269. /package/src/main/lib/{crashTelemetry.cjs → telemetry/crashTelemetry.cjs} +0 -0
  270. /package/src/main/lib/{otlpTraceExporter.cjs → telemetry/otlpTraceExporter.cjs} +0 -0
  271. /package/src/main/lib/{telemetryConsent.cjs → telemetry/telemetryConsent.cjs} +0 -0
  272. /package/src/main/lib/{telemetryCounters.cjs → telemetry/telemetryCounters.cjs} +0 -0
  273. /package/src/main/lib/{atomicFs.cjs → util/atomicFs.cjs} +0 -0
  274. /package/src/main/lib/{encodeCwd.cjs → util/encodeCwd.cjs} +0 -0
  275. /package/src/main/lib/{expandHome.cjs → util/expandHome.cjs} +0 -0
  276. /package/src/main/lib/{extractJson.cjs → util/extractJson.cjs} +0 -0
  277. /package/src/main/lib/{fileTail.cjs → util/fileTail.cjs} +0 -0
  278. /package/src/main/lib/{headTailBuffer.cjs → util/headTailBuffer.cjs} +0 -0
  279. /package/src/main/lib/{kebabCase.cjs → util/kebabCase.cjs} +0 -0
  280. /package/src/main/lib/{loopDelay.cjs → util/loopDelay.cjs} +0 -0
  281. /package/src/main/lib/{lruCache.cjs → util/lruCache.cjs} +0 -0
  282. /package/src/main/lib/{withTimeout.cjs → util/withTimeout.cjs} +0 -0
@@ -0,0 +1,390 @@
1
+ /**
2
+ * scheduler/starvation.cjs — pure queue starvation/health classifiers and the
3
+ * dispatch-idle clock. Extracted verbatim from scheduler.cjs; the watchdog that
4
+ * arms timers / forces ticks stays there and calls in. No IO, no timers.
5
+ */
6
+ 'use strict';
7
+
8
+ const launchFailure = require('../lib/scheduler/launchFailure.cjs');
9
+ const { STARVE_ESCALATION_MS } = require('../lib/scheduler/schedulerConfig.cjs');
10
+
11
+ /**
12
+ * computeBlockedChains(jobs) → [{ cwd, blockedBy, blocked }]
13
+ *
14
+ * Pure, no IO. The gap computeStallSummary above cannot see.
15
+ *
16
+ * `stalled` is defined as `running === 0 && pending === 0`, which encodes an
17
+ * assumption that a PENDING row is healthy in-progress work. It is not: a
18
+ * pending row whose `dependsOn` chain terminates in a TERMINAL non-completed
19
+ * status (`failed`/`skipped`) can never be dispatched by pickForProject, and
20
+ * never will be, but it still counts toward `pending` and so reads as a
21
+ * healthy queue to every monitor in the app.
22
+ *
23
+ * On 2026-09-05 starry-night-ships held 42 such rows behind one `failed`
24
+ * job for three hours. Machine-wide `stalled` was false (42 pending),
25
+ * per-project `stalled` was false (42 pending), the queue-health sweep
26
+ * doesn't count `failed` at all, and the supervisor only probes `running` —
27
+ * so nothing anywhere reported a problem while nothing could ever run.
28
+ *
29
+ * Reported per project as { blockedBy: [terminal slugs], blocked: count }.
30
+ * Transitive by construction: a row blocked by a row that is itself blocked
31
+ * resolves through the same walk.
32
+ */
33
+ function computeBlockedChains(jobs) {
34
+ const rows = (Array.isArray(jobs) ? jobs : []).filter(Boolean);
35
+ const byCwd = new Map();
36
+ for (const j of rows) {
37
+ const key = j.cwd || '(unknown)';
38
+ if (!byCwd.has(key)) byCwd.set(key, []);
39
+ byCwd.get(key).push(j);
40
+ }
41
+
42
+ const out = [];
43
+ for (const [cwd, projectJobs] of byCwd) {
44
+ // Reuse the picker's OWN dep resolution so this can never disagree with
45
+ // what the scheduler will actually dispatch (bare-name fallback included).
46
+ const rowBySlug = new Map(projectJobs.map((j) => [j.slug, j]));
47
+ const rowsByBareSlug = new Map();
48
+ for (const j of projectJobs) {
49
+ const bare = String(j.slug ?? '').replace(/^\d+-/, '');
50
+ if (!rowsByBareSlug.has(bare)) rowsByBareSlug.set(bare, []);
51
+ rowsByBareSlug.get(bare).push(j);
52
+ }
53
+ const rowsForDep = (slug) => {
54
+ const exact = rowBySlug.get(slug);
55
+ if (exact) return [exact];
56
+ return rowsByBareSlug.get(String(slug ?? '').replace(/^\d+-/, '')) ?? [];
57
+ };
58
+
59
+ // Memoised walk: does this row's dep closure hit a terminally-stuck row?
60
+ const TERMINAL_STUCK = new Set(['failed', 'skipped']);
61
+ const verdicts = new Map(); // slug -> Set of terminal blocker slugs
62
+ const visiting = new Set();
63
+ const blockersFor = (job) => {
64
+ if (!job) return new Set();
65
+ if (verdicts.has(job.slug)) return verdicts.get(job.slug);
66
+ if (visiting.has(job.slug)) return new Set(); // dependsOn cycle — not our problem here
67
+ visiting.add(job.slug);
68
+ const found = new Set();
69
+ for (const depSlug of job.dependsOn ?? []) {
70
+ for (const dep of rowsForDep(depSlug)) {
71
+ if (TERMINAL_STUCK.has(dep.status)) found.add(dep.slug);
72
+ else if (dep.status !== 'completed') for (const b of blockersFor(dep)) found.add(b);
73
+ }
74
+ }
75
+ visiting.delete(job.slug);
76
+ verdicts.set(job.slug, found);
77
+ return found;
78
+ };
79
+
80
+ const blockedBy = new Set();
81
+ let blocked = 0;
82
+ for (const j of projectJobs) {
83
+ if (j.status !== 'pending') continue;
84
+ const bs = blockersFor(j);
85
+ if (bs.size === 0) continue;
86
+ blocked += 1;
87
+ for (const b of bs) blockedBy.add(b);
88
+ }
89
+ if (blocked > 0) out.push({ cwd, blockedBy: [...blockedBy].sort(), blocked });
90
+ }
91
+ return out;
92
+ }
93
+
94
+ /** How long a queue may hold ready work with nothing running before the
95
+ * starvation watchdog forces a tick. Deliberately longer than the poll
96
+ * loop's own cadence + backoff, so this only ever fires when the normal
97
+ * path has genuinely stopped driving the queue — it is a safety net, not a
98
+ * second scheduler. */
99
+ const QUEUE_STARVATION_MS = 10 * 60_000;
100
+
101
+ /**
102
+ * dispatchIdleMs({ lastRunAtMs, lastPauseClearedAtMs, schedulerBootedAtMs, now }) → ms
103
+ *
104
+ * Pure. The ONE dispatch-idleness clock: `now - max(lastRunAt, lastPauseClearedAt,
105
+ * schedulerBootedAt)`. `lastRunAt` has exactly one writer (tickQueue, right
106
+ * before the spawn loop) so it already means "a batch actually launched";
107
+ * a pause clear and a scheduler boot are the other two moments the queue
108
+ * legitimately (re)starts. Deliberately NOT `lastDispatchAttemptAt`, which
109
+ * tickQueue stamps before every gate — a queue that ticks every 30 s and
110
+ * launches nothing (leaked slot, stuck hold) would refresh that stamp
111
+ * forever and never look idle (the structural repeat of the f18e161 bug
112
+ * where lastRunAt was refreshed every poll). No finite input → Infinity.
113
+ */
114
+ function dispatchIdleMs({ lastRunAtMs, lastPauseClearedAtMs, schedulerBootedAtMs, now } = {}) {
115
+ const stamps = [lastRunAtMs, lastPauseClearedAtMs, schedulerBootedAtMs].filter(Number.isFinite);
116
+ return stamps.length ? now - Math.max(...stamps) : Infinity;
117
+ }
118
+
119
+ /**
120
+ * launchBlockedSlugs(jobs, launchBlocks) → Set<slug>
121
+ *
122
+ * Pure, sync (health.cjs runs as a cold process). Pending rows whose persona
123
+ * has ANY launch-breaker entry — a superset of computeLaunchHolds, which
124
+ * additionally lets one half-open probe row through per persona.
125
+ */
126
+ function launchBlockedSlugs(jobs, launchBlocks) {
127
+ const out = new Set();
128
+ if (!launchBlocks || !Object.keys(launchBlocks).length) return out;
129
+ for (const j of Array.isArray(jobs) ? jobs : []) {
130
+ if (j && j.status === 'pending' && launchBlocks[launchFailure.launchBlockKeyFor(j)]) out.add(j.slug);
131
+ }
132
+ return out;
133
+ }
134
+
135
+ /**
136
+ * classifyQueueStarvation({ jobs, paused, runningCount, lastRunAtMs, now, thresholdMs })
137
+ * → null | { kind: 'starved' | 'blocked', pending, dispatchable, blockedChains, idleMs }
138
+ *
139
+ * Pure, no IO. Answers the one question the user's invariant reduces to:
140
+ * "there are PRDs in a queue — is anything actually going to run them?"
141
+ *
142
+ * Every stall this codebase has seen was a DIFFERENT cause with the SAME
143
+ * shape: ready rows, nothing running, nobody ticking. A rate-limited exit
144
+ * stamped terminal `failed` (2026-09-05, 42 rows); a spin loop past the
145
+ * manual-clear cooldown; a worktree merge-back that left the project on a
146
+ * job branch; a job parked `needs_review` with no fix plan; app churn
147
+ * counted as unfinished work. Guarding each cause individually will always
148
+ * lag the next one, so this guards the SHAPE instead.
149
+ *
150
+ * Two outcomes, deliberately distinguished — they need opposite responses:
151
+ * 'starved' — at least one pending row is dispatchable RIGHT NOW and
152
+ * nothing is running. Whatever should have ticked, didn't.
153
+ * Forcing a tick is safe and fixes it.
154
+ * 'blocked' — every pending row is behind a terminal/parked dependency.
155
+ * A tick cannot help; this needs a human (or a heal pass) to
156
+ * resolve the blocker, and must be reported as such rather
157
+ * than silently re-ticking forever.
158
+ *
159
+ * Returns null when the queue is healthy (work running, nothing pending,
160
+ * paused on purpose, or simply not idle long enough yet).
161
+ */
162
+ function classifyQueueStarvation({ jobs, paused, runningCount, lastRunAtMs, lastPauseClearedAtMs, schedulerBootedAtMs, heldSlugs, now, thresholdMs = QUEUE_STARVATION_MS } = {}) {
163
+ if (paused) return null; // paused is a DECISION, not a stall
164
+ if (runningCount > 0) return null; // work is flowing
165
+ const rows = Array.isArray(jobs) ? jobs : [];
166
+ const pending = rows.filter((j) => j && j.status === 'pending');
167
+ if (pending.length === 0) return null; // nothing to run — not a stall
168
+
169
+ const idleMs = dispatchIdleMs({ lastRunAtMs, lastPauseClearedAtMs, schedulerBootedAtMs, now });
170
+ if (idleMs < thresholdMs) return null; // give the normal path its chance first
171
+
172
+ // Which pending rows could actually dispatch? Anything NOT named by a
173
+ // blocked chain (computeBlockedChains walks dependsOn with the picker's own
174
+ // resolution, so the two can never disagree) and NOT held by an open launch
175
+ // breaker / the quietMachine lease (`heldSlugs` — a separate input, never
176
+ // folded into the dependsOn walker). Held rows can't launch no matter how
177
+ // often we tick, so they read as 'blocked' (needs a human), not 'starved'.
178
+ const blockedChains = computeBlockedChains(rows);
179
+ const held = heldSlugs?.has ? heldSlugs : new Set(heldSlugs ?? []);
180
+ const open = held.size > 0 ? rows.filter((j) => !(j.status === 'pending' && held.has(j.slug))) : rows;
181
+ const openBlocked = held.size > 0 ? computeBlockedChains(open) : blockedChains;
182
+ const openPending = open.filter((j) => j.status === 'pending').length;
183
+ const dispatchable = openPending - openBlocked.reduce((n, c) => n + c.blocked, 0);
184
+
185
+ return {
186
+ kind: dispatchable > 0 ? 'starved' : 'blocked',
187
+ pending: pending.length,
188
+ dispatchable,
189
+ blockedChains,
190
+ idleMs,
191
+ };
192
+ }
193
+
194
+ /**
195
+ * classifyQueueStarvationByProject({ jobs, paused, runningSet, lastRunAtMs, now, thresholdMs })
196
+ * → [{ cwd, kind: 'starved' | 'blocked', pending, dispatchable, blockedChains, idleMs }]
197
+ *
198
+ * Per-project driver around classifyQueueStarvation's pure single-project
199
+ * core. `runningCount > 0` inside that core used to be fed the MACHINE-WIDE
200
+ * `runningSet.size`, which meant one long-lived job in ANY project disarmed
201
+ * the watchdog for EVERY other project on the box — observed live
202
+ * 2026-09-12: a job in starry-night-ships ran 80+ minutes while two other
203
+ * projects sat starved/blocked for hours, and the watchdog never fired once
204
+ * because "work is flowing" was true somewhere else. Partitioning by cwd
205
+ * (the same grouping computeBlockedChains already does) fixes DETECTION only
206
+ * — the idle clock (see dispatchIdleMs) stays machine-wide, since
207
+ * `lastRunAt` is machine-level state, and only one tick is ever
208
+ * forced per watchdog pass regardless of how many cwds are starved.
209
+ *
210
+ * Pure, no IO. Returns [] when paused (a DECISION, not a stall) or when no
211
+ * project has a verdict.
212
+ */
213
+ function classifyQueueStarvationByProject({ jobs, paused, runningSet: runningSlugs, lastRunAtMs, lastPauseClearedAtMs, schedulerBootedAtMs, heldSlugs, now, thresholdMs = QUEUE_STARVATION_MS } = {}) {
214
+ if (paused) return [];
215
+ const rows = (Array.isArray(jobs) ? jobs : []).filter(Boolean);
216
+ const byCwd = new Map();
217
+ for (const j of rows) {
218
+ const key = j.cwd || '(unknown)';
219
+ if (!byCwd.has(key)) byCwd.set(key, []);
220
+ byCwd.get(key).push(j);
221
+ }
222
+
223
+ const verdicts = [];
224
+ for (const [cwd, projectJobs] of byCwd) {
225
+ // Same source of truth tickQueue itself uses for "is anything running":
226
+ // the in-process runningSet OR a row already stamped status:'running'.
227
+ const projRunningCount = projectJobs.filter(
228
+ (j) => j.status === 'running' || runningSlugs?.has?.(j.slug),
229
+ ).length;
230
+ const verdict = classifyQueueStarvation({
231
+ jobs: projectJobs,
232
+ paused: false,
233
+ runningCount: projRunningCount,
234
+ lastRunAtMs,
235
+ lastPauseClearedAtMs,
236
+ schedulerBootedAtMs,
237
+ heldSlugs,
238
+ now,
239
+ thresholdMs,
240
+ });
241
+ if (verdict) verdicts.push({ cwd, ...verdict });
242
+ }
243
+ return verdicts;
244
+ }
245
+
246
+ /**
247
+ * classifyQueueHealth({ jobs, paused, launchBlocks, runningSet, freeSlots,
248
+ * totalSlots, lastRunAtMs, lastPauseClearedAtMs, schedulerBootedAtMs, now, cwd, thresholdMs })
249
+ * → { kind, cwd, pending, dispatchable, blockedChains, needsReviewCount, runningCount, ... }
250
+ *
251
+ * Single source of truth for the Scheduler page's queue-health header: the
252
+ * one thing a human staring at a stale-looking queue needs is "which of the
253
+ * genuinely different causes is this" (all slots busy? every pending row
254
+ * blocked on a dependency? the dispatch driver itself never ticked?) — this
255
+ * function names that cause instead of leaving the renderer to re-derive it.
256
+ *
257
+ * Reuses classifyQueueStarvation for the blocked/stalled read so the header
258
+ * can never disagree with runQueueStarvationWatchdog's own decision to force
259
+ * a tick: both are handed the same launch-keyed idle clock (dispatchIdleMs) and
260
+ * the same computeBlockedChains walk under the hood. Called here with
261
+ * `thresholdMs: 0` first (a live header must say "blocked" the instant every
262
+ * pending row is dependency-stuck, not wait out the watchdog's own 10-minute
263
+ * grace period) — the returned `idleMs` is then compared against the REAL
264
+ * `thresholdMs` to decide 'stalled' vs the healthy 'running' default, which
265
+ * is exactly the comparison classifyQueueStarvation would make internally.
266
+ *
267
+ * `pending`/`dispatchable`/`blockedChains`/`needsReviewCount` are always
268
+ * populated (via computeBlockedChains — the exact primitive
269
+ * classifyQueueStarvation itself calls) regardless of kind, so a 'saturated'
270
+ * or 'running' header can still say how much of the backlog is dependency-
271
+ * blocked, not just the kinds where that's the headline cause.
272
+ *
273
+ * Kinds, in the priority order they're checked (paused is a decision, not a
274
+ * stall; an open launch breaker explains an otherwise-inexplicable
275
+ * non-dispatch before slot/dependency causes are even considered):
276
+ * 'paused' — the scheduler itself is paused.
277
+ * 'launch-blocked' — a pending row's persona has an active circuit-breaker
278
+ * entry (lib/scheduler/launchFailure.cjs).
279
+ * 'idle' — nothing pending in this scope.
280
+ * 'saturated' — pending work exists but every session slot is in use.
281
+ * 'blocked' — nothing running, slots free, every pending row's
282
+ * dependsOn chain terminates in a non-completed row.
283
+ * 'stalled' — nothing running, slots free, at least one row is
284
+ * dispatchable right now, and the dispatch driver has
285
+ * been idle >= thresholdMs (agrees with the watchdog).
286
+ * 'running' — the healthy default: work is flowing, or the driver
287
+ * hasn't been idle long enough to call a stall yet.
288
+ *
289
+ * Pure, no IO. `cwd` scopes jobs/pending/blocked/needsReview to one project
290
+ * (the Scheduler nav row is PROJECT-face — see CLAUDE.md); `freeSlots` /
291
+ * `totalSlots` / `launchBlocks` stay machine-wide inputs by design, same as
292
+ * WindowStrip's existing scopeCwd split.
293
+ */
294
+ function classifyQueueHealth({
295
+ jobs, paused, launchBlocks, runningSet: runningSlugs, freeSlots, totalSlots,
296
+ lastRunAtMs, lastPauseClearedAtMs, schedulerBootedAtMs, now, cwd = null, thresholdMs = QUEUE_STARVATION_MS,
297
+ } = {}) {
298
+ const rows = (Array.isArray(jobs) ? jobs : []).filter(Boolean);
299
+ const projectJobs = cwd ? rows.filter((j) => j.cwd === cwd) : rows;
300
+ const pendingRows = projectJobs.filter((j) => j.status === 'pending');
301
+ const runningRows = projectJobs.filter((j) => j.status === 'running' || runningSlugs?.has?.(j.slug));
302
+ const needsReviewCount = projectJobs.filter((j) => j.status === 'needs_review').length;
303
+
304
+ // computeBlockedChains is the SAME primitive classifyQueueStarvation calls
305
+ // internally, computed once here so EVERY kind (not just 'blocked'/
306
+ // 'stalled') carries real dispatchable-vs-blocked counts instead of a null.
307
+ const blockedChains = computeBlockedChains(projectJobs);
308
+ const blockedTotal = blockedChains.reduce((n, c) => n + c.blocked, 0);
309
+ const dispatchable = Math.max(0, pendingRows.length - blockedTotal);
310
+ const base = {
311
+ cwd, pending: pendingRows.length, dispatchable, blockedChains, needsReviewCount, runningCount: runningRows.length,
312
+ };
313
+
314
+ if (paused) return { ...base, kind: 'paused', reason: paused.reason ?? null };
315
+
316
+ // launch-blocked: only a persona a PENDING row in this scope actually uses
317
+ // — a breaker open for a persona nothing here needs is not this scope's
318
+ // problem (matches WindowStrip's own unconditional-banner-per-block read).
319
+ const neededAgentTypes = new Set(pendingRows.map((j) => launchFailure.launchBlockKeyFor(j)));
320
+ for (const [key, block] of Object.entries(launchBlocks ?? {})) {
321
+ if (block && neededAgentTypes.has(key)) {
322
+ return { ...base, kind: 'launch-blocked', agentType: key, block };
323
+ }
324
+ }
325
+
326
+ if (pendingRows.length === 0) return { ...base, kind: 'idle' };
327
+
328
+ // classifyQueueStarvation only ever classifies while nothing is running
329
+ // (its own runningCount > 0 guard) — that boundary is also exactly where
330
+ // slot saturation, not dependency shape, is the honest cause.
331
+ if (runningRows.length > 0) {
332
+ if (Number.isFinite(freeSlots) && freeSlots <= 0) {
333
+ return { ...base, kind: 'saturated', totalSlots: totalSlots ?? null };
334
+ }
335
+ return { ...base, kind: 'running' };
336
+ }
337
+
338
+ // Nothing running: hand the SAME rows + idle clock to classifyQueueStarvation
339
+ // (thresholdMs: 0 — a live header must say "blocked" the instant every
340
+ // pending row is dependency-stuck, not wait out the watchdog's own grace
341
+ // period) purely for its idleMs reading; its own dispatchable/blockedChains
342
+ // are mathematically identical to `base`'s (same computeBlockedChains walk
343
+ // over the same rows), so `base` already carries them.
344
+ const immediate = classifyQueueStarvation({
345
+ jobs: projectJobs, paused: false, runningCount: 0,
346
+ lastRunAtMs, lastPauseClearedAtMs, schedulerBootedAtMs, now, thresholdMs: 0,
347
+ });
348
+ // pending.length is already > 0 above, so `immediate` can only be null when
349
+ // the clock stamp is itself in the future (clock skew) — fall back to
350
+ // computing idleMs the same way rather than asserting a kind we can't
351
+ // back up with a real number.
352
+ const idleMs = immediate ? immediate.idleMs
353
+ : dispatchIdleMs({ lastRunAtMs, lastPauseClearedAtMs, schedulerBootedAtMs, now });
354
+ if (dispatchable === 0) return { ...base, kind: 'blocked', idleMs };
355
+ const kind = idleMs >= thresholdMs ? 'stalled' : 'running';
356
+ return { ...base, kind, idleMs };
357
+ }
358
+
359
+ /**
360
+ * selectStarveEscalations(starvedProjects, escalatedCwds, thresholdMs)
361
+ * → { toEscalate: [...sp], toClear: [cwd, ...] }
362
+ *
363
+ * Pure. `starvedProjects` is this sweep's findStarvedProjects() output (the
364
+ * per-cwd STARVED verdict — cwd, pendingCount, oldestPendingSlug, ageMs);
365
+ * `escalatedCwds` is the Set already latched from a prior sweep.
366
+ *
367
+ * toEscalate: rows crossing thresholdMs for the FIRST time this stretch —
368
+ * i.e. old enough AND not already latched.
369
+ * toClear: previously-latched cwds no longer reported as starved at all this
370
+ * sweep, so a LATER starve on that project escalates again instead of being
371
+ * silently suppressed forever by a stale latch.
372
+ */
373
+ function selectStarveEscalations(starvedProjects, escalatedCwds, thresholdMs = STARVE_ESCALATION_MS) {
374
+ const stillStarved = new Set(starvedProjects.map((sp) => sp.cwd));
375
+ const toClear = [...escalatedCwds].filter((cwd) => !stillStarved.has(cwd));
376
+ const toEscalate = starvedProjects.filter((sp) => sp.ageMs >= thresholdMs && !escalatedCwds.has(sp.cwd));
377
+ return { toEscalate, toClear };
378
+ }
379
+
380
+ module.exports = {
381
+ computeBlockedChains,
382
+ QUEUE_STARVATION_MS,
383
+ STARVE_ESCALATION_MS,
384
+ dispatchIdleMs,
385
+ launchBlockedSlugs,
386
+ classifyQueueStarvation,
387
+ classifyQueueStarvationByProject,
388
+ classifyQueueHealth,
389
+ selectStarveEscalations,
390
+ };