tumwater 0.1.3 → 0.2.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 (359) hide show
  1. package/README.md +63 -28
  2. package/dist/build-info.json +2 -2
  3. package/dist/src/agent/agent-args.js +24 -0
  4. package/dist/src/agent/agent-backend.js +2 -2
  5. package/dist/src/agent/backend-traits.js +60 -0
  6. package/dist/src/agent/claude/claude-account.js +61 -8
  7. package/dist/src/agent/claude/claude-args.js +12 -15
  8. package/dist/src/agent/claude/claude-backend.js +51 -41
  9. package/dist/src/agent/claude/claude-env.js +7 -3
  10. package/dist/src/agent/claude/claude-stream.js +7 -16
  11. package/dist/src/agent/image-input.js +107 -0
  12. package/dist/src/agent/notes-shim.js +41 -0
  13. package/dist/src/agent/opencode/opencode-args.js +61 -0
  14. package/dist/src/agent/opencode/opencode-backend.js +255 -0
  15. package/dist/src/agent/opencode/opencode-catalog.js +184 -0
  16. package/dist/src/agent/opencode/opencode-config.js +188 -0
  17. package/dist/src/agent/opencode/opencode-env.js +36 -0
  18. package/dist/src/agent/opencode/opencode-session-id.js +37 -0
  19. package/dist/src/agent/opencode/opencode-stream.js +278 -0
  20. package/dist/src/agent/role-notes-write.js +16 -32
  21. package/dist/src/agent/run-agent.js +95 -18
  22. package/dist/src/agent/session-ledger.js +57 -1
  23. package/dist/src/backlog/backlog-archive.js +296 -0
  24. package/dist/src/backlog/backlog-cli.js +573 -0
  25. package/dist/src/backlog/backlog-eligibility.js +332 -43
  26. package/dist/src/backlog/backlog-md.js +79 -17
  27. package/dist/src/backlog/backlog-render.js +62 -11
  28. package/dist/src/backlog/backlog-structure.js +261 -58
  29. package/dist/src/backlog/backlog-write.js +178 -441
  30. package/dist/src/backlog/backlog.js +113 -16
  31. package/dist/src/backlog/question-answer.js +339 -0
  32. package/dist/src/baseline/main-baseline.js +75 -88
  33. package/dist/src/baseline/main-red.js +32 -30
  34. package/dist/src/{brief-edit.js → brief/brief-edit.js} +12 -11
  35. package/dist/src/{brief.js → brief/brief.js} +33 -8
  36. package/dist/src/budget/budget.js +32 -15
  37. package/dist/src/budget/fallback-breaker.js +8 -7
  38. package/dist/src/build/build-check-detect.js +8 -7
  39. package/dist/src/build/build-check-events.js +68 -22
  40. package/dist/src/build/build-check-report.js +22 -23
  41. package/dist/src/build/build-check-scoped.js +51 -62
  42. package/dist/src/build/build-check.js +15 -14
  43. package/dist/src/build/build-info.js +2 -1
  44. package/dist/src/build/build-stage.js +23 -22
  45. package/dist/src/build/dep-install.js +22 -21
  46. package/dist/src/build/host-sleep.js +1 -1
  47. package/dist/src/change/change-data.js +13 -11
  48. package/dist/src/change/change-render.js +11 -10
  49. package/dist/src/cli/brief-command.js +9 -7
  50. package/dist/src/cli/cli-args.js +25 -5
  51. package/dist/src/cli/cli-command-args.js +43 -10
  52. package/dist/src/cli/cli-editor.js +13 -1
  53. package/dist/src/cli/cli-flag-specs.js +31 -6
  54. package/dist/src/cli/cli-marker-commands.js +13 -10
  55. package/dist/src/cli/cli-query-commands.js +439 -56
  56. package/dist/src/cli/cli-repo-gate.js +1 -1
  57. package/dist/src/cli/cli-run.js +124 -68
  58. package/dist/src/cli/completion.js +70 -12
  59. package/dist/src/cli/config-commands.js +14 -11
  60. package/dist/src/cli/help.js +264 -124
  61. package/dist/src/cli/log-commands.js +127 -21
  62. package/dist/src/cli/notify-command.js +33 -18
  63. package/dist/src/cli/question-commands.js +13 -224
  64. package/dist/src/cli.js +78 -36
  65. package/dist/src/collections.js +66 -16
  66. package/dist/src/concurrency/check-permit.js +16 -17
  67. package/dist/src/concurrency/lock.js +6 -5
  68. package/dist/src/concurrency/provider-lease.js +69 -0
  69. package/dist/src/concurrency/semaphore.js +258 -36
  70. package/dist/src/config/config-aliases.js +106 -0
  71. package/dist/src/config/config-editable-keys.js +3 -3
  72. package/dist/src/config/config-fallbacks.js +379 -0
  73. package/dist/src/config/config-field-checks.js +19 -10
  74. package/dist/src/config/config-keys.js +160 -0
  75. package/dist/src/config/config-live.js +64 -12
  76. package/dist/src/config/config-schema.js +34 -22
  77. package/dist/src/config/config-validation.js +132 -48
  78. package/dist/src/config/config-views.js +107 -325
  79. package/dist/src/config/config-write.js +138 -177
  80. package/dist/src/config/config.js +60 -49
  81. package/dist/src/config/model-selector.js +28 -2
  82. package/dist/src/doctor/doctor-backlog.js +494 -68
  83. package/dist/src/doctor/doctor-checks.js +509 -58
  84. package/dist/src/doctor/doctor-holds.js +139 -78
  85. package/dist/src/doctor/doctor-install.js +30 -0
  86. package/dist/src/doctor/doctor-launch-services.js +2 -2
  87. package/dist/src/doctor/doctor-model-checks.js +549 -157
  88. package/dist/src/doctor/doctor-notify.js +2 -3
  89. package/dist/src/doctor/doctor-orphans.js +10 -9
  90. package/dist/src/doctor/doctor-render.js +36 -9
  91. package/dist/src/doctor/doctor.js +76 -25
  92. package/dist/src/doctor/image-probe.js +244 -0
  93. package/dist/src/doctor/probe.js +64 -0
  94. package/dist/src/events/event-format.js +219 -106
  95. package/dist/src/events/event-labels.js +116 -0
  96. package/dist/src/events/event-legacy.js +140 -0
  97. package/dist/src/events/event-read.js +58 -22
  98. package/dist/src/events/event-window.js +27 -7
  99. package/dist/src/events/events.js +18 -14
  100. package/dist/src/events/notify.js +80 -30
  101. package/dist/src/failure/failure-cluster.js +12 -9
  102. package/dist/src/failure/failure-data.js +96 -48
  103. package/dist/src/failure/failure-render.js +69 -58
  104. package/dist/src/failure/failure-state-change.js +33 -26
  105. package/dist/src/failure/time-spend.js +79 -59
  106. package/dist/src/files/file-queue.js +16 -4
  107. package/dist/src/files/files.js +18 -6
  108. package/dist/src/files/json-object.js +18 -3
  109. package/dist/src/files/tail.js +1 -1
  110. package/dist/src/gates/bootstrap-gates.js +5 -4
  111. package/dist/src/gates/budget-gates.js +250 -34
  112. package/dist/src/gates/disk-gate.js +18 -18
  113. package/dist/src/gates/gate-polls.js +117 -69
  114. package/dist/src/gates/maintenance-quota.js +29 -29
  115. package/dist/src/gates/pause-gates.js +11 -11
  116. package/dist/src/gates/readiness.js +2 -1
  117. package/dist/src/gates/role-cap-gates.js +16 -21
  118. package/dist/src/gates/startup-gate.js +75 -31
  119. package/dist/src/gates/streak-gate.js +18 -16
  120. package/dist/src/git/commit-message.js +6 -5
  121. package/dist/src/git/git-diff.js +16 -16
  122. package/dist/src/git/git-run.js +8 -8
  123. package/dist/src/git/git.js +79 -59
  124. package/dist/src/git/slots-state.js +14 -5
  125. package/dist/src/git/worktree-pool.js +25 -18
  126. package/dist/src/git/worktree-use.js +15 -14
  127. package/dist/src/git/worktree.js +18 -18
  128. package/dist/src/gui/gui-args.js +10 -14
  129. package/dist/src/gui/gui-backlog-commands.js +273 -0
  130. package/dist/src/gui/gui-command.js +1 -1
  131. package/dist/src/gui/gui-config-commands.js +440 -0
  132. package/dist/src/gui/gui-endpoint-commands.js +16 -716
  133. package/dist/src/gui/gui-endpoint-common.js +49 -0
  134. package/dist/src/gui/gui-endpoints.js +105 -54
  135. package/dist/src/gui/gui-operator-commands.js +145 -0
  136. package/dist/src/gui/gui-prompt-commands.js +234 -0
  137. package/dist/src/gui/gui-server.js +25 -8
  138. package/dist/src/gui/gui-team-commands.js +164 -0
  139. package/dist/src/gui/http-body.js +17 -1
  140. package/dist/src/history/history-data.js +179 -104
  141. package/dist/src/history/history-render.js +76 -0
  142. package/dist/src/history/history.js +29 -70
  143. package/dist/src/inbox/inbox-attachments.js +66 -10
  144. package/dist/src/inbox/inbox-cancel.js +7 -5
  145. package/dist/src/inbox/inbox-edit.js +1 -1
  146. package/dist/src/inbox/inbox-submit.js +44 -4
  147. package/dist/src/inbox/inbox-top.js +83 -0
  148. package/dist/src/inbox/inbox.js +8 -2
  149. package/dist/src/inbox/pending-prompt.js +1 -1
  150. package/dist/src/inbox/prompt-commands.js +114 -73
  151. package/dist/src/init/init-templates.js +16 -14
  152. package/dist/src/init/init.js +12 -11
  153. package/dist/src/loop/leftover.js +28 -27
  154. package/dist/src/loop/loop-fallback.js +2 -2
  155. package/dist/src/loop/loop-pi.js +104 -50
  156. package/dist/src/loop/loop-recovery.js +15 -15
  157. package/dist/src/loop/loop-state.js +130 -13
  158. package/dist/src/loop/loop-tick.js +190 -88
  159. package/dist/src/loop/loop.js +68 -36
  160. package/dist/src/loop/model-fallback.js +3 -2
  161. package/dist/src/loop/revision.js +16 -7
  162. package/dist/src/merge/backlog-conflicts.js +155 -0
  163. package/dist/src/{landing/landing-batch.js → merge/merge-batch.js} +90 -89
  164. package/dist/src/{landing/landing-check-failures.js → merge/merge-check-failures.js} +87 -41
  165. package/dist/src/{landing/landing-core.js → merge/merge-core.js} +119 -95
  166. package/dist/src/{landing/landing-diff.js → merge/merge-diff.js} +1 -1
  167. package/dist/src/merge/merge-drain.js +212 -0
  168. package/dist/src/{landing/landing-git.js → merge/merge-git.js} +94 -20
  169. package/dist/src/merge/merge-pipeline.js +99 -0
  170. package/dist/src/{landing/landing-questions.js → merge/merge-questions.js} +9 -6
  171. package/dist/src/merge/merge-queue-render.js +75 -0
  172. package/dist/src/{landing/landing-queue.js → merge/merge-queue.js} +95 -36
  173. package/dist/src/merge/merge-slot.js +340 -0
  174. package/dist/src/{landing/landing-stack.js → merge/merge-stack.js} +66 -62
  175. package/dist/src/{landing/landing-merge.js → merge/merge-to-main.js} +72 -62
  176. package/dist/src/merge/merge-verify.js +212 -0
  177. package/dist/src/notes-cli.js +97 -0
  178. package/dist/src/operator/operator-commands.js +113 -45
  179. package/dist/src/operator/operator-intent.js +74 -53
  180. package/dist/src/operator/operator-requests.js +52 -52
  181. package/dist/src/operator/retire.js +47 -28
  182. package/dist/src/operator/revert.js +71 -0
  183. package/dist/src/orchestrator/orchestrator-launch.js +74 -14
  184. package/dist/src/orchestrator/orchestrator-scheduling.js +74 -43
  185. package/dist/src/orchestrator/orchestrator.js +149 -96
  186. package/dist/src/orchestrator/retention.js +29 -12
  187. package/dist/src/orchestrator/startup-migration-steps.js +51 -0
  188. package/dist/src/orchestrator/startup-migrations.js +40 -0
  189. package/dist/src/paths.js +124 -38
  190. package/dist/src/pi/pi-args.js +26 -3
  191. package/dist/src/pi/pi-backend.js +15 -23
  192. package/dist/src/pi/pi-bin.js +18 -12
  193. package/dist/src/pi/pi-event-line.js +17 -1
  194. package/dist/src/pi/pi-failure.js +87 -0
  195. package/dist/src/pi/pi-models.js +126 -38
  196. package/dist/src/pi/pi-run-result.js +1 -1
  197. package/dist/src/pi/pi-settings.js +86 -0
  198. package/dist/src/pi/pi-stream.js +131 -107
  199. package/dist/src/pi/pi-watchdogs.js +4 -3
  200. package/dist/src/pi-extension/bounded-output.js +20 -5
  201. package/dist/src/pi-extension/context-budget.js +3 -1
  202. package/dist/src/pi-extension/context-shake.js +54 -21
  203. package/dist/src/pi-extension/context-window.js +37 -0
  204. package/dist/src/pi-extension/full-output.js +2 -2
  205. package/dist/src/pi-extension/strip-thinking.js +80 -0
  206. package/dist/src/pi-extension/trim-system-prompt.js +38 -0
  207. package/dist/src/process/launch-services.js +11 -10
  208. package/dist/src/process/process-table.js +1 -1
  209. package/dist/src/process/process.js +7 -7
  210. package/dist/src/process/run-marker.js +29 -6
  211. package/dist/src/process/supervisor.js +36 -25
  212. package/dist/src/prompt/dialect.js +30 -10
  213. package/dist/src/prompt/gate-prompts.js +170 -67
  214. package/dist/src/prompt/principles.js +1 -0
  215. package/dist/src/prompt/prompt-followup.js +29 -20
  216. package/dist/src/prompt/prompt.js +249 -91
  217. package/dist/src/redeploy/redeploy-policy.js +10 -10
  218. package/dist/src/redeploy/redeploy.js +6 -4
  219. package/dist/src/redeploy/redeployer.js +63 -55
  220. package/dist/src/redeploy/self-reload.js +1 -1
  221. package/dist/src/report/report-data.js +259 -254
  222. package/dist/src/report/report-fold.js +353 -0
  223. package/dist/src/report/report-render.js +235 -44
  224. package/dist/src/request-timeouts.js +1 -1
  225. package/dist/src/review/known-flakes.js +34 -14
  226. package/dist/src/review/protected-edits.js +4 -3
  227. package/dist/src/review/review-followup.js +16 -4
  228. package/dist/src/review/review-precheck.js +14 -13
  229. package/dist/src/review/review-verdict.js +14 -0
  230. package/dist/src/review/review.js +55 -34
  231. package/dist/src/review/secret-leaks.js +127 -0
  232. package/dist/src/review/suite-rerun.js +4 -3
  233. package/dist/src/roles/legacy-role-ids.js +21 -0
  234. package/dist/src/roles/legacy-role-migration.js +62 -0
  235. package/dist/src/roles/loop-ids.js +19 -2
  236. package/dist/src/roles/role-catalog.js +109 -131
  237. package/dist/src/roles/role-guidance.js +64 -31
  238. package/dist/src/roles/role-labels.js +10 -0
  239. package/dist/src/roles/role-render.js +194 -18
  240. package/dist/src/roles/role-view.js +216 -17
  241. package/dist/src/roles/roles.js +16 -14
  242. package/dist/src/scheduling/backoff.js +29 -19
  243. package/dist/src/scheduling/claims.js +15 -8
  244. package/dist/src/scheduling/once-round.js +11 -11
  245. package/dist/src/scheduling/quiet-hours.js +25 -32
  246. package/dist/src/scheduling/scheduling.js +28 -25
  247. package/dist/src/scheduling/{work-landed-cache.js → work-merged-cache.js} +14 -14
  248. package/dist/src/status/status-budget.js +115 -0
  249. package/dist/src/status/status-data.js +72 -184
  250. package/dist/src/status/status-inbox.js +47 -0
  251. package/dist/src/status/status-install.js +27 -0
  252. package/dist/src/status/status-polls.js +50 -39
  253. package/dist/src/status/status-roles.js +105 -0
  254. package/dist/src/{fleet → team}/error-storm.js +10 -10
  255. package/dist/src/{fleet → team}/failure-spread.js +8 -8
  256. package/dist/src/{fleet → team}/orchestrator-info.js +84 -18
  257. package/dist/src/{fleet → team}/reclaim.js +5 -4
  258. package/dist/src/{fleet/fleet-hold.js → team/team-hold.js} +55 -21
  259. package/dist/src/{fleet/fleet-polls.js → team/team-polls.js} +116 -65
  260. package/dist/src/{fleet/fleet-state.js → team/team-state.js} +39 -38
  261. package/dist/src/text/datetime.js +42 -2
  262. package/dist/src/text/format.js +10 -1
  263. package/dist/src/text/markdown.js +1 -1
  264. package/dist/src/text/phrases.js +35 -34
  265. package/dist/src/text/suggest.js +3 -2
  266. package/dist/src/text/text.js +2 -2
  267. package/dist/src/tick/qa-coverage.js +2 -1
  268. package/dist/src/tick/stage-check.js +142 -25
  269. package/dist/src/tick/telemetry-digest.js +10 -9
  270. package/dist/src/tick/tick-apply.js +101 -72
  271. package/dist/src/tick/tick-detail-data.js +23 -22
  272. package/dist/src/tick/tick-detail.js +37 -24
  273. package/dist/src/tick/tick-finalize.js +29 -22
  274. package/dist/src/tick/tick-outcome.js +2 -1
  275. package/dist/src/tick/tick-prompt.js +94 -26
  276. package/dist/src/tick/tick-resume.js +29 -13
  277. package/dist/src/tick/tick-stage.js +67 -53
  278. package/dist/src/tick/tick-timing.js +60 -59
  279. package/dist/src/tick/tick-usage.js +53 -23
  280. package/dist/src/tick/tick-verdict.js +75 -10
  281. package/dist/src/ui/badges.js +113 -69
  282. package/dist/src/ui/gui/gui-client-boot.js +202 -29
  283. package/dist/src/ui/gui/gui-client-composer.js +102 -23
  284. package/dist/src/ui/gui/gui-client-drawer.js +330 -75
  285. package/dist/src/ui/gui/gui-client-history.js +94 -37
  286. package/dist/src/ui/gui/gui-client-labels.js +24 -0
  287. package/dist/src/ui/gui/gui-client-loops.js +90 -47
  288. package/dist/src/ui/gui/gui-client-markdown.js +1 -0
  289. package/dist/src/ui/gui/gui-client-model.js +60 -43
  290. package/dist/src/ui/gui/gui-client-operator.js +73 -43
  291. package/dist/src/ui/gui/gui-client-pending.js +34 -31
  292. package/dist/src/ui/gui/gui-client-report.js +330 -47
  293. package/dist/src/ui/gui/gui-client-settings.js +126 -18
  294. package/dist/src/ui/gui/gui-client-sound.js +2 -1
  295. package/dist/src/ui/gui/{gui-client-fleet.js → gui-client-team.js} +190 -74
  296. package/dist/src/ui/gui/gui-client.js +26 -20
  297. package/dist/src/ui/gui/gui-page.js +18 -19
  298. package/dist/src/ui/gui/gui-styles.js +12 -4
  299. package/dist/src/ui/progress-data.js +5 -3
  300. package/dist/src/ui/status-model.js +176 -102
  301. package/dist/src/ui/status-payload.js +114 -36
  302. package/dist/src/ui/status-render.js +121 -52
  303. package/dist/src/ui/team-alerts.js +341 -0
  304. package/dist/src/ui/tick-progress-model.js +14 -14
  305. package/dist/src/ui/tone.js +9 -8
  306. package/dist/src/ui/transcript-tail.js +1 -0
  307. package/dist/src/ui/transcript.js +23 -16
  308. package/dist/src/ui/tui/tui-app.js +1 -0
  309. package/dist/src/ui/tui/tui-backlog.js +85 -11
  310. package/dist/src/ui/tui/tui-editor-answer.js +97 -0
  311. package/dist/src/ui/tui/tui-editor-attach.js +107 -0
  312. package/dist/src/ui/tui/tui-editor-brief.js +107 -0
  313. package/dist/src/ui/tui/tui-editor-budget.js +155 -0
  314. package/dist/src/ui/tui/tui-editor-ctx.js +1 -0
  315. package/dist/src/ui/tui/tui-editor-entry.js +230 -0
  316. package/dist/src/ui/tui/tui-editor-interval.js +122 -0
  317. package/dist/src/ui/tui/tui-editor-notes.js +114 -0
  318. package/dist/src/ui/tui/tui-editor-pause.js +266 -0
  319. package/dist/src/ui/tui/tui-editor-prompt.js +95 -0
  320. package/dist/src/ui/tui/tui-editor-queued.js +98 -0
  321. package/dist/src/ui/tui/tui-editor-quiet.js +106 -0
  322. package/dist/src/ui/tui/tui-editor-retire.js +83 -0
  323. package/dist/src/ui/tui/tui-editor-search.js +73 -0
  324. package/dist/src/ui/tui/tui-editor-session.js +97 -0
  325. package/dist/src/ui/tui/tui-editors.js +152 -0
  326. package/dist/src/ui/tui/tui-frame.js +61 -57
  327. package/dist/src/ui/tui/tui-history.js +12 -7
  328. package/dist/src/ui/tui/tui-input.js +44 -50
  329. package/dist/src/ui/tui/tui-keymap.js +1 -0
  330. package/dist/src/ui/tui/tui-keys.js +87 -1821
  331. package/dist/src/ui/tui/tui-pane-activity.js +99 -0
  332. package/dist/src/ui/tui/tui-pane-backlog.js +224 -0
  333. package/dist/src/ui/tui/tui-pane-brief.js +30 -0
  334. package/dist/src/ui/tui/tui-pane-ctx.js +11 -0
  335. package/dist/src/ui/tui/tui-pane-health.js +124 -0
  336. package/dist/src/ui/tui/tui-pane-history.js +316 -0
  337. package/dist/src/ui/tui/tui-pane-pending.js +74 -0
  338. package/dist/src/ui/tui/tui-pane-queued.js +143 -0
  339. package/dist/src/ui/tui/tui-pane-report.js +122 -0
  340. package/dist/src/ui/tui/tui-pane-session.js +101 -0
  341. package/dist/src/ui/tui/tui-pane-transcript.js +150 -0
  342. package/dist/src/ui/tui/tui-pane.js +52 -28
  343. package/dist/src/ui/tui/tui-panes.js +294 -0
  344. package/dist/src/ui/tui/tui.js +44 -26
  345. package/dist/src/verdict/fix-claim.js +56 -33
  346. package/dist/src/verdict/no-change.js +19 -0
  347. package/dist/src/verdict/refusal.js +43 -11
  348. package/dist/src/verdict/reply-contract.js +17 -5
  349. package/dist/src/version.js +3 -2
  350. package/package.json +4 -2
  351. package/dist/src/agent/mcp/role-notes-server.js +0 -160
  352. package/dist/src/landing/backlog-conflicts.js +0 -116
  353. package/dist/src/landing/landing-drain.js +0 -204
  354. package/dist/src/landing/landing-pipeline.js +0 -96
  355. package/dist/src/landing/landing-queue-render.js +0 -56
  356. package/dist/src/landing/landing-slot.js +0 -256
  357. package/dist/src/landing/landing-vetting.js +0 -196
  358. package/dist/src/pi-extension/role-notes.js +0 -40
  359. package/dist/src/ui/fleet-alerts.js +0 -174
@@ -1,200 +1,100 @@
1
- import path from "node:path";
2
1
  import { readSlotsState } from "../git/slots-state.js";
3
2
  import { openBugs, openQuestions } from "../backlog/backlog.js";
4
- import { enabledRoleIds, isCustomRole } from "../config/config.js";
5
- import { baseRoleOf, loopIds } from "../roles/loop-ids.js";
6
- import { modelFallbackView, modelSelectorField, resolveTierFallbacks, roleSeamTier, configForRole, reviewConfig, resolverConfig, tierFallbackLabels, tiersResolveDistinctPairs, } from "../config/config-views.js";
7
- import { claudeWindowEntries, readClaudeAccount } from "../agent/claude/claude-account.js";
8
- import { isJsonObject } from "../files/json-object.js";
9
- import { fallbackModelFree, fleetModelsFree, piModelsPath, piPairFree, readPiProviders } from "../pi/pi-models.js";
10
- import { configForStatus, queueInFlight, loopStateForPoll, mainCheckForPoll, maintenanceQuotaForPoll } from "./status-polls.js";
11
- import { queuedRolePromptEntries } from "../inbox/inbox.js";
12
- import { deliverableAt } from "../inbox/prompt-not-before.js";
3
+ import { baseRoleOf, loopIds, loopPaused } from "../roles/loop-ids.js";
4
+ import { piModelsPath } from "../pi/pi-models.js";
5
+ import { configForStatus, queueInFlight, mainCheckForPoll, maintenanceQuotaForPoll } from "./status-polls.js";
13
6
  import { quietHoursStatus, roleQuietHold } from "../scheduling/quiet-hours.js";
14
7
  import { DIRECTOR_ROLE } from "../roles/roles.js";
15
- import { pausedRoles, pausedRolesUntil, scheduledStopAt, scheduledWakes, standingFleetPause } from "../fleet/fleet-state.js";
16
- import { orchestratorAlive, readOrchestratorInfo, } from "../fleet/orchestrator-info.js";
17
- import { budgetReached, fleetDailyCost, modelPairName, projectCapHit } from "../budget/budget.js";
18
- import { roleCapVerdicts } from "../gates/role-cap-gates.js";
8
+ import { pausedRoles, pausedRolesUntil, scheduledStopAt, scheduledWakes, standingTeamPause } from "../team/team-state.js";
9
+ import { orchestratorAlive, readOrchestratorInfo, } from "../team/orchestrator-info.js";
19
10
  import { bootstrapHoldsRole, bootstrapStatus } from "../gates/bootstrap-gates.js";
20
- import { readLandingQueue } from "../landing/landing-queue.js";
11
+ import { readMergeQueue } from "../merge/merge-queue.js";
12
+ import { collectImageProbeFailures, collectRoleRows } from "./status-roles.js";
13
+ import { collectInboxes } from "./status-inbox.js";
14
+ import { collectBudget } from "./status-budget.js";
15
+ import { collectRootInstallDrift } from "./status-install.js";
21
16
  /** The earlier of the two optional stop deadlines (`run --for`'s and a scheduled stop's). */
22
17
  function earliestStop(a, b) {
23
18
  return a === undefined ? b : b === undefined ? a : Math.min(a, b);
24
19
  }
25
- /** The claude subscription-window block for a poll (see StatusSnapshot.claudeWindow): absent
26
- * unless the account reads subscription billing with at least one window AND a resolved seam
27
- * routes to claude. `only` is the free-ness classification the display needs: every paying seam
28
- * is a subscription claude seam exactly when no other seam is priced — a pi seam judged by
29
- * piPairFree, any other backend's billing unseen so it counts as paying too. The reviewer and
30
- * the strong-tier resolver are included, like fleetModelsFree, since they can spend. */
31
- function claudeWindowBlock(cfg, providers, root, now) {
32
- const account = readClaudeAccount(root, now);
33
- if (account?.billing !== "subscription")
34
- return undefined;
35
- const windows = claudeWindowEntries(account);
36
- if (windows.length === 0)
37
- return undefined;
38
- const seams = enabledRoleIds(cfg).map((role) => configForRole(cfg, role));
39
- if (cfg.review.enabled)
40
- seams.push(reviewConfig(cfg));
41
- if (seams.length > 0)
42
- seams.push(resolverConfig(cfg));
43
- if (!seams.some((s) => s.agent === "claude"))
44
- return undefined;
45
- const priced = seams.some((s) => {
46
- if (s.agent === undefined || s.agent === "pi")
47
- return !piPairFree(providers, s.provider, s.model);
48
- return s.agent !== "claude"; // a backend the account cannot bill is never subscription-paid
49
- });
50
- const heldUntil = windows
51
- .filter((w) => w.status === "rejected" && w.isUsingOverage !== true && typeof w.resetsAt === "number")
52
- .reduce((max, w) => Math.max(max, w.resetsAt), 0) || null;
53
- return { windows, primary: windows[0], only: !priced, heldUntil };
54
- }
55
- /** One fresh fleet snapshot for observers. `modelsPath` overrides pi's model definitions
20
+ /** One fresh team snapshot for observers. `modelsPath` overrides pi's model definitions
56
21
  * location (default ~/.pi/agent/models.json) — a test seam, like doctor's pathEnv. `now` pins
57
22
  * the poll's clock for the budget figures (the persisted day-stamp check and the burn-rate
58
23
  * projection) — one instant for the whole block, and the seam that keeps a test's expected
59
24
  * projection from racing the snapshot's own `Date.now()`; production callers take the default
60
- * and read the live clock, as always. */
25
+ * and read the live clock, as always. The three per-domain collectors (status-roles, status-
26
+ * inbox, status-budget) each take the shared reads and the pinned `now` as parameters, so
27
+ * every poll still reads a shared resource exactly once and the one `now` pins the whole
28
+ * snapshot — the consistency property the per-collector comments call out. */
61
29
  export function snapshot(root, modelsPath = piModelsPath(), now = Date.now()) {
62
30
  const cfg = configForStatus(root);
31
+ const rootDrift = collectRootInstallDrift(root, now);
63
32
  // The gate's read-only bootstrap verdict (plans/work-ratio.md, part 2/2), computed over the
64
33
  // same last-known-good config the caps read, so the dashboards show the hold the scheduler
65
34
  // enforces. Null when no bootstrap is configured.
66
35
  const bootstrapNow = bootstrapStatus(root, cfg);
67
36
  // One row per loop id (part 5b/7): an instance role shows `feature` and `feature-2`.
68
37
  const roles = loopIds(cfg);
38
+ const markerRoles = pausedRoles(root);
39
+ const markerSet = new Set(markerRoles);
69
40
  // One read of the orchestrator info file per poll: it serves both the displayed pid and the
70
41
  // liveness check (passing it to orchestratorAlive skips its own re-read).
71
42
  const info = readOrchestratorInfo(root);
72
43
  // One slots.json read per poll serves every row: the pooled slot a role leases (a running
73
- // tick or vet) or is pinned to.
44
+ // tick or verification) or is pinned to — threaded into the roles collector so `running`,
45
+ // `disk` and the rows agree.
74
46
  const slots = readSlotsState(root).slots;
75
- const loops = roles.map((r) => {
76
- const base = { ...loopStateForPoll(root, r), custom: isCustomRole(cfg, r) };
77
- const leased = slots.find((slot) => slot.lease?.role === r);
78
- const pinned = slots.find((slot) => slot.pinnedFor === r);
79
- const slot = leased ?? pinned;
80
- // Every row carries the selector its config resolves to, so the dashboards show which
81
- // model a role rides even with a single string `model`. A tier-name `roles.<id>.model`
82
- // and a role selector override both resolve through configForRole — the same view the
83
- // seam consumes. Part 7a (model-tiers.md "Observability"): the seam tier tag is added
84
- // only with a tier map declared, where it names the tier a role rides.
85
- const eff = configForRole(cfg, r);
86
- // Model failure fallback, part 2/2: while the role's episode is active its ticks run on
87
- // the tier-resolved fallback pair, so the row names that pair, the episode's start, and
88
- // the failure that tripped it. `now` is the poll's own clock (the snapshot seam), so
89
- // every row agrees on what "active" means.
90
- const episode = modelFallbackView(base.modelFallback, cfg, r, now);
91
- return {
92
- ...base,
93
- ...modelSelectorField(eff),
94
- ...(isJsonObject(cfg.model) ? { modelTier: roleSeamTier(cfg, r) } : {}),
95
- ...(episode !== null ? { fallback: episode } : {}),
96
- ...(slot ? { slot: path.basename(slot.dir), slotPinned: pinned !== undefined } : {}),
97
- ...(baseRoleOf(r) === r ? {} : { instanceOf: baseRoleOf(r) }),
98
- };
99
- });
100
- // One inbox pass per poll serves all four director fields (queuedRolePromptEntries lists
101
- // the directory and reads each file once): the count is the entries' length, so a prompt
102
- // enqueued or dequeued mid-snapshot can never make the header badge disagree with its
103
- // numbered previews, and each preview keeps its queue-file address beside it.
104
- const queued = queuedRolePromptEntries(root, DIRECTOR_ROLE);
105
- const inboxPrompts = queued.map((e) => e.preview);
106
- const inboxFiles = queued.map((e) => e.file);
107
- const inboxQueuedAt = queued.map((e) => e.queuedAtMs);
108
- const inboxNotBefore = queued.map((e) => e.notBeforeMs);
109
- // One directory listing pass per role per poll (below) fills the per-role counts; the
110
- // director is excluded because its queue is the shared inbox above.
111
- // A role with prompts queued also fills its cancel-addressable entries — the
112
- // stat-keyed prompt cache keeps an unchanged file at one stat per poll. Entries follow the
113
- // full list, deferred prompts included (the Queued tab lists and cancels them; only the
114
- // deliverable count above excludes them).
115
- const roleInbox = {};
116
- const roleInboxPrompts = {};
117
- // One listing pass per role serves both the deliverable count and the cancel-addressable
118
- // entries: queuedRolePromptRecords already carries each readable prompt's notBeforeMs, and
119
- // deliverableAt is exactly `notBeforeMs === null || notBeforeMs <= now` (prompt-not-before.ts)
120
- // over the same cached text — so the old second pass (queuedRolePromptCount's own readdir +
121
- // deliverability filter, once per role per poll across all 12 non-director loops) repeated
122
- // work the entries pass had just done. One instant — the snapshot's own pinned `now` — pins
123
- // the count and the entries to the same moment, so a caller that pins the clock sees
124
- // deliverability advance with it instead of the wall clock.
125
- for (const r of roles) {
126
- if (r === DIRECTOR_ROLE)
127
- continue;
128
- const entries = queuedRolePromptEntries(root, r);
129
- roleInbox[r] = entries.filter((e) => deliverableAt(e.notBeforeMs, now)).length;
130
- if (entries.length)
131
- roleInboxPrompts[r] = entries;
132
- }
47
+ const loops = collectRoleRows(root, cfg, now, slots, modelsPath);
48
+ // The doctor's cached image-probe failures (part 5/5): one read per snapshot, labelled, so
49
+ // the amber probe alert and `status --json` both answer from the same collector.
50
+ const imageProbeFailures = collectImageProbeFailures(root, now);
51
+ // One inbox pass per poll (status-inbox.ts) serves all four director fields and the per-role
52
+ // counts/entries from the same queuedRolePromptEntries reads.
53
+ const { inbox, inboxPrompts, inboxFiles, inboxQueuedAt, inboxNotBefore, roleInbox, roleInboxPrompts } = collectInboxes(root, roles, now);
133
54
  const running = orchestratorAlive(root, info);
134
- // One fleet-pause-marker read per poll serves both the paused flag and its deadline:
135
- // isFleetPaused + pausedUntil would each re-read the same marker file every second.
136
- const fleetPause = standingFleetPause(root);
137
- // One read of the pending scheduled wakes feeds both the earliest and the full list, so they cannot disagree.
55
+ // One team-pause-marker read per poll serves both the paused flag and its deadline:
56
+ // isTeamPaused + pausedUntil would each re-read the same marker file every second.
57
+ const teamPause = standingTeamPause(root);
58
+ // One read of the pending scheduled wakes feeds both the earliest and the full list, so they
59
+ // cannot disagree.
138
60
  const wakes = scheduledWakes(root, now);
139
- // One land-queue pass per poll (landing-queue.ts's stat cache keeps an unchanged queue at one
61
+ // One merge queue pass per poll (merge-queue.ts's stat cache keeps an unchanged queue at one
140
62
  // stat per file) serves the depth; inFlight is the 4/5 marker only when a live orchestrator
141
63
  // still has a queue entry for what the marker names (per change, for a batch) — the
142
64
  // cross-check makes every crash ordering self-healing (a stale marker alone never displays,
143
65
  // and needs no cleanup pass).
144
- const queue = readLandingQueue(root);
145
- const landings = queue.entries;
146
- const landQueue = {
147
- depth: landings.length + queue.unreadable,
66
+ const queue = readMergeQueue(root);
67
+ const merges = queue.entries;
68
+ const mergeQueue = {
69
+ depth: merges.length + queue.unreadable,
148
70
  };
149
- if (landings.length > 0) {
150
- landQueue.entries = landings.map((e) => ({
71
+ if (merges.length > 0) {
72
+ mergeQueue.entries = merges.map((e) => ({
151
73
  role: e.role,
152
74
  sha: e.sha,
153
- tick: e.tick,
75
+ run: e.run,
154
76
  summary: e.summary,
155
77
  enqueuedAt: e.enqueuedAt,
78
+ ...(e.revisionRound !== undefined ? { revisionRound: e.revisionRound } : {}),
79
+ ...(Array.isArray(e.priorReview?.reasons) && e.priorReview.reasons.length > 0 ? { priorObjections: e.priorReview.reasons.length } : {}),
156
80
  }));
157
81
  }
158
- const inFlight = queueInFlight(root, landings, running);
82
+ const inFlight = queueInFlight(root, merges, running);
159
83
  if (inFlight)
160
- landQueue.inFlight = inFlight;
161
- // The running orchestrator's published gate figures (read once above, with the pid and the
162
- // demotion): the scheduler's own sum over its live runner states. Only while running — the
163
- // exit removes the info file, so a stale file beside a dead pid must not speak for a fleet
164
- // whose persisted files are final.
165
- const publishedSpend = running && info?.budget ? info.budget.spentUsd : null;
166
- // The one materialized spend the budget block renders and the burn-rate projection both
167
- // read — the scheduler's published figure while running, the persisted states' sum otherwise.
168
- const spentUsd = publishedSpend ?? fleetDailyCost(loops, now);
84
+ mergeQueue.inFlight = inFlight;
169
85
  // Quiet hours 2/2 — one parse and one clock read per poll serve both the window string the
170
86
  // header badge renders and the in-window boolean the active reading turns on; the value is
171
87
  // read fresh from the same cached config load the budget block uses, so a live edit shows
172
88
  // on the next poll exactly when the gate applies it.
173
89
  const quiet = quietHoursStatus(cfg.quietHours, new Date());
174
- // The per-tier budget pause set (part 5c/8): the same resolution the scheduler's gate folds,
175
- // over the SAME usable predicate an observer can re-derive — the price check, and the pair
176
- // not among the running orchestrator's published per-pair demotions (a demotion lives in
177
- // the orchestrator's memory, so it publishes them; the legacy single-pair field
178
- // `fallbackDemoted` is the engaged pair's entry of the same map). Computed only while the
179
- // cap is reached — the hold never stands under an open gate — and the director exempt.
180
- const budgetReachedNow = budgetReached({ spentUsd, capUsd: cfg.maxDailyCostUsd });
181
- const providers = readPiProviders(modelsPath);
182
- // The persisted billing answer (6a/7), read once for every free-ness verdict below: a
183
- // subscription claude seam is free, so the badge and the pause set judge it as such.
184
- const account = readClaudeAccount(root);
185
- const publishedDemotions = info?.fallbackDemotions ?? {};
186
- const usable = (p) => piPairFree(providers, p.provider, p.model, account) && !(modelPairName(p) in publishedDemotions);
187
- const servingResolved = resolveTierFallbacks(cfg, usable);
188
- // The subscription usage window (part 6d/7): one account read per poll serves the block.
189
- const claudeWindow = claudeWindowBlock(cfg, providers, root, now);
190
- const budgetPausedRoles = budgetReachedNow
191
- ? roles.filter((r) => r !== DIRECTOR_ROLE && servingResolved[roleSeamTier(cfg, r)].pair === null)
192
- : [];
90
+ // The spend/billing block (status-budget.ts): the budget object, the claude window, and the
91
+ // two per-role pause verdicts, all from one readPiProviders and one readClaudeAccount per poll.
92
+ const { budget, claudeWindow, budgetPausedRoles, capPaused } = collectBudget(root, cfg, loops, roles, running, info, modelsPath, now);
193
93
  return {
194
94
  running,
195
95
  pid: info?.pid,
196
96
  build: running && info?.build ? info.build : null,
197
- inbox: inboxPrompts.length,
97
+ inbox,
198
98
  inboxPrompts,
199
99
  inboxFiles,
200
100
  inboxQueuedAt,
@@ -202,42 +102,26 @@ export function snapshot(root, modelsPath = piModelsPath(), now = Date.now()) {
202
102
  roleInbox,
203
103
  roleInboxPrompts,
204
104
  questions: openQuestions(root).length,
105
+ imageProbeFailures,
205
106
  loops,
206
- // Unconditional (never null): a disabled fleet still shows its spend and the badge is
107
+ // Unconditional (never null): a disabled team still shows its spend and the badge is
207
108
  // the affordance for SETTING a cap. models.json itself is stat-cached inside pi-models.ts,
208
109
  // so an unchanged catalog costs one stat per poll, not a re-read plus parse.
209
- budget: {
210
- spentUsd,
211
- capUsd: cfg.maxDailyCostUsd,
212
- // One projection per poll, from the same spend the field above renders: null exactly
213
- // when no forecast may be stated (no cap, no spend, cap reached, burn misses midnight).
214
- capHitAt: projectCapHit({ spentUsd, capUsd: cfg.maxDailyCostUsd }, now),
215
- free: fleetModelsFree(cfg, modelsPath, account),
216
- agent: cfg.agent ?? "pi",
217
- // Null unless the gate could actually engage it (configured AND priced at zero AND not
218
- // demoted by the running orchestrator's breaker): a fallback the scheduler would refuse
219
- // must not be advertised as one that will save the fleet — and a demoted one is refused
220
- // until its probe serves, so the dashboards read `budget paused` exactly while the
221
- // scheduler is (BUGS.md 2026-09-20). The pair is the price-aware default tier's
222
- // resolution, so a map with no `default` entry names the pair the gate actually runs.
223
- fallback: fallbackModelFree(cfg, modelsPath, account) && !(running && info?.fallbackDemoted)
224
- ? (servingResolved.default.pair ?? null)
225
- : null,
226
- tiers: budgetReachedNow && tiersResolveDistinctPairs(servingResolved)
227
- ? tierFallbackLabels(servingResolved)
228
- : null,
229
- },
110
+ budget,
230
111
  ...(claudeWindow ? { claudeWindow } : {}),
231
- paused: fleetPause !== null,
232
- pausedRoles: pausedRoles(root),
233
- capPaused: [...roleCapVerdicts(loops.map((l) => ({ role: l.role, state: l })), cfg.maxDailyCostUsdPerRole, now).values()]
234
- .filter((v) => v.over)
235
- .flatMap((v) => v.roleIds),
112
+ ...(running && info?.chain && Object.keys(info.chain).length > 0 ? { chain: info.chain } : {}),
113
+ // Only while running: a stale info file beside a dead pid must not speak for a stopped
114
+ // team, and only while a hold stands (an empty list is the same as none).
115
+ ...(running && info?.providerHolds && info.providerHolds.length > 0 ? { providerHolds: info.providerHolds } : {}),
116
+ paused: teamPause !== null,
117
+ pausedRoles: markerRoles,
118
+ pausedLoops: roles.filter((id) => loopPaused(markerSet, id)),
119
+ capPaused,
236
120
  // The maintenance allowance (plans/work-ratio.md, part 1b/4): the same pure verdict the
237
121
  // gate folds, over the same event log, so the dashboards show the hold the scheduler
238
- // enforces. `landings` is the one land-queue snapshot read above, so in-flight maintenance
239
- // landings count once here and in landQueue.
240
- maintenanceQuota: maintenanceQuotaForPoll(root, cfg.maintenancePerWorkLanding, loops, new Set(landings.map((e) => e.role)), now),
122
+ // enforces. `merges` is the one merge queue snapshot read above, so in-flight maintenance
123
+ // merges count once here and in mergeQueue.
124
+ maintenanceQuota: maintenanceQuotaForPoll(root, cfg.maintenancePerWorkMerge, loops, new Set(merges.map((e) => e.role)), now),
241
125
  // The new-project bootstrap hold (plans/work-ratio.md, part 2/2): the same read-only
242
126
  // verdict the gate computes (bootstrapStatus), so the dashboards and the scheduler agree.
243
127
  // The held set is computed with the gate's own predicate over the same loop list.
@@ -249,28 +133,32 @@ export function snapshot(root, modelsPath = piModelsPath(), now = Date.now()) {
249
133
  : [],
250
134
  budgetPausedRoles,
251
135
  // The per-role quiet windows' held roles (PLANS.md quietHoursPerRole): keyed role →
252
- // window, so the dashboards can render the same `quiet until <end>` wording the fleet
136
+ // window, so the dashboards can render the same `quiet until <end>` wording the team
253
137
  // badge uses, scoped to the loop's own schedule. The window string comes back out of
254
138
  // the same config the hold was decided from, so the label cannot name a window that is
255
139
  // not holding (roleQuietHold true implies a parseable, non-empty string there).
256
140
  roleQuietPaused: Object.fromEntries(loops
257
141
  .filter((l) => l.role !== DIRECTOR_ROLE && roleQuietHold(cfg.quietHoursPerRole, baseRoleOf(l.role), new Date()))
258
142
  .map((l) => [l.role, cfg.quietHoursPerRole?.[baseRoleOf(l.role)] ?? ""])),
259
- pausedUntil: fleetPause?.until,
143
+ pausedUntil: teamPause?.until,
260
144
  pausedRolesUntil: pausedRolesUntil(root),
261
145
  scheduledWake: wakes[0],
262
146
  scheduledWakes: wakes.length > 0 ? wakes : undefined,
263
- pauseReason: fleetPause?.reason,
147
+ pauseReason: teamPause?.reason,
264
148
  quietHours: quiet.window ?? undefined,
265
149
  inQuietHours: quiet.inWindow,
266
- landQueue,
150
+ mergeQueue,
267
151
  mainCheck: mainCheckForPoll(root, cfg),
268
152
  // The disk floor's published state (plans/disk-floor.md, part 4/4): only while the
269
153
  // orchestrator runs — the exit removes the info file, so a stale file beside a dead pid
270
- // must not speak for a stopped fleet — and only when the last poll could measure the
154
+ // must not speak for a stopped team — and only when the last poll could measure the
271
155
  // volume. Absent otherwise, so the surfaces render exactly as before.
272
156
  disk: running && info?.disk ? info.disk : undefined,
273
- // Only while running: a stale info file beside a dead pid must not speak for a stopped fleet.
157
+ // Independent of the team running: a stopped team's worktrees still borrow the root install.
158
+ rootInstallDrift: rootDrift.length ? rootDrift : undefined,
159
+ // Only while running: a stale info file beside a dead pid must not speak for a stopped team.
160
+ permits: running && info?.permits ? info.permits : undefined,
161
+ // Only while running: a stale info file beside a dead pid must not speak for a stopped team.
274
162
  runStopsAt: running ? earliestStop(info?.stopsAt, scheduledStopAt(root)) : undefined,
275
163
  scheduledStopAt: running ? scheduledStopAt(root) : undefined,
276
164
  };
@@ -0,0 +1,47 @@
1
+ import { queuedRolePromptEntries } from "../inbox/inbox.js";
2
+ import { deliverableAt } from "../inbox/prompt-not-before.js";
3
+ import { DIRECTOR_ROLE } from "../roles/roles.js";
4
+ /** One inbox pass per poll serves all four director fields (queuedRolePromptEntries lists the
5
+ * directory and reads each file once): the count is the deliverable entries' length, so a
6
+ * prompt enqueued or dequeued mid-snapshot can never make the header badge disagree with its
7
+ * numbered previews, and each preview keeps its queue-file address beside it. A second
8
+ * listing pass per role (all non-director loops) fills the per-role counts and their
9
+ * cancel-addressable entries from the same `queuedRolePromptEntries` records, with
10
+ * deliverableAt (prompt-not-before.ts) over the same cached text. `roles` is the
11
+ * already-computed loop list so one `loopIds` read serves the caller and both passes; `now`
12
+ * is the snapshot's pinned clock, so deliverability and the counts advance together instead
13
+ * of racing the wall clock. */
14
+ export function collectInboxes(root, roles, now) {
15
+ const queued = queuedRolePromptEntries(root, DIRECTOR_ROLE);
16
+ const inboxPrompts = queued.map((e) => e.preview);
17
+ const inboxFiles = queued.map((e) => e.file);
18
+ const inboxQueuedAt = queued.map((e) => e.queuedAtMs);
19
+ const inboxNotBefore = queued.map((e) => e.notBeforeMs);
20
+ // The director and the per-role queues count queued prompts by the one rule — deliverableAt
21
+ // (prompt-not-before.ts) over the same cached text — so a deferred prompt never inflates
22
+ // either the header badge or a per-role marker while it is still listed and cancelable.
23
+ // One directory listing pass per role per poll fills the per-role counts; the director is
24
+ // excluded because its queue is the shared inbox above. A role with prompts queued also
25
+ // fills its cancel-addressable entries — the stat-keyed prompt cache keeps an unchanged
26
+ // file at one stat per poll. Entries follow the full list, deferred prompts included (the
27
+ // Queued tab lists and cancels them; only the deliverable count above excludes them).
28
+ const roleInbox = {};
29
+ const roleInboxPrompts = {};
30
+ for (const r of roles) {
31
+ if (r === DIRECTOR_ROLE)
32
+ continue;
33
+ const entries = queuedRolePromptEntries(root, r);
34
+ roleInbox[r] = entries.filter((e) => deliverableAt(e.notBeforeMs, now)).length;
35
+ if (entries.length)
36
+ roleInboxPrompts[r] = entries;
37
+ }
38
+ return {
39
+ inbox: queued.filter((e) => deliverableAt(e.notBeforeMs, now)).length,
40
+ inboxPrompts,
41
+ inboxFiles,
42
+ inboxQueuedAt,
43
+ inboxNotBefore,
44
+ roleInbox,
45
+ roleInboxPrompts,
46
+ };
47
+ }
@@ -0,0 +1,27 @@
1
+ // vocabulary: plain
2
+ import { installDrift } from "../build/dep-install.js";
3
+ /** How long a computed drift answer is reused. The answer depends on two files (the lockfile and
4
+ * each installed manifest), so it is throttled by time rather than keyed by stat: the dashboard
5
+ * polls every second and must not re-parse a large lockfile each time. */
6
+ const ROOT_INSTALL_POLL_MS = 60_000;
7
+ /** Safety cap, as in files/stat-cache.ts: many short-lived roots (tests) cannot grow the map
8
+ * unbounded. */
9
+ const MAX_ROOTS_CACHED = 64;
10
+ const cache = new Map();
11
+ /** Direct dependencies `root`'s node_modules does not hold at package-lock.json's pin (the
12
+ * doctor's `root install` row), recomputed at most once per ROOT_INSTALL_POLL_MS per root.
13
+ * `[]` when there is no readable lockfile. The caller owns the returned array. */
14
+ export function collectRootInstallDrift(root, now) {
15
+ const hit = cache.get(root);
16
+ if (hit && now - hit.at < ROOT_INSTALL_POLL_MS)
17
+ return [...hit.drift];
18
+ const drift = installDrift(root);
19
+ if (cache.size >= MAX_ROOTS_CACHED)
20
+ cache.clear();
21
+ cache.set(root, { at: now, drift });
22
+ return [...drift];
23
+ }
24
+ /** Forget every cached answer (tests). */
25
+ export function resetRootInstallCache() {
26
+ cache.clear();
27
+ }