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
@@ -0,0 +1,341 @@
1
+ // vocabulary: plain
2
+ /** The team ALERT model: everything about the team that needs the operator — or that they
3
+ * should know — phrased once for both observer surfaces: the dashboard's alert banners
4
+ * (shipped in the status payload as `alerts`) and the TUI's attention lines
5
+ * (tui-frame.ts alertLines). Split from status-model.ts, whose per-loop derivation this reads
6
+ * as input; `tone` ranks an
7
+ * alert (red and amber ask for action, indigo asks a question, blue and gray inform), and
8
+ * `actions` are the dashboard's buttons (`act` names a page action — open a loop, a view,
9
+ * the cap editor…). */
10
+ import { stripEntryStamp } from "../backlog/backlog-structure.js";
11
+ import { quietWindowEnd } from "../scheduling/quiet-hours.js";
12
+ import { chainFragments, pauseCountdown } from "./badges.js";
13
+ import { fallbackTierEntries } from "../budget/budget.js";
14
+ import { fallbackDisplayPair } from "../config/config-views.js";
15
+ import { agree, pauseReasonSuffix, plural } from "../text/phrases.js";
16
+ import { STALL_RE, duration } from "./tick-progress-model.js";
17
+ import { formatGB, usd, usdCap } from "../text/format.js";
18
+ import { formatTimestamp, humanUntil } from "../text/datetime.js";
19
+ import { roleLabel } from "../roles/role-labels.js";
20
+ /** The dashboard's "Open <role>" buttons for an alert about a set of loops: one button per
21
+ * loop (capped to the first three so the banner does not overflow), each opening that loop's
22
+ * pane. Shared by the failing, stuck, and no-image-target alerts, so the button shape and its
23
+ * cap cannot drift between them. */
24
+ function openRoleActions(loops) {
25
+ return loops.slice(0, 3).map((l) => ({ label: `Open ${roleLabel(l.role)}`, act: "role", arg: l.role }));
26
+ }
27
+ /** Natural English list of the given names: “a and b” for one or two, “a, b, and c”
28
+ * beyond that. Shared by the loop-row and held-id banners so the wording cannot drift. */
29
+ function andJoin(names) {
30
+ return names.length <= 2 ? names.join(" and ") : `${names.slice(0, -1).join(", ")}, and ${names[names.length - 1]}`;
31
+ }
32
+ function listRoles(loops) {
33
+ return andJoin(loops.map((l) => roleLabel(l.role)));
34
+ }
35
+ /** The `listRoles` rendering for a set of held role ids (status-data.ts's `capPaused` and
36
+ * `maintenanceQuota.held`, the scheduler's enabled-but-held sets rather than rendered loop
37
+ * rows): names every held id through roleLabel even when a stale id matches no live loop, so
38
+ * the role-cap and maintenance banners never drop a held role from their copy. */
39
+ function listRoleIds(ids) {
40
+ return andJoin(ids.map(roleLabel));
41
+ }
42
+ /** The stuck alert's per-loop detail: the stall phrase the phase rendered, plus — when that
43
+ * phrase does not already carry one — the honest silence span from the live tail (stuckMs,
44
+ * ms since the loop's last pi log write, capped to the tick by progressOfTick). A
45
+ * "no agent output for <d>" phrase already carries its duration, so only a tool-call stall gets
46
+ * a " for <duration>" suffix, and only when the caller holds a live tail (stuckMs present). */
47
+ function stuckDetail(l) {
48
+ const cause = (STALL_RE.exec(l.phase)?.[0] ?? "").trim();
49
+ if (cause && !cause.startsWith("no agent output for") && l.stuckMs !== undefined) {
50
+ return `${roleLabel(l.role)}: ${cause} for ${duration(l.stuckMs)}`;
51
+ }
52
+ return `${roleLabel(l.role)}: ${cause}`;
53
+ }
54
+ /** ISO instants in server text (a restart cooldown's deadline) in local time. */
55
+ function localizeInstants(text) {
56
+ return text.replace(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z/g, (iso) => formatTimestamp(Date.parse(iso)));
57
+ }
58
+ /** Everything about the team that needs the operator, most urgent first: a spent budget, a red
59
+ * main, loops failing tick after tick, loops that look stuck, a stale running build, open
60
+ * questions, refused entries, a pause, a stopped team. `now` is passed so a timed pause's
61
+ * countdown agrees with the rest of the frame. */
62
+ export function teamAlerts(snap, questions, loops, now, refused = []) {
63
+ const out = [];
64
+ const b = snap.budget;
65
+ const onFallback = chainFragments(snap.chain);
66
+ if (onFallback) {
67
+ out.push({
68
+ key: "chain",
69
+ tone: "blue",
70
+ title: "Running on fallback",
71
+ detail: `running on fallback: ${onFallback}`,
72
+ actions: [],
73
+ });
74
+ }
75
+ if (!b.free && b.capUsd > 0 && b.spentUsd >= b.capUsd) {
76
+ // Tiered fallbacks (part 7b/8): the snapshot's tier map stands exactly when the
77
+ // tiers resolve to two or more distinct pairs, so list them and let the borrowed
78
+ // ones' `(from …)` suffixes mark the borrow; a tier-free snapshot keeps the
79
+ // single-pair sentence.
80
+ const fallbackDetail = b.tiers
81
+ ? `${fallbackTierEntries(b.tiers).join(", ")} carry them at no cost until midnight.`
82
+ : `${fallbackDisplayPair(b.fallback, "The fallback")} carries them at no cost until midnight.`;
83
+ out.push(b.fallback
84
+ ? {
85
+ key: "fallback",
86
+ tone: "blue",
87
+ title: "Today's budget is spent — roles switched to the over-cap model",
88
+ // The roles whose tier has no usable free pair (gate-polls.ts's budgetPausedRoles)
89
+ // don't move: they sit on `budget paused` rows while the banner says everyone
90
+ // switched. One sentence names both halves of the split so the spent-cap banner
91
+ // doesn't read as if every role kept working.
92
+ detail: snap.budgetPausedRoles.length > 0
93
+ ? `${fallbackDetail} ${listRoleIds(snap.budgetPausedRoles)} ${agree(snap.budgetPausedRoles.length, "has", "have")} no free pair and ${agree(snap.budgetPausedRoles.length, "stays", "stay")} paused until local midnight.`
94
+ : fallbackDetail,
95
+ actions: [{ label: "Raise the cap", act: "budget" }],
96
+ }
97
+ : {
98
+ key: "budget",
99
+ tone: "red",
100
+ title: "Today's budget is spent, so the roles are paused",
101
+ detail: `Spent ${usd(b.spentUsd)} of the ${usdCap(b.capUsd)} cap. They resume at midnight, or as soon as you raise the cap.`,
102
+ actions: [{ label: "Raise the cap", act: "budget" }],
103
+ });
104
+ }
105
+ // A role over its maxDailyCostUsdPerRole cap (status-model.ts "waiting: daily cap spent") is
106
+ // a per-role hold — one role sits idle while the rest of the team keeps working — so the team
107
+ // banner names it the way the budget, disk, and quiet banners name theirs, instead of leaving
108
+ // an operator to read every row. An amber ask (it needs a raise to resume); clears on its own
109
+ // at local midnight when the spend window rolls over.
110
+ const capPausedLoops = loops.filter((l) => snap.capPaused.includes(l.role));
111
+ if (snap.capPaused.length > 0) {
112
+ out.push({
113
+ key: "role-cap",
114
+ tone: "amber",
115
+ title: `${listRoleIds(snap.capPaused)} ${agree(snap.capPaused.length, "is over its daily cap", "are over their daily caps")}`,
116
+ detail: `The daily cap (maxDailyCostUsdPerRole) holds ${listRoleIds(snap.capPaused)}: no new runs until local midnight. Raise the cap to resume ${agree(snap.capPaused.length, "it", "them")}.`,
117
+ actions: openRoleActions(capPausedLoops),
118
+ });
119
+ }
120
+ // The disk floor holding new work (plans/disk-floor.md, part 4/4): the header badge and the
121
+ // loop rows say what is happening, but the banner names the threshold and the way out, so an
122
+ // operator hitting a full volume does not have to read events. An amber ask — the team is
123
+ // stopped and needs room, not merely informed. Only while the hold is on; below the reclaim
124
+ // threshold but not yet held is the badge's quieter business.
125
+ if (snap.disk?.held) {
126
+ out.push({
127
+ key: "disk",
128
+ tone: "amber",
129
+ title: `Waiting: low disk — the team is holding new work at ${formatGB(snap.disk.freeGB)} free`,
130
+ detail: `Free space is below the ${snap.disk.holdGB} GB floor. Build outputs are reclaimed first; new work starts again once space recovers.`,
131
+ actions: [],
132
+ });
133
+ }
134
+ // The root checkout's node_modules is behind package-lock.json (a failed post-merge re-sync):
135
+ // checks install into their own trees until it is fixed. Clears on drift, so a hand fix
136
+ // clears it too.
137
+ const drift = snap.rootInstallDrift ?? [];
138
+ if (drift.length) {
139
+ const shown = drift.slice(0, 5).join(", ");
140
+ const names = drift.length > 5 ? `${shown}, +${drift.length - 5} more` : shown;
141
+ out.push({
142
+ key: "install",
143
+ tone: "amber",
144
+ title: "The root install is behind package-lock.json",
145
+ detail: `${plural(drift.length, "direct dependency", "direct dependencies")} not installed as pinned (${names}). Run npm install in the project's root checkout; until then each check installs into its own tree, and \`tumwater doctor\` names the last re-sync failure.`,
146
+ actions: [],
147
+ });
148
+ }
149
+ const red = loops.filter((l) => l.phase === "main's check is failing");
150
+ if (red.length) {
151
+ out.push({
152
+ key: "mainred",
153
+ tone: "red",
154
+ title: `main's check is failing — ${plural(red.length, "role is", "roles are")} blocked`,
155
+ detail: "Code roles stop authoring until main's check passes, and pick up on their own once it does.",
156
+ actions: [{ label: "See failures", act: "view", arg: "failures" }],
157
+ });
158
+ }
159
+ // A loop whose own failure streak is at or past the alarm threshold (onErrorStreak — an
160
+ // error streak at ERROR_STREAK_WARN, or a quiet-kill streak): the red "failing tick after
161
+ // tick" banner. Keyed on the streak, not on the rendered phase, so it stays up while the
162
+ // loop's next tick runs — where loopPhase reads "working"/"merge"/"reviewing" rather
163
+ // than "failing" — and clears only when the streak resets (BUGS.md 2026-10-14). The
164
+ // wording names what happened (a streak of failed or rejected ticks), not a single repeated
165
+ // error, because a rejection streak is three different rejected changes.
166
+ const failing = loops.filter((l) => l.errorStreak);
167
+ if (failing.length) {
168
+ out.push({
169
+ key: "failing",
170
+ tone: "red",
171
+ title: `${listRoles(failing)} ${agree(failing.length, "is", "are")} failing run after run`,
172
+ detail: failing.some((l) => l.lastError)
173
+ ? failing.map((l) => `${roleLabel(l.role)}: ${l.lastError || "see its transcript"}`).join(" · ")
174
+ : "The streak clears only on the next healthy run; the transcript names each failure.",
175
+ actions: openRoleActions(failing),
176
+ });
177
+ }
178
+ // A stall marker rides only a running label. The director is excluded from `inFlight` (it
179
+ // holds no maxConcurrent permit), so its own running phase is accepted too — otherwise a
180
+ // stalled director would go unalerted (BUGS.md 2026-10-06).
181
+ const stuck = loops.filter((l) => (l.inFlight || l.phase.startsWith("director working")) && STALL_RE.test(l.phase));
182
+ if (stuck.length) {
183
+ out.push({
184
+ key: "stuck",
185
+ tone: "amber",
186
+ title: `${listRoles(stuck)} ${agree(stuck.length, "looks", "look")} stuck`,
187
+ detail: stuck.map(stuckDetail).join(" · "),
188
+ actions: openRoleActions(stuck),
189
+ });
190
+ }
191
+ if (snap.build?.stale) {
192
+ const behind = plural(snap.build.aheadCommits ?? 0, "commit");
193
+ out.push(snap.build.restartBlocked
194
+ ? {
195
+ key: "build",
196
+ tone: "amber",
197
+ title: `Update available (${behind} behind) — restart blocked`,
198
+ detail: localizeInstants(snap.build.restartBlocked),
199
+ actions: [],
200
+ }
201
+ : {
202
+ key: "build",
203
+ tone: "blue",
204
+ title: `Update available (${behind} behind)${snap.build.restartPending ? " — restarts when idle" : ""}`,
205
+ detail: snap.build.restartPending ? "The team restarts onto it once running work finishes." : "Restart tumwater start to pick it up.",
206
+ actions: [],
207
+ });
208
+ }
209
+ if (questions.length) {
210
+ out.push({
211
+ key: "questions",
212
+ tone: "indigo",
213
+ title: `${plural(questions.length, "question needs", "questions need")} your answer`,
214
+ detail: `${stripEntryStamp(questions[0])}${questions.length > 1 ? ` — and ${questions.length - 1} more` : ""}`,
215
+ actions: [{ label: "Answer", act: "questions" }],
216
+ });
217
+ }
218
+ // Refused entries hold until a human decides; amber because the operator has to act.
219
+ if (refused.length) {
220
+ out.push({
221
+ key: "refused",
222
+ tone: "amber",
223
+ title: `${plural(refused.length, "refused entry waits", "refused entries wait")} on your decision`,
224
+ detail: `${stripEntryStamp(refused[0])}${refused.length > 1 ? ` — and ${refused.length - 1} more` : ""} · no role takes it until you release it (tumwater plan|bug --release <n>, or the entry's drawer) or cancel it`,
225
+ actions: [{ label: "Open backlog", act: "backlog" }],
226
+ });
227
+ }
228
+ // Quiet hours (plans: "Quiet hours … part 2/2, observability") informs rather than asks:
229
+ // the team holding to a schedule is not a problem, but an operator watching idle loops
230
+ // inside the window needs the same explanation the pause alert gives a paused team —
231
+ // why nothing ticks, and when it starts again. The window end comes from quiet-hours.ts's
232
+ // quietWindowEnd, the same helper quietBadge uses for the header.
233
+ if (snap.inQuietHours && snap.quietHours) {
234
+ const end = quietWindowEnd(snap.quietHours);
235
+ out.push({
236
+ key: "quiet",
237
+ tone: "blue",
238
+ title: `Quiet hours — roles start no new runs until ${end}`,
239
+ detail: `The schedule (${snap.quietHours} local time, quietHours in tumwater.json) holds them. Runs in progress finish, and the director still runs your prompts.`,
240
+ actions: [],
241
+ });
242
+ }
243
+ // The maintenance allowance (plans/work-ratio.md, part 1b/4): by design and self-clearing —
244
+ // the scheduler holds maintenance loops at the rolling 24 h allowance until merges free room,
245
+ // then they tick again on their own — so it informs (gray, no actions), the quiet alert's
246
+ // tone, rather than amber like the failed/refused asks. An operator watching idle maintenance
247
+ // roles gets the same explanation the hold's own row carries, team-wide.
248
+ if (snap.maintenanceQuota.held.length > 0) {
249
+ const mq = snap.maintenanceQuota;
250
+ out.push({
251
+ key: "maintenance",
252
+ tone: "gray",
253
+ title: "Maintenance is held at its rolling allowance",
254
+ detail: `${mq.used} of ${mq.allowance} maintenance allowance used over the last 24 h; ${listRoleIds(mq.held)} ${agree(mq.held.length, "resumes", "resume")} as work merges.`,
255
+ actions: [],
256
+ });
257
+ }
258
+ // A standing provider failure hold (src/team/team-hold.ts `standingHolds`, published through
259
+ // the orchestrator info file): a provider storm holds roles on that provider, so an operator
260
+ // watching idle loops with no log open sees why nothing new starts there. Self-clearing by
261
+ // design — the hold re-opens at its own deadline — so it informs (blue, no actions), the
262
+ // quiet and maintenance alerts' tone, rather than asking. The snapshot carries the holds only
263
+ // while a live orchestrator publishes one.
264
+ if (snap.providerHolds && snap.providerHolds.length > 0) {
265
+ const phrased = snap.providerHolds
266
+ .map((h) => `${h.provider ?? "pi's default backend"} (${h.kind}) until it re-opens in ${humanUntil(h.until, now)}`)
267
+ .join("; ");
268
+ out.push({
269
+ key: "provider-hold",
270
+ tone: "blue",
271
+ title: agree(snap.providerHolds.length, "Provider limit is holding new runs", "Provider limits are holding new runs"),
272
+ detail: `${phrased}. Runs in progress finish; roles on a held provider start no new runs until it re-opens.`,
273
+ actions: [],
274
+ });
275
+ }
276
+ if (snap.paused) {
277
+ const left = pauseCountdown(snap.pausedUntil, now);
278
+ out.push({
279
+ key: "paused",
280
+ tone: "amber",
281
+ // The operator's pause reason (`pause --reason <text>`) states verbatim, so an
282
+ // operator returning to a paused team knows why without digging through history.
283
+ title: left
284
+ ? `The team is paused and resumes in ${left}${pauseReasonSuffix(snap.pauseReason)}`
285
+ : `The team is paused${pauseReasonSuffix(snap.pauseReason)}`,
286
+ detail: "Roles start no new runs. Runs in progress finish, and the director still runs your prompts.",
287
+ actions: [{ label: "Resume now", act: "resume" }],
288
+ });
289
+ }
290
+ if (!snap.running) {
291
+ out.push({
292
+ key: "stopped",
293
+ tone: "gray",
294
+ title: "tumwater is not running — start it with `tumwater start`",
295
+ detail: "Start it with tumwater start in the project directory; the dashboards pick it up on their own.",
296
+ actions: [{ label: "Copy the command", act: "copy", arg: "tumwater start" }],
297
+ });
298
+ }
299
+ // Image input (plans/image-input.md "Surfaces", part 5/5): a loop whose model cannot read
300
+ // images. Blue when every text-only loop has a reroute target (a tick that reads an image
301
+ // stops and reruns — nothing fails); amber when some text-only loop has none (a blind read
302
+ // fails and image submissions are refused); a separate amber when a claimed model failed
303
+ // doctor's live probe (the claim is contradicted, whatever any loop resolved to). No alert
304
+ // when every enabled loop reads images (imageInput true or unknown — an unknown seam is not
305
+ // counted as text-only, mirroring the submit gate and the doctor's `tier models` stance).
306
+ const textOnly = loops.filter((l) => l.imageInput === false);
307
+ const noTarget = textOnly.filter((l) => l.imageReroute == null || l.imageReroute === "");
308
+ if (noTarget.length > 0) {
309
+ out.push({
310
+ key: "images",
311
+ tone: "amber",
312
+ title: `No configured model can read images for ${listRoles(noTarget)}`,
313
+ detail: "A run that reads an image fails, and image attachments to these roles are refused. Declare image input on a vision model in pi's models.json, or point a tier at one.",
314
+ actions: openRoleActions(noTarget),
315
+ });
316
+ }
317
+ else if (textOnly.length > 0) {
318
+ const targets = [...new Set(textOnly.map((l) => l.imageReroute ?? ""))].filter(Boolean);
319
+ out.push({
320
+ key: "images",
321
+ tone: "blue",
322
+ title: `${listRoles(textOnly)} ${agree(textOnly.length, "runs", "run")} on models that can't read images`,
323
+ detail: `A run that reads an image stops and reruns on ${targets.join(", ")}.`,
324
+ actions: [],
325
+ });
326
+ }
327
+ if (snap.imageProbeFailures.length > 0) {
328
+ out.push({
329
+ key: "images-probe",
330
+ tone: "amber",
331
+ title: `A claimed image-capable model failed doctor's probe`,
332
+ detail: `${snap.imageProbeFailures.join(", ")} declare image input but failed the live probe — run \`tumwater doctor --reprobe\` to re-check.`,
333
+ actions: [{ label: "See failures", act: "view", arg: "failures" }],
334
+ });
335
+ }
336
+ return out;
337
+ }
338
+ /** Does this alert ask something of the operator (red, amber, indigo), rather than inform? */
339
+ export function alertNeedsYou(alert) {
340
+ return alert.tone === "red" || alert.tone === "amber" || alert.tone === "indigo";
341
+ }
@@ -1,7 +1,7 @@
1
1
  import { readLiveProgress } from "./progress-data.js";
2
2
  import { compactTokens } from "../text/format.js";
3
3
  /** The in-flight tick's PROGRESS display model (split from status-model.ts, which keeps the
4
- * loop-phase ladder, the landing cells, and the row derivations): how a running tick renders
4
+ * loop-phase ladder, the merge cells, and the row derivations): how a running tick renders
5
5
  * its live detail — elapsed, turn, context, current tool, stall — derived from the live log
6
6
  * tail (progress-data.ts) and shared by BOTH observer surfaces: the terminal table
7
7
  * (status-render.ts's state cell) and the JSON/GUI payload (status-payload.ts), plus the
@@ -9,7 +9,7 @@ import { compactTokens } from "../text/format.js";
9
9
  * drift apart. This is the disk-reading half: every helper here reads (or accepts a caller's
10
10
  * precomputed) live log tail, while status-model.ts derives labels from the snapshot. */
11
11
  /** Seconds/minutes/hours elapsed formatting shared by the progress helpers and the phase
12
- * ladder's elapsed branches (loopPhase's landing and parked branches). The hours bucket
12
+ * ladder's elapsed branches (loopPhase's merge and parked branches). The hours bucket
13
13
  * rounds to whole minutes off the total, so a rounded-up 59m5Xs carries into the next hour
14
14
  * ("2h0m"); rounding the remainder alone would print the impossible "1h60m". */
15
15
  export function duration(ms) {
@@ -24,10 +24,10 @@ export function duration(ms) {
24
24
  /** The label for a loop's in-flight tick: `<label>` plus how long it has been running
25
25
  * ("working 3m", "reviewing 2m") — shared by workingDetail and the review-gate branch of
26
26
  * loopPhase so their elapsed formatting cannot drift. A tick with no recorded start time
27
- * renders as just the bare label. (A landing's elapsed is its marker's, never the tick's —
28
- * loopPhase's landing branch builds that head itself.) */
27
+ * renders as just the bare label. (A merge's elapsed is its marker's, never the tick's —
28
+ * loopPhase's merge branch builds that head itself.) */
29
29
  export function inFlightLabel(s, label) {
30
- const elapsed = s.lastTickStartedAt ? duration(Date.now() - s.lastTickStartedAt) : "";
30
+ const elapsed = s.lastRunStartedAt ? duration(Date.now() - s.lastRunStartedAt) : "";
31
31
  return `${label} ${elapsed}`.trim();
32
32
  }
33
33
  /** One rule for whether a log tail's progress describes THIS tick: the log's newest write
@@ -47,25 +47,25 @@ export function inFlightLabel(s, label) {
47
47
  function progressOfTick(s, p) {
48
48
  if (!p)
49
49
  return null;
50
- if (s.lastTickStartedAt === undefined)
50
+ if (s.lastRunStartedAt === undefined)
51
51
  return p;
52
- const elapsed = Math.max(0, Date.now() - s.lastTickStartedAt);
52
+ const elapsed = Math.max(0, Date.now() - s.lastRunStartedAt);
53
53
  if (p.quietMs > elapsed)
54
54
  return null; // Newest write predates this tick: a previous run's tail.
55
55
  return { ...p, quietMs: Math.min(p.quietMs, elapsed) };
56
56
  }
57
57
  /** The stall phrases a phase label can carry — inFlightDetail appends "tool call stalled:
58
- * <tool>" and "no pi output for <duration>" to the head — as a regex SOURCE string plus the
58
+ * <tool>" and "no agent output for <duration>" to the head — as a regex SOURCE string plus the
59
59
  * compiled matcher, single-homed beside the code that renders those phrases. Every consumer
60
60
  * that must recognize a stall inside a rendered phase label goes through these two, so the
61
- * server-side alert (fleet-alerts.ts) and the dashboard's browser twin (gui-client-model.ts,
61
+ * server-side alert (team-alerts.ts) and the dashboard's browser twin (gui-client-model.ts,
62
62
  * interpolating the source into its String.raw script) cannot drift from the wording the cell
63
63
  * actually renders. */
64
- export const STALL_SOURCE = String.raw `tool call stalled[^·]*|no pi output for [^·]*`;
65
- /** STALL_SOURCE compiled — the server-side matcher (fleet-alerts.ts's stuck alert). */
64
+ export const STALL_SOURCE = String.raw `tool call stalled[^·]*|no agent output for [^·]*`;
65
+ /** STALL_SOURCE compiled — the server-side matcher (team-alerts.ts's stuck alert). */
66
66
  export const STALL_RE = new RegExp(STALL_SOURCE);
67
67
  /** The shared parts assembly for an in-flight state cell: `head` (the phase label with its
68
- * own elapsed — inFlightLabel for a tick, the landing branch's own for a landing) plus turn,
68
+ * own elapsed — inFlightLabel for a tick, the merge branch's own for a merge) plus turn,
69
69
  * live context, current tool, and the ≥5-min no-output stall flag. `p` is null when there is
70
70
  * no progress (no log yet) — falls back to the bare head. */
71
71
  export function inFlightDetail(head, p) {
@@ -84,7 +84,7 @@ export function inFlightDetail(head, p) {
84
84
  // Silence under five minutes is normal (slow local-model prefills, long tool calls);
85
85
  // only flag a stall once at least five minutes have passed without any pi output.
86
86
  if (p.quietMs >= 300_000)
87
- parts.push(`no pi output for ${duration(p.quietMs)}`);
87
+ parts.push(`no agent output for ${duration(p.quietMs)}`);
88
88
  return parts.join(" · ");
89
89
  }
90
90
  /** Which pi run kind (progress-data.ts's ProgressRunKind) a loop's in-flight cells should read:
@@ -103,7 +103,7 @@ export function progressKind(s) {
103
103
  * role's log itself — the author accumulator by default, the gate one via `kind`. Every
104
104
  * tail-reading caller outside this module routes through it; status-model.ts's staged branch
105
105
  * is the one exception, keeping its raw readLiveProgress call because it feeds inFlightDetail
106
- * a freshly read gate tail for the landing label, not a per-frame threaded one. */
106
+ * a freshly read gate tail for the merge label, not a per-frame threaded one. */
107
107
  export function tickProgress(root, s, live, kind = "author") {
108
108
  return progressOfTick(s, live === undefined ? readLiveProgress(root, s.role, kind) : live);
109
109
  }
@@ -1,3 +1,4 @@
1
+ // vocabulary: plain
1
2
  import { loopRank } from "./status-model.js";
2
3
  /** A loop phase label's (status-model loopPhase) tone, by its rank: live and pipeline work
3
4
  * blue, attention red, paused yellow, idle none. */
@@ -5,8 +6,8 @@ export function phaseTone(phase) {
5
6
  const rank = loopRank(phase);
6
7
  return rank <= 1 ? "blue" : rank === 2 ? "red" : rank === 3 ? "yellow" : undefined;
7
8
  }
8
- /** A tick result's tone (tick-outcome.ts TickResult): a completed landing (changed) green,
9
- * queued to land cyan, the failures red, the held-back outcomes (refused, rejected, aborted,
9
+ /** A tick result's tone (tick-outcome.ts TickResult): a completed merge (changed) green,
10
+ * queued to merge cyan, the failures red, the held-back outcomes (refused, rejected, aborted,
10
11
  * quiet_killed) yellow. */
11
12
  export function resultTone(result) {
12
13
  switch (result) {
@@ -29,17 +30,17 @@ export function resultTone(result) {
29
30
  return undefined;
30
31
  }
31
32
  }
32
- /** The tick results that make a `tick_end` a problem — one home of the list, also fed to the
33
+ /** The tick results that make a `run_end` a problem — one home of the list, also fed to the
33
34
  * dashboard's browser twin (gui-client-model.ts interpolates it into its script, both for
34
35
  * eventKind and for resultWhy's "does this result carry an error" check), so a newly problem
35
36
  * result cannot be classified on one surface and not the other. */
36
37
  export const PROBLEM_RESULTS = ["refused", "rejected", "review_error", "merge_conflict", "merge_blocked", "error", "aborted", "quiet_killed", "main_red"];
37
38
  /** The named event types that are routine bookkeeping — one home of the list, interpolated into
38
39
  * gui-client-model.ts's browser eventKind exactly as PROBLEM_RESULTS is. */
39
- export const ROUTINE_EVENTS = ["tick_start", "wake", "tick_deferred", "review_start", "review_verdict", "land_queued", "landed", "resume", "counters_reset"];
40
+ export const ROUTINE_EVENTS = ["run_start", "wake", "run_deferred", "review_start", "review_verdict", "merge_queued", "merge_done", "resume", "counters_reset", "state_migrated"];
40
41
  /** The named event types that are problems — one home of the list, interpolated into
41
42
  * gui-client-model.ts's browser eventKind exactly as PROBLEM_RESULTS is. */
42
- export const PROBLEM_EVENTS = ["land_failed", "review_rejected", "review_failed", "restart_blocked", "restart_refused", "budget_warning", "budget_paused", "role_cap_warning", "role_cap_paused", "disk_low", "supervisor_exit", "warning"];
43
+ export const PROBLEM_EVENTS = ["merge_failed", "review_rejected", "review_failed", "restart_blocked", "restart_refused", "budget_warning", "budget_paused", "role_cap_warning", "role_cap_paused", "disk_low", "main_red_hold", "supervisor_exit", "warning"];
43
44
  /** The build-check results that read as routine rather than problem in `eventKind` — one home
44
45
  * of the list, interpolated into gui-client-model.ts's browser eventKind exactly as
45
46
  * PROBLEM_RESULTS is. */
@@ -51,16 +52,18 @@ const ROUTINE_EVENT_SET = new Set(ROUTINE_EVENTS);
51
52
  const PROBLEM_EVENT_SET = new Set(PROBLEM_EVENTS);
52
53
  const ROUTINE_BUILD_RESULT_SET = new Set(ROUTINE_BUILD_RESULTS);
53
54
  /** Which EventKind one event is, checked in precedence order: the two hard-coded news types
54
- * first (merged, question_posted), then the result-carrying types by their result — a tick_end
55
+ * first (merged, question_posted), then the result-carrying types by their result — a run_end
55
56
  * is routine unless its result is a known failure, and a build_check is routine only when it
56
57
  * passed or was skipped — then the named event sets, with anything unrecognized reading as
57
58
  * info (an event this file predates is news until it is classified). */
58
59
  export function eventKind(type, result) {
59
60
  if (type === "merged")
60
- return "landing";
61
+ return "landing"; // vocabulary-ok: EventKind value renamed by the code series
61
62
  if (type === "question_posted")
62
63
  return "attention";
63
- if (type === "tick_end")
64
+ if (type === "entry_refused")
65
+ return "attention";
66
+ if (type === "run_end")
64
67
  return result !== undefined && PROBLEM_RESULT_SET.has(result) ? "problem" : "routine";
65
68
  if (type === "build_check")
66
69
  return result !== undefined && ROUTINE_BUILD_RESULT_SET.has(result) ? "routine" : "problem";
@@ -1,3 +1,4 @@
1
+ // vocabulary: plain
1
2
  import fs from "node:fs";
2
3
  import { openForRead, statOrNull } from "../files/files.js";
3
4
  import { formatTranscript } from "./transcript.js";
@@ -1,3 +1,4 @@
1
+ // vocabulary: plain
1
2
  import { parsePiEventLine } from "../pi/pi-event-line.js";
2
3
  import { statRoleLog, withTail } from "../files/tail.js";
3
4
  import { collapseWhitespace, squash, textOr, truncate } from "../text/text.js";
@@ -15,6 +16,26 @@ function contentBlocks(content) {
15
16
  return [];
16
17
  return content.map((raw) => (raw ?? {}));
17
18
  }
19
+ /** Append a `text` content block's lines to `out` under the per-message line budget, returning
20
+ * the new (shown, overflow-marked) counters. Blank lines render nothing; once the budget is
21
+ * spent the ellipsis marks the overflow a single time and every later line is dropped. */
22
+ function renderTextLines(block, out, textShown, textOverflowMarked) {
23
+ for (const rawLine of textOr(block.text, "").split("\n")) {
24
+ const line = rawLine.trim();
25
+ if (!line)
26
+ continue;
27
+ if (textShown >= TEXT_LINES_PER_MESSAGE) {
28
+ if (!textOverflowMarked) {
29
+ out.push(" …");
30
+ textOverflowMarked = true;
31
+ }
32
+ break;
33
+ }
34
+ out.push(` ${truncate(line, TEXT_LINE_MAX_COLS)}`);
35
+ textShown += 1;
36
+ }
37
+ return { textShown, textOverflowMarked };
38
+ }
18
39
  /** Render one completed assistant message into transcript lines: abbreviated thinking,
19
40
  * indented text (capped), and labeled tool calls — in the order they appear. */
20
41
  function renderAssistantMessage(content) {
@@ -29,23 +50,9 @@ function renderAssistantMessage(content) {
29
50
  out.push(`· ${truncate(thinking, THINKING_MAX_CHARS)}`);
30
51
  break;
31
52
  }
32
- case "text": {
33
- for (const rawLine of textOr(block.text, "").split("\n")) {
34
- const line = rawLine.trim();
35
- if (!line)
36
- continue;
37
- if (textShown >= TEXT_LINES_PER_MESSAGE) {
38
- if (!textOverflowMarked) {
39
- out.push(" …");
40
- textOverflowMarked = true;
41
- }
42
- break;
43
- }
44
- out.push(` ${truncate(line, TEXT_LINE_MAX_COLS)}`);
45
- textShown += 1;
46
- }
53
+ case "text":
54
+ ({ textShown, textOverflowMarked } = renderTextLines(block, out, textShown, textOverflowMarked));
47
55
  break;
48
- }
49
56
  case "toolCall":
50
57
  out.push(`→ ${describeToolCall(textOr(block.name), block.arguments)}`);
51
58
  break;
@@ -1,4 +1,5 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
+ // vocabulary: plain
2
3
  /** The ink component tree that draws `tumwater tui`'s frame. Pure drawing: it takes the
3
4
  * frame tui.tsx's render step assembles — the same clipped StatusLines the tui-frame.ts
4
5
  * builders produce — and lays one line out per row, one styled Text per span. Ink