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,19 +1,31 @@
1
- import { openBugs, openQuestions, plannedPlans } from "../backlog/backlog.js";
2
- import { backlogMarks } from "../backlog/backlog-eligibility.js";
1
+ // vocabulary: plain
2
+ import { answeredQuestionSummaries } from "../backlog/question-answer.js";
3
+ import { completionDate, donePlanEntries, DONE_DATE_RE, FIXED_DATE_RE, fixedBugEntries, openBugs, openQuestions, plannedPlans } from "../backlog/backlog.js";
4
+ import { backlogHolds, backlogInProgress, backlogMarks, refusedEntryTitles } from "../backlog/backlog-eligibility.js";
5
+ import { finishedTitle, groupBlockedSeries, partsLabel } from "../backlog/backlog-structure.js";
3
6
  import { readEvents } from "../events/event-read.js";
4
7
  import { eventFeedItem, formatEvent } from "../events/event-format.js";
5
8
  import { projectName } from "../project-name.js";
6
9
  import { dailyCost } from "../budget/budget.js";
10
+ import { queueEntryMergeLabel } from "../merge/merge-queue-render.js";
7
11
  import { snapshot } from "../status/status-data.js";
8
- import { bootstrapBadge, buildBadge, budgetBadge, diskBadge, landingBadge, mainCheckBadge, mainCountsFragment, quietBadge } from "./badges.js";
9
- import { isActivePhase, loopRowCells } from "./status-model.js";
12
+ import { bootstrapBadge, buildBadge, budgetBadge, diskBadge, mergeBadge, mainCheckBadge, mainCountsFragment, permitsBadge, quietBadge, runWindowBadge } from "./badges.js";
13
+ import { isActivePhase, loopRowCells, onErrorStreak } from "./status-model.js";
10
14
  import { yieldMultiplierFor } from "../scheduling/backoff.js";
11
- import { fleetAlerts } from "./fleet-alerts.js";
15
+ import { teamAlerts } from "./team-alerts.js";
12
16
  /** How many recent events the payload carries — enough for the dashboard's activity feed to
13
- * still show a screenful of notable events (landings, failures, questions) after it filters
17
+ * still show a screenful of notable events (merges, failures, questions) after it filters
14
18
  * out the routine ones (tick starts, wakes, passing checks). */
15
19
  const RECENT_EVENTS = 80;
16
- /** The one fleet-state document both observer surfaces carry: `GET /api/status` (gui/gui-server.ts)
20
+ /** How many answered questions the dashboard lists under the open ones (a default, not a knob). */
21
+ export const DASHBOARD_ANSWERED_QUESTIONS = 5;
22
+ /** How many done plans / fixed bugs the dashboard lists under the open ones (a default). */
23
+ export const DASHBOARD_RECENT_FINISHED = 5;
24
+ /** The newest finished entries (sections are newest first), titles cleaned of their stamp. */
25
+ export function recentFinished(entries, dateRe) {
26
+ return entries.slice(0, DASHBOARD_RECENT_FINISHED).map((e) => ({ title: finishedTitle(e.title, dateRe), completedOn: completionDate(e.title, dateRe) }));
27
+ }
28
+ /** The one team-state document both observer surfaces carry: `GET /api/status` (gui/gui-server.ts)
17
29
  * spreads it and adds the serving process's own `serverBuildSha` (the page's cue to notice a
18
30
  * newer build and reload), while `tumwater status --json` (cli.ts) prints it verbatim — every
19
31
  * other field is shared, so the dashboard and the CLI can never drift apart. Assembled here —
@@ -34,13 +46,31 @@ export function statusPayload(root, now = Date.now()) {
34
46
  // the prompt's backlog index and `tumwater backlog` render, projected to the mark each
35
47
  // surface appends. Read once here, consumed as the parallel planHolds/bugHolds arrays below.
36
48
  const marks = backlogMarks(root);
37
- const loops = snap.loops.map((s) => {
49
+ const holds = backlogHolds(root);
50
+ const plans = plannedPlans(root);
51
+ // Which blocked plans fold into a series row: the grouping `plan --list` and the prompt's
52
+ // backlog index use, as a slot per plan (null for a plan that stays an ordinary row).
53
+ const working = backlogInProgress(root);
54
+ const planSeries = plans.map(() => null);
55
+ for (const g of groupBlockedSeries(holds.plans.map((hold, i) => ({ i, title: plans[i], hold, claimer: working.plans[i] ?? undefined })))) {
56
+ if (g.kind !== "series")
57
+ continue;
58
+ const slot = { name: g.series, label: `${g.series}, ${g.rows.length} blocked parts (${partsLabel(g.rows)})` };
59
+ for (const r of g.rows)
60
+ planSeries[r.i] = slot;
61
+ }
62
+ const roles = snap.loops.map((s) => {
38
63
  // loopRowCells single-homes the per-loop tail read + metrics + phase derivation the
39
64
  // TUI table (status-render.ts's renderStatus) repeats, so the two dashboards cannot
40
65
  // drift apart.
41
66
  const { live, generated, peakCtx, phase } = loopRowCells(snap, root, s);
42
67
  return {
43
68
  role: s.role,
69
+ // The loop's own failure-streak tell (onErrorStreak, status-model.ts): the red
70
+ // "failing tick after tick" alert keys on this rather than the rendered phase, so it
71
+ // stays up while the loop's next tick runs (phase reads "working") and clears only
72
+ // when the streak resets (BUGS.md 2026-10-14).
73
+ errorStreak: onErrorStreak(s),
44
74
  // User-defined-loop marker (computed in snapshot — see StatusSnapshot.loops): the GUI
45
75
  // renders it as an asterisk beside the loop name.
46
76
  custom: s.custom,
@@ -50,28 +80,42 @@ export function statusPayload(root, now = Date.now()) {
50
80
  // the loop name and the selector in the sub line.
51
81
  ...(s.modelTier ? { modelTier: s.modelTier } : {}),
52
82
  ...(s.model ? { model: s.model } : {}),
83
+ // pi's own saved default (see StatusSnapshot.loops.modelFromPi): ships beside `model` so
84
+ // the dashboard can mark a row whose model tumwater.json did not name. Same conditional
85
+ // spread as `model`, present on exactly the rows that carry the flag.
86
+ ...(s.modelFromPi ? { modelFromPi: true } : {}),
53
87
  // Model failure fallback (PLANS.md "Model failure fallback, part 2/2"): present only
54
88
  // while the loop's ticks run off-model, so the common row shape is unchanged.
55
89
  ...(s.fallback ? { fallback: s.fallback } : {}),
56
90
  ...(s.slot ? { slot: s.slot, slotPinned: s.slotPinned === true } : {}),
57
91
  // Parallel work instances (plans/parallel-work-instances.md "Observability", part
58
92
  // 6/7): the backlog entry this instance claimed, shown by the dashboards while it is
59
- // idle or landing, and the base role it belongs to (`feature-2` → `feature`). `claim`
93
+ // idle or merge, and the base role it belongs to (`feature-2` → `feature`). `claim`
60
94
  // is the claimed heading text, not the persisted record; both are absent on a bare,
61
95
  // unclaimed row so the common payload shape is unchanged.
62
96
  ...(s.claim ? { claim: s.claim.title } : {}),
63
97
  ...(s.instanceOf ? { instanceOf: s.instanceOf } : {}),
98
+ // The loop's image-input surface (plans/image-input.md "Surfaces", part 5/5), always
99
+ // present: whether its own model reads images and where a blind prompt reroutes — the
100
+ // GUI's images alert and composer notice read them (the JSON field is `imageReroute`, the
101
+ // display label, not the persisted handoff state).
102
+ ...(s.imageInput !== undefined ? { imageInput: s.imageInput } : {}),
103
+ ...(s.imageRerouteLabel !== undefined ? { imageReroute: s.imageRerouteLabel } : {}),
64
104
  phase,
65
105
  // In-flight flag derived from the same rendered phase (isActivePhase's
66
- // permit-holder prefixes: working/reviewing/landing). The running director is excluded
106
+ // permit-holder prefixes: working/reviewing/merge). The running director is excluded
67
107
  // (it holds no maxConcurrent permit), so this is the cap count, not the abort set — the
68
108
  // GUI's row actions derive their own live flag from the phase instead (BUGS.md
69
109
  // 2026-10-06).
70
110
  inFlight: isActivePhase(phase),
111
+ // The stuck alert's silence span (ms since the last pi log write, capped to the tick by
112
+ // progressOfTick) — the dashboards render it as "for <duration>" on a tool-call stall.
113
+ // Present only while the loop holds a live tail, so the common payload shape is unchanged.
114
+ ...(live?.quietMs !== undefined ? { stuckMs: live.quietMs } : {}),
71
115
  // What a working loop is doing right now (first assistant text of the in-flight run).
72
116
  // Null when idle — never show a stale item from a finished tick.
73
117
  currentWork: live?.currentWork ?? null,
74
- ticks: s.ticks,
118
+ runs: s.runs,
75
119
  commits: s.commits,
76
120
  generated,
77
121
  peakCtx,
@@ -81,12 +125,16 @@ export function statusPayload(root, now = Date.now()) {
81
125
  todayUsd: dailyCost(s),
82
126
  lastResult: s.lastResult ?? null,
83
127
  lastSummary: s.lastSummary ?? null,
84
- // The last failure's text (a tick error, a failed landing's reason); the dashboard shows it
128
+ // The last failure's text (a tick error, a failed merge's reason); the dashboard shows it
85
129
  // beside a problem result that carries no summary of its own.
86
130
  lastError: s.lastError ?? null,
87
- lastTickEndedAt: s.lastTickEndedAt ?? null,
131
+ lastRunEndedAt: s.lastRunEndedAt ?? null,
132
+ // When the loop last actually merged a change on main (epoch ms; null until it ever
133
+ // does) — the direct answer to "how long has the dry spell been", the productivity
134
+ // complement to lastError/lastRunEndedAt.
135
+ lastMergedAt: s.lastMergedAt ?? null,
88
136
  // The scheduling facts behind the `next run` column: raw epoch ms and seconds, formatted
89
- // client-side like lastTickEndedAt/costUsd — the GUI's fmtNextRun twin applies the same
137
+ // client-side like lastRunEndedAt/costUsd — the GUI's fmtNextRun twin applies the same
90
138
  // rules nextRunCell does (see status-render.ts).
91
139
  nextRunAt: s.nextRunAt,
92
140
  backoffSeconds: s.backoffSeconds,
@@ -94,6 +142,12 @@ export function statusPayload(root, now = Date.now()) {
94
142
  // clocks, PLANS.md): the payload carries the computed number, not the ring, so the
95
143
  // GUI's JS twin applies the same display rule without re-deriving the role sets.
96
144
  yieldMultiplier: yieldMultiplierFor(s),
145
+ // The consecutive no-change streak (tick-apply.ts applyTickOutcome's noChangeStreak, an
146
+ // observational counter): how many completed ticks in a row returned nothing to do. The
147
+ // TUI reads it off the rendered label; the GUI's phaseDetailBase re-derives the ` · no
148
+ // change ×N` suffix from this number (its row and drawer share noChangeStreak) so its
149
+ // JS twin applies the same display rule without re-parsing the phase text.
150
+ noChangeStreak: s.noChangeStreak ?? 0,
97
151
  };
98
152
  });
99
153
  return {
@@ -110,21 +164,34 @@ export function statusPayload(root, now = Date.now()) {
110
164
  // not be re-derived client-side where it could drift from the TUI header.
111
165
  buildBadge: buildBadge(snap.build),
112
166
  // The header's daily-cost-budget badge preformatted through badges.ts's budgetBadge —
113
- // the same string the TUI/status table renders (n/a for an all-free fleet; `· no cap`
167
+ // the same string the TUI/status table renders (n/a for an all-free team; `· no cap`
114
168
  // when disabled). Sent display-ready like buildBadge so the page cannot re-derive it.
115
- budgetBadge: budgetBadge(snap.budget, snap.claudeWindow),
169
+ budgetBadge: budgetBadge(snap.budget, snap.claudeWindow, now, snap.chain),
116
170
  // The disk floor (plans/disk-floor.md, part 4/4): the raw block is machine-readable for
117
171
  // `status --json`, and the header badge is preformatted through badges.ts's diskBadge —
118
172
  // the same string the TUI/status header renders. Omitted (not null) when no orchestrator
119
173
  // has published one — the same omit-undefined idiom as pausedUntil.
120
174
  ...(snap.disk ? { disk: snap.disk } : {}),
121
- diskBadge: diskBadge(snap.disk, now),
122
- // The land queue (plans/merge-queue.md 4/5): depth always (machine-readable for
123
- // `status --json`; 0 when idle) plus the in-flight landing's identity only while one is
175
+ diskBadge: diskBadge(snap.disk),
176
+ // The permit pool (plans/provider-concurrency.md §8): raw block machine-readable for
177
+ // `status --json`, plus the display-ready header badge — the same `slots 5/8 · omlx 3/3`
178
+ // string the TUI/status header renders. Omitted when no orchestrator has published one.
179
+ ...(snap.permits ? { permits: snap.permits } : {}),
180
+ permitsBadge: permitsBadge(snap.permits),
181
+ // The `run --for` deadline: raw epoch ms (only when windowed) plus the display-ready badge.
182
+ ...(snap.runStopsAt !== undefined ? { runStopsAt: snap.runStopsAt } : {}),
183
+ ...(snap.scheduledStopAt !== undefined ? { scheduledStopAt: snap.scheduledStopAt } : {}),
184
+ runWindowBadge: runWindowBadge(snap.runStopsAt, now),
185
+ // The merge queue (plans/merge-queue.md 4/5): depth always (machine-readable for
186
+ // `status --json`; 0 when idle) plus the in-flight merge's identity only while one is
124
187
  // actually running. Raw data here, like budget — and the display-ready header badge
125
- // preformatted through badges.ts's landingBadge, so the page cannot re-derive it.
126
- landQueue: snap.landQueue,
127
- landingBadge: landingBadge(snap.landQueue),
188
+ // preformatted through badges.ts's mergeBadge, so the page cannot re-derive it.
189
+ mergeQueue: snap.mergeQueue,
190
+ mergeBadge: mergeBadge(snap.mergeQueue),
191
+ // One bracketed merge label per merge queue entry (queueEntryMergeLabel, "" for none),
192
+ // parallel to mergeQueue.entries — display-ready like mergeBadge, since browser JS cannot
193
+ // import the TypeScript stage labels.
194
+ mergeQueueLabels: (snap.mergeQueue.entries ?? []).map((e) => queueEntryMergeLabel(e.sha, snap.mergeQueue.inFlight, now)),
128
195
  // Main's newest merge-scope check: the
129
196
  // raw block is machine-readable for `status --json`, and the header badge is
130
197
  // preformatted through badges.ts's mainCheckBadge — the same string the TUI/status
@@ -167,14 +234,18 @@ export function statusPayload(root, now = Date.now()) {
167
234
  // the preformatted `budgetBadge`.
168
235
  budget: snap.budget,
169
236
  // The subscription usage-window block (part 6d/7), absent on a pi-only or API-claude
170
- // fleet — the same raw shape `status --json` carries, which the GUI's spend card reads
237
+ // team — the same raw shape `status --json` carries, which the GUI's spend card reads
171
238
  // to render the window (the preformatted budgetBadge above already folds it into the
172
239
  // header for the TUI/status). Raw data, like budget.
173
240
  ...(snap.claudeWindow ? { claudeWindow: snap.claudeWindow } : {}),
241
+ // Each tier's fallback step (fallback chain part 7c/7), raw for the GUI side card.
242
+ ...(snap.chain ? { chain: snap.chain } : {}),
174
243
  // Operator pause (`tumwater pause` marker present): every idle role loop's phase reads
175
244
  // `paused` ahead of budget/main-red — one flag covers both dashboards through loopPhase.
176
245
  paused: snap.paused,
177
246
  pausedRoles: snap.pausedRoles,
247
+ // The loop ids the scheduler treats as paused by the marker; the dashboards read this.
248
+ pausedLoops: snap.pausedLoops,
178
249
  // The per-role cap's held roles (part 2/2): an idle
179
250
  // loop in this set reads `cap paused` — the loopPhase ladder names it, and `status
180
251
  // --json` carries the raw set beside pausedRoles so scripts read the same verdict.
@@ -194,12 +265,13 @@ export function statusPayload(root, now = Date.now()) {
194
265
  // window, machine-readable for `status --json` beside capPaused; the dashboards render
195
266
  // the loop's own `quiet until <end>` from it through loopPhase.
196
267
  roleQuietPaused: snap.roleQuietPaused,
197
- // The fleet marker's standing timed-pause deadline (ms epoch), absent when the fleet is
268
+ // The team marker's standing timed-pause deadline (ms epoch), absent when the team is
198
269
  // not timed-paused (undefined drops from the JSON). Raw data, like paused/pausedRoles —
199
270
  // machine-readable for `status --json`; the planned GUI pause-badge countdown reads it
200
271
  // client-side.
201
272
  pausedUntil: snap.pausedUntil,
202
- // The paused-roles marker's shared `until` while a timed per-role pause stands (omitted otherwise).
273
+ // The paused-roles marker's shared `until` while a timed per-role pause stands (omitted
274
+ // otherwise).
203
275
  pausedRolesUntil: snap.pausedRolesUntil,
204
276
  // A pending scheduled wake ({ roles, atMs }); omitted when none stands.
205
277
  scheduledWake: snap.scheduledWake,
@@ -217,7 +289,7 @@ export function statusPayload(root, now = Date.now()) {
217
289
  ...(snap.quietHours ? { quietHours: snap.quietHours } : {}),
218
290
  inQuietHours: snap.inQuietHours,
219
291
  quietBadge: quietBadge(snap.quietHours, snap.inQuietHours),
220
- loops,
292
+ roles,
221
293
  // The recent event feed as the one-line text `tumwater logs` prints…
222
294
  events: recent.map((e) => formatEvent(e)),
223
295
  // …and the same events as data for the dashboard's activity feed: when, which loop, what
@@ -227,16 +299,31 @@ export function statusPayload(root, now = Date.now()) {
227
299
  // Project status (planned features + open bugs + open questions), fresh per poll like
228
300
  // events — loops edit these files constantly, so there is no cache to go stale. The page
229
301
  // derives the header badge count from this list's length.
230
- plans: plannedPlans(root),
302
+ plans,
231
303
  bugs: openBugs(root),
232
- // The holds beside those titles, index-for-index parallel (backlogMarks and plannedPlans/
233
- // openBugs walk the same sectionEntries file order): holdMark renders an eligible entry as
234
- // the empty string, so a client can append the mark without a separate eligibility check.
304
+ // The holds and working-on-it marks beside those titles, index-for-index parallel
305
+ // (backlogMarks and plannedPlans/openBugs walk the same sectionEntries file order): an
306
+ // eligible entry nobody is working on is the empty string, so a client can append the
307
+ // mark without a separate eligibility check.
235
308
  planHolds: marks.plans,
236
309
  bugHolds: marks.bugs,
310
+ // Whether each entry carries any eligibility hold (parallel to plans/bugs): the dashboard
311
+ // lists unblocked rows first and gathers the rest under a Blocked divider.
312
+ planBlocked: holds.plans.map((h) => h !== null),
313
+ bugBlocked: holds.bugs.map((h) => h !== null),
314
+ // The series each blocked plan folds into (parallel to plans): the Plans tab shows one
315
+ // expandable row per series instead of every waiting part; null for an ordinary row.
316
+ planSeries,
237
317
  questions,
238
- // What needs the operator (fleet-alerts.ts's fleetAlerts): the dashboard's alert banners and
318
+ // The newest answered questions, newest first, read fresh each poll like the other backlog
319
+ // lists. Alerts and badges keep counting open questions only.
320
+ answeredQuestions: answeredQuestionSummaries(root, DASHBOARD_ANSWERED_QUESTIONS),
321
+ // The newest done plans and fixed bugs, newest first: cleaned titles only, no bodies, read
322
+ // fresh each poll. Badges and alerts keep counting open entries only.
323
+ donePlans: recentFinished(donePlanEntries(root), DONE_DATE_RE),
324
+ fixedBugs: recentFinished(fixedBugEntries(root), FIXED_DATE_RE),
325
+ // What needs the operator (team-alerts.ts's teamAlerts): the dashboard's alert banners and
239
326
  // the TUI's attention lines, phrased once.
240
- alerts: fleetAlerts(snap, questions, loops, now),
327
+ alerts: teamAlerts(snap, questions, roles, now, refusedEntryTitles(root)),
241
328
  };
242
329
  }
@@ -1,17 +1,22 @@
1
1
  import { dailyCost } from "../budget/budget.js";
2
2
  import { clipToWidth, displayWidth } from "../text/text-width.js";
3
3
  import { compactTokens, usd } from "../text/format.js";
4
- import { elapsedSeconds, formatTime, humanSeconds, pad2, secondsUntil } from "../text/datetime.js";
4
+ import { elapsedSeconds, formatTime, humanSeconds, nextRunPhrase, pad2, secondsUntil } from "../text/datetime.js";
5
5
  import { projectName } from "../project-name.js";
6
- import { bootstrapBadge, buildBadge, budgetBadge, claudeWindowTone, diskBadge, landingBadge, mainCheckBadge, mainCheckVerdict, pauseBadge, loopWakeMarker, quietBadge, wakeBadge } from "./badges.js";
7
- import { idleClaimText, isActivePhase, loopRowCells, sortLoopsByState } from "./status-model.js";
6
+ import { bootstrapBadge, buildBadge, budgetBadge, claudeWindowTone, diskBadge, mergeBadge, mainCheckBadge, mainCheckVerdict, pauseBadge, loopWakeMarker, permitsBadge, quietBadge, runWindowBadge, wakeBadge } from "./badges.js";
7
+ import { idleClaimText, isActivePhase, loopRowCells, onErrorStreak, sortLoopsByState } from "./status-model.js";
8
8
  import { tickProgress } from "./tick-progress-model.js";
9
- import { yieldMultiplierFor } from "../scheduling/backoff.js";
9
+ import { eventTypeLabel } from "../events/event-labels.js";
10
+ import { roleLabel } from "../roles/role-labels.js";
11
+ import { yieldMultiplierFor, yieldMultiplierSuffix } from "../scheduling/backoff.js";
10
12
  import { phaseTone, resultTone } from "./tone.js";
13
+ import { teamAlerts } from "./team-alerts.js";
14
+ import { openQuestions } from "../backlog/backlog.js";
15
+ import { refusedEntryTitles } from "../backlog/backlog-eligibility.js";
11
16
  /** The status RENDER layer: time/token cell formatters and the width-aware table shared by
12
17
  * `tumwater status` and the TUI. The labels, badges, and metrics it draws come from the shared
13
18
  * display model (status-model.ts); this module decides column widths and layout. Depends on
14
- * status/status-data.ts one way — rendering reads the snapshot; it never collects fleet state
19
+ * status/status-data.ts one way — rendering reads the snapshot; it never collects team state
15
20
  * itself (live tick detail is display-only). */
16
21
  function ago(ts) {
17
22
  if (!ts)
@@ -33,32 +38,35 @@ export function lastTickCell(ts) {
33
38
  s = `${pad2(d.getMonth() + 1)}-${pad2(d.getDate())} ${s}`;
34
39
  return `${s} · ${ago(ts)}`;
35
40
  }
41
+ /** The table's `last merged` cell: when the loop last merged a change on main, in the SAME
42
+ * absolute-time-plus-relative-age shape as the `last tick` cell (lastTickCell), so the two
43
+ * columns cannot drift on what "ago" means. A loop that has never merged shows the same dash
44
+ * lastTickCell uses for a never-ticked loop. */
45
+ export function lastMergedCell(ts) {
46
+ return lastTickCell(ts);
47
+ }
36
48
  /** The table's `next run` cell: when the loop may tick again, or `-` when that is not the
37
- * operator's question — a loop in flight (an active rendered phase: working/reviewing/landing,
38
- * or `s.running`) has no next run to speak of, and a stopped fleet renders no schedule at all
49
+ * operator's question — a loop in flight (an active rendered phase: working/reviewing/merge,
50
+ * or `s.running`) has no next run to speak of, and a stopped team renders no schedule at all
39
51
  * (its `nextRunAt`s are stale leftovers, not plans). A due idle loop reads `now`; a future one
40
52
  * reads its remaining time through humanSeconds (the same bucketing the sleeping phase label
41
53
  * uses, so the two cannot drift), prefixed `backoff ` while `backoffSeconds > 0` — the wake
42
54
  * row-action clears exactly that, so the prefix is what makes waking meaningful. The GUI
43
55
  * renders the same rules from its own JS copy in gui-client.ts (fmtNextRun). The `phase` and
44
- * `fleetRunning` arguments come from the caller because neither fact lives on LoopState: the
45
- * active-phase check needs the rendered label (a landing is visible only there), and the
46
- * fleet's running flag lives on the snapshot, not the loop. */
47
- export function nextRunCell(s, phase, now, fleetRunning) {
48
- if (!fleetRunning || s.running || isActivePhase(phase))
56
+ * `teamRunning` arguments come from the caller because neither fact lives on LoopState: the
57
+ * active-phase check needs the rendered label (a merge is visible only there), and the
58
+ * team's running flag lives on the snapshot, not the loop. */
59
+ export function nextRunCell(s, phase, now, teamRunning) {
60
+ if (!teamRunning || s.running || isActivePhase(phase))
49
61
  return "-";
50
62
  const remain = secondsUntil(s.nextRunAt, now);
51
63
  // The yield multiplier rides the cell as `×N` (yield-scaled clocks, PLANS.md): a scalable
52
- // role whose recent ticks landed nothing keeps a longer effective gap than nextRunAt's
64
+ // role whose recent ticks merged nothing keeps a longer effective gap than nextRunAt's
53
65
  // countdown shows, and the suffix is what makes that visible. The multiplier gates the
54
66
  // scheduled gap and the main-moved wake in isEligible, so a `now`-due cell at ×4 is a
55
67
  // role waiting out its stretched gap, not a broken clock.
56
- const mult = yieldMultiplierFor(s);
57
- const suffix = mult > 1 ? ` ×${mult}` : "";
58
- if (remain <= 0)
59
- return `now${suffix}`;
60
- const label = humanSeconds(remain);
61
- return (s.backoffSeconds > 0 ? `backoff ${label}` : label) + suffix;
68
+ const suffix = yieldMultiplierSuffix(yieldMultiplierFor(s));
69
+ return nextRunPhrase(remain, s.backoffSeconds > 0, "now", false) + suffix;
62
70
  }
63
71
  /** The table's state cell: a loop's phase label (loopPhase, computed once per row by
64
72
  * renderStatus so the same label drives the row order), with the current work item prepended
@@ -70,11 +78,11 @@ export function nextRunCell(s, phase, now, fleetRunning) {
70
78
  function stateCell(root, s, phase, live) {
71
79
  // While under review the log tail's "current work" is the reviewer's own output, not the
72
80
  // author's task — don't prepend it; the phase cell already carries the reviewer's live
73
- // detail. The landing label rides the same guard: the landing role is not running, so its
74
- // phase ("landing <elapsed> · <stage>", with the reviewer's live detail while it reviews)
81
+ // detail. The merge label rides the same guard: the merge role is not running, so its
82
+ // phase ("merge <elapsed> · <stage>", with the reviewer's live detail while it reviews)
75
83
  // is returned without a work-item prefix.
76
84
  if (!s.running || s.parkedSince || s.phase === "review") {
77
- // Parallel work instances, part 6/7: an idle or landing instance leads with the entry it
85
+ // Parallel work instances, part 6/7: an idle or merge instance leads with the entry it
78
86
  // claimed (or, for an idle extra with none, the no-unclaimed note), so the operator can
79
87
  // see WHAT it is parked on without opening its drawer.
80
88
  const idle = idleClaimText(s, phase);
@@ -87,11 +95,13 @@ function stateCell(root, s, phase, live) {
87
95
  /** Columns allowed to shrink when the table is wider than the terminal, widest offender
88
96
  * first: `last result` (holds the tick summary), then `state` (live working detail), then
89
97
  * `last tick` — it shrinks last so on a narrow terminal it loses " · 3m ago" before whole
90
- * lines clip; its minimum is a bare HH:MM:SS. Indices are positional in the `cols` array
91
- * below — renumber when columns change. (`today`, like `cost`, is a short fixed-width cell:
92
- * never flexible.) */
98
+ * lines clip; its minimum is a bare HH:MM:SS. `last merged` sits right of `last tick` and is
99
+ * not flexible: it is a short sibling time cell whose width follows lastTickCell's, and the
100
+ * whole-line clip that already protects the header also bounds it. Indices are positional in
101
+ * the `cols` array below — renumber when columns change. (`today`, like `cost`, is a short
102
+ * fixed-width cell: never flexible.) */
93
103
  const FLEXIBLE_COLUMNS = [
94
- { index: 9, minWidth: 12 },
104
+ { index: 10, minWidth: 12 },
95
105
  { index: 1, minWidth: 12 },
96
106
  { index: 8, minWidth: 10 },
97
107
  ];
@@ -124,7 +134,35 @@ export function clipSpans(line, width) {
124
134
  }
125
135
  return out;
126
136
  }
127
- /** A header badge (" · land queue: 2", ", build abc…") as a plain separator plus its toned
137
+ /** The attention line's mark and tone, per alert tone — shared by the TUI and `tumwater
138
+ * status` (status-render.ts alertLines), so the two surfaces draw the same banner glyphs. */
139
+ const ALERT_MARKS = {
140
+ red: { mark: "!", tone: "red" },
141
+ amber: { mark: "!", tone: "yellow" },
142
+ indigo: { mark: "?", tone: "magenta" },
143
+ blue: { mark: "i", tone: "blue" },
144
+ gray: { mark: "i", tone: "dim" },
145
+ };
146
+ /** A plain line clipped to a width, in one tone. */
147
+ export function toneLine(text, width, tone) {
148
+ return clipSpans([{ text, ...(tone ? { tone } : {}) }], width);
149
+ }
150
+ /** The attention lines under a header: one per alert — its mark and title in its tone, then
151
+ * the detail — at most `max` of them, the rest counted on a last line. This is the banner
152
+ * block the TUI draws under its header and `tumwater status` pushes under its own, so the two
153
+ * surfaces render the same wording from the one shared alert source (team-alerts.ts). */
154
+ export function alertLines(alerts, width, max = 3) {
155
+ const shown = alerts.length > max ? alerts.slice(0, max - 1) : alerts;
156
+ const lines = shown.map((a) => {
157
+ const { mark, tone } = ALERT_MARKS[a.tone];
158
+ const t = tone ? { tone } : {};
159
+ return clipSpans([{ text: `${mark} `, ...t }, { text: a.title, ...t }, { text: a.detail ? ` — ${a.detail}` : "" }], width);
160
+ });
161
+ if (shown.length < alerts.length)
162
+ lines.push(toneLine(` +${alerts.length - shown.length} more — the dashboard lists them all`, width, "dim"));
163
+ return lines;
164
+ }
165
+ /** A header badge (" · merge queue: 2", ", build abc…") as a plain separator plus its toned
128
166
  * words, so the TUI colors the words and not the dots. Empty badges add nothing. */
129
167
  function badgeSpans(badge, tone) {
130
168
  if (!badge)
@@ -157,27 +195,32 @@ export function renderStatusSpans(root, snap, maxWidth) {
157
195
  // The questions badge (like the inbox one) appears only when something needs an answer.
158
196
  // The budget badge is standing information for a money-spending system in EVERY cap state
159
197
  // (disabled reads `· no cap` — and it is the affordance for editing the cap); on narrow
160
- // terminals the header's existing last-resort whole-line clipping applies. A fleet whose
198
+ // terminals the header's existing last-resort whole-line clipping applies. A team whose
161
199
  // models are all free reads n/a — spend can never accumulate against a cap that cannot be
162
200
  // reached, so a dollar figure would mislead. The timed-pause badge (plans/pause-countdown
163
- // wording) rides last so a fleet under a timed pause reads how long until it comes back —
164
- // empty unless the fleet marker's deadline stands in the future, so a role-only or
201
+ // wording) rides last so a team under a timed pause reads how long until it comes back —
202
+ // empty unless the team marker's deadline stands in the future, so a role-only or
165
203
  // indefinite pause keeps today's header byte-identical.
166
204
  const running = snap.running
167
205
  ? [{ text: "running", tone: "green" }, { text: ` (pid ${snap.pid}` }, ...badgeSpans(buildBadge(snap.build), snap.build?.stale ? "yellow" : undefined), { text: ")" }]
168
- : [{ text: "not running — start with `tumwater run`", tone: "dim" }];
206
+ : [{ text: "not running — start with `tumwater start`", tone: "dim" }];
169
207
  lines.push([
170
208
  { text: "tumwater", tone: "brand" },
171
209
  { text: ` · ${name} · ` },
172
210
  ...running,
173
- ...badgeSpans(landingBadge(snap.landQueue), "blue"),
211
+ ...(snap.running ? badgeSpans(runWindowBadge(snap.runStopsAt, now), "blue") : []),
212
+ ...badgeSpans(mergeBadge(snap.mergeQueue), "blue"),
174
213
  ...(snap.inbox ? [{ text: ` · inbox: ${snap.inbox}` }] : []),
175
214
  ...(snap.questions ? badgeSpans(` · questions: ${snap.questions}`, "magenta") : []),
176
- ...badgeSpans(budgetBadge(snap.budget, snap.claudeWindow), budgetTone(snap.budget, snap.claudeWindow)),
215
+ ...badgeSpans(budgetBadge(snap.budget, snap.claudeWindow, now, snap.chain), budgetTone(snap.budget, snap.claudeWindow)),
177
216
  // The disk floor (plans/disk-floor.md, part 4/4): `disk 8.2 GB free — holding new work`
178
217
  // while the hold is on, or the bare reading below the reclaim threshold. Empty in every
179
218
  // comfortable or unmeasured state, so an unaffected header stays byte-identical.
180
- ...badgeSpans(diskBadge(snap.disk, now), snap.disk?.held ? "yellow" : "blue"),
219
+ ...badgeSpans(diskBadge(snap.disk), snap.disk?.held ? "yellow" : "blue"),
220
+ // The permit pool's live state (plans/provider-concurrency.md §8): the team line reads
221
+ // `slots 5/8 · omlx 3/3` only while the per-provider cap map is set (permitsBadge already
222
+ // omits the providers on a map-less team, so the header stays byte-identical).
223
+ ...badgeSpans(permitsBadge(snap.permits), "blue"),
181
224
  // New-project bootstrap (plans/work-ratio.md, part 2/2): the progress reading while the
182
225
  // hold is active. Empty in every normal project, so the header stays byte-identical.
183
226
  ...badgeSpans(bootstrapBadge(snap.bootstrap), "blue"),
@@ -199,9 +242,10 @@ export function renderStatusSpans(root, snap, maxWidth) {
199
242
  // or missing, so loops that never ticked — or last ticked yesterday — read zero without a
200
243
  // save. It renders whether or not the cap is enabled: spend observability does not depend
201
244
  // on it.
202
- // `next run` is appended last and never flexible (a short fixed-width cell like `today`):
203
- // FLEXIBLE_COLUMNS' positional indices above stay untouched when columns change.
204
- const cols = ["loop", "state", "ticks", "commits", "gen", "peak ctx", "cost", "today", "last tick", "last result", "next run"];
245
+ // `next run` is a short fixed-width cell like `today`, never flexible: the columns that
246
+ // give way when the table is wider than the terminal are `last result`, then `state`, then
247
+ // `last tick`, in FLEXIBLE_COLUMNS' order.
248
+ const cols = ["role", "state", "runs", "commits", "output", "peak ctx", "cost", "today", "last run", "last merged", "last result", "next run"];
205
249
  // One live tail read per running loop per frame, threaded through every cell that shows
206
250
  // in-flight detail (metrics, state, current work) — each helper used to re-read the log on
207
251
  // its own, up to three stats + reads per loop per second. The phase is computed once per
@@ -225,7 +269,7 @@ export function renderStatusSpans(root, snap, maxWidth) {
225
269
  live: cells.live,
226
270
  phase: cells.phase,
227
271
  role: s.role,
228
- lastTickEndedAt: s.lastTickEndedAt,
272
+ lastRunEndedAt: s.lastRunEndedAt,
229
273
  };
230
274
  });
231
275
  // User-defined loops (snapshot's `custom` flag) get an asterisk beside their name — the
@@ -244,12 +288,12 @@ export function renderStatusSpans(root, snap, maxWidth) {
244
288
  const modelSuffix = s.modelTier
245
289
  ? ` (${s.modelTier}${s.model ? ` · ${s.model}` : ""})`
246
290
  : s.model
247
- ? ` (${s.model})`
291
+ ? ` (${s.model}${s.modelFromPi ? ", pi default" : ""})`
248
292
  : "";
249
293
  // Model failure fallback, part 2/2: while the loop runs off-model its name cell says so,
250
294
  // so the operator can see which loops the fallback pair is carrying without reading logs.
251
- const fallbackSuffix = s.fallback ? " (on fallback)" : "";
252
- const name = (s.custom ? `${s.role}*` : s.role) + modelSuffix + fallbackSuffix;
295
+ const fallbackSuffix = s.fallback ? " (over-cap model)" : "";
296
+ const name = (s.custom ? `${roleLabel(s.role)}*` : roleLabel(s.role)) + modelSuffix + fallbackSuffix;
253
297
  const state = stateCell(root, s, phase, live);
254
298
  const tone = phaseTone(phase);
255
299
  // The whole state cell carries the phase's tone: a work item leads the cell (so it
@@ -260,20 +304,21 @@ export function renderStatusSpans(root, snap, maxWidth) {
260
304
  const resultT = resultTone(s.lastResult);
261
305
  return [
262
306
  cell(name),
263
- // Merge queue 4/5 — the landing role's phase label (the marker-driven record, filtered to
264
- // this role by loopPhase's `landing` argument above) rides this same phase string.
307
+ // Merge queue 4/5 — the merge role's phase label (the marker-driven record, filtered to
308
+ // this role by loopPhase's `merge` argument above) rides this same phase string.
265
309
  [...stateSpans, { text: roleQueued(s.role) }, ...(wakeMark ? [{ text: wakeMark, tone: "blue" }] : [])],
266
- cell(String(s.ticks)),
310
+ cell(String(s.runs)),
267
311
  cell(String(s.commits)),
268
312
  cell(compactTokens(m.generated)),
269
313
  cell(compactTokens(m.peakCtx)),
270
314
  cell(usd(s.totalCostUsd)),
271
315
  cell(usd(dailyCost(s))),
272
- cell(lastTickCell(s.lastTickEndedAt)),
316
+ cell(lastTickCell(s.lastRunEndedAt)),
317
+ cell(lastMergedCell(s.lastMergedAt)),
273
318
  s.lastResult
274
- ? [{ text: s.lastResult, ...(resultT ? { tone: resultT } : {}) }, { text: s.lastSummary ? ` — ${s.lastSummary}` : "" }]
319
+ ? [{ text: eventTypeLabel(s.lastResult), ...(resultT ? { tone: resultT } : {}) }, { text: s.lastSummary ? ` — ${s.lastSummary}` : "" }]
275
320
  : cell("-"),
276
- cell(next, next.startsWith("backoff") ? "yellow" : undefined),
321
+ cell(next, next.startsWith("sleeping") ? "yellow" : undefined),
277
322
  ];
278
323
  });
279
324
  const totalsRow = [
@@ -284,7 +329,7 @@ export function renderStatusSpans(root, snap, maxWidth) {
284
329
  compactTokens(withMetrics.reduce((sum, { m }) => sum + m.generated, 0)),
285
330
  compactTokens(Math.max(0, ...withMetrics.map(({ m }) => m.peakCtx))),
286
331
  usd(snap.loops.reduce((sum, s) => sum + s.totalCostUsd, 0)),
287
- // The fleet's today-spend, from the snapshot's budget block — the same number the header
332
+ // The team's today-spend, from the snapshot's budget block — the same number the header
288
333
  // badge reads, so table and badge cannot drift. While the orchestrator runs that figure is
289
334
  // the scheduler's live sum over its in-memory states, which can sit ABOVE the per-loop
290
335
  // `today` cells above (each loop's persisted copy, written only at its saves): the gap is
@@ -295,6 +340,7 @@ export function renderStatusSpans(root, snap, maxWidth) {
295
340
  "",
296
341
  "",
297
342
  "",
343
+ "",
298
344
  ].map((text) => cell(text));
299
345
  const allRows = [...rows, totalsRow];
300
346
  // Widths are measured in terminal display columns (displayWidth), not UTF-16 code units:
@@ -344,6 +390,18 @@ export function renderStatusSpans(root, snap, maxWidth) {
344
390
  }
345
391
  return out;
346
392
  };
393
+ const loops = withMetrics.map(({ s, phase, live }) => ({
394
+ role: s.role,
395
+ phase,
396
+ errorStreak: onErrorStreak(s),
397
+ inFlight: isActivePhase(phase),
398
+ ...(live?.quietMs !== undefined ? { stuckMs: live.quietMs } : {}),
399
+ ...(s.lastError ? { lastError: s.lastError } : {}),
400
+ // The loop's image-input surface (plans/image-input.md "Surfaces", part 5/5): always
401
+ // present, so the TUI's images attention line keys on it like the dashboard's.
402
+ imageInput: s.imageInput,
403
+ imageReroute: s.imageRerouteLabel,
404
+ }));
347
405
  const separator = [{ text: widths.map((w) => "-".repeat(w)).join(" "), tone: "dim" }];
348
406
  lines.push(fmt(cols.map((c) => cell(c)), "dim"));
349
407
  lines.push(separator);
@@ -352,10 +410,9 @@ export function renderStatusSpans(root, snap, maxWidth) {
352
410
  lines.push(separator);
353
411
  lines.push(fmt(totalsRow, "bold"));
354
412
  // One footnote under the table when any custom loop exists — explains the asterisk without
355
- // taking a column. Absent (byte-identical table) on a fleet with no user-defined loops.
413
+ // taking a column. Absent (byte-identical table) on a team with no user-defined loops.
356
414
  if (snap.loops.some((l) => l.custom))
357
- lines.push([{ text: "* user-defined loop", tone: "dim" }]);
358
- const loops = withMetrics.map(({ s, phase }) => ({ role: s.role, phase, inFlight: isActivePhase(phase), ...(s.lastError ? { lastError: s.lastError } : {}) }));
415
+ lines.push([{ text: "* custom role", tone: "dim" }]);
359
416
  // The header line (and any residual overflow past the columns' minimums) is clipped too,
360
417
  // so no status line ever wraps in a terminal of `maxWidth` columns.
361
418
  return { lines: maxWidth === undefined ? lines : lines.map((l) => clipSpans(l, maxWidth)), loops };
@@ -364,5 +421,18 @@ export function renderStatusSpans(root, snap, maxWidth) {
364
421
  * renderStatusSpans). When `maxWidth` is given, wide cells are clipped so no line exceeds it
365
422
  * (terminal rows never wrap). */
366
423
  export function renderStatus(root, snap, maxWidth) {
367
- return renderStatusSpans(root, snap, maxWidth).lines.map(plainText).join("\n");
424
+ const { lines, loops } = renderStatusSpans(root, snap, maxWidth);
425
+ // The team-attention banners (team-alerts.ts) drop under the header's blank line here, on
426
+ // the CLI-only plain-text surface — the same spelled-out banners the dashboard and TUI draw,
427
+ // from the one shared source, so the CLI/GUI/TUI cannot drift on wording. One QUESTIONS.md
428
+ // read (openQuestions) for the pending-questions alert — the dashboard's cadence. The CLI
429
+ // omits the not-running banner: the header's first line already spells out the team's state,
430
+ // so a `status` run on a stopped team does not lead with a banner that duplicates it.
431
+ // Composed here, NOT inside renderStatusSpans: the TUI re-slices that function's shared
432
+ // `lines` (lines[0], its own attention block, then lines.slice(1)), so a banner embedded
433
+ // there would render twice in the TUI.
434
+ const alerts = teamAlerts(snap, openQuestions(root), loops, Date.now(), refusedEntryTitles(root)).filter((a) => a.key !== "stopped");
435
+ const banner = alertLines(alerts, maxWidth ?? Number.MAX_SAFE_INTEGER, alerts.length).map(plainText);
436
+ const text = lines.map(plainText);
437
+ return [...text.slice(0, 2), ...banner, ...text.slice(2)].join("\n");
368
438
  }