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