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,15 +1,19 @@
1
+ // vocabulary: plain
1
2
  import { eventRole, eventTick, eventUsage } from "./event-read.js";
2
- import { ORCHESTRATOR_STOPPED, backendKindPhrase, budgetPhrase, firstReason, fleetPausePhrase, fleetResumePhrase, holdPhrase, plural, rolePausePhrase, roleResumePhrase, rolesPhrase, shortSpanPhrase } from "../text/phrases.js";
3
+ import { eventTypeLabel } from "./event-labels.js";
4
+ import { roleLabel } from "../roles/role-labels.js";
5
+ import { TUMWATER_SOURCE } from "./events.js";
6
+ import { ORCHESTRATOR_STOPPED, backendKindPhrase, budgetPhrase, firstReason, teamPausePhrase, teamResumePhrase, holdPhrase, plural, rolePausePhrase, roleResumePhrase, rolesPhrase, shortSpanPhrase } from "../text/phrases.js";
3
7
  import { compactTokens, formatGB, shortSha, usd } from "../text/format.js";
4
8
  import { padToWidth } from "../text/text-width.js";
5
- import { textOr } from "../text/text.js";
9
+ import { squash, textOr, truncate } from "../text/text.js";
6
10
  import { formatTimestamp, formatTime } from "../text/datetime.js";
7
11
  import { finiteNumber, isJsonObject, stringList } from "../files/json-object.js";
8
12
  import { fallbackTierEntries } from "../budget/budget.js";
9
- /** The `<N> tok · $<spent>` usage fragment every event that records a run's cost shares (tick_end,
10
- * landed): the usage numbers arrive via eventUsage (the loose-typing coercion every usage consumer
11
- * shares), rendered through the shared token/money formats (compactTokens, usd) in one place.
12
- * Either part is omitted when zero or absent, so skipped ticks and zero-usage landings render
13
+ /** The `<N> tok · $<spent>` usage fragment every event that records a run's cost shares (run_end,
14
+ * merge_done): the usage numbers arrive via eventUsage (the loose-typing coercion every usage
15
+ * consumer shares), rendered through the shared token/money formats (compactTokens, usd) in one
16
+ * place. Either part is omitted when zero or absent, so skipped ticks and zero-usage merges render
13
17
  * byte-identical to a pre-feature line — no leading or trailing separator. "·" is the separator
14
18
  * the budget badge already uses. Sentence contexts prepend their own " · " via usagePhrase;
15
19
  * table-cell contexts (history's usage column) use the bare text. */
@@ -57,8 +61,15 @@ function windowResetPhrase(e) {
57
61
  const at = finiteNumber(e.resetsAt, null);
58
62
  return at === null ? "" : `, resets ${formatTime(new Date(at))}`;
59
63
  }
64
+ /** The parenthesized ` (<window>)` a quiet-hours event names when it carries one, or "" when
65
+ * the field is absent or blank (pollQuietHoursGate may send no window, so a routine hold must
66
+ * not fabricate a pair). Shared by the started and ended lines so the two cannot drift on the
67
+ * window's presence or format. */
68
+ function windowSuffix(e) {
69
+ return typeof e.window === "string" && e.window.trim() ? ` (${textOr(e.window)})` : "";
70
+ }
60
71
  /** The backend a hold event names, rendered as " at <provider>" when one is configured
61
- * (pollFleetHold omits it for pi's default) and "" otherwise, so a multi-provider fleet can
72
+ * (pollTeamHold omits it for pi's default) and "" otherwise, so a multi-provider team can
62
73
  * tell WHICH backend the storm was at. Shared by the hold and resumed lines. */
63
74
  function providerAt(e) {
64
75
  return typeof e.provider === "string" ? ` at ${e.provider}` : "";
@@ -69,7 +80,13 @@ function providerAt(e) {
69
80
  function providerScope(e) {
70
81
  return typeof e.provider === "string" ? "that provider" : "every provider";
71
82
  }
72
- /** An event's outcome string — a tick's `result`, or a landing's or build check's `status`,
83
+ /** The provider/model pair a fallback-ish line names, as "provider/model", defaulting each
84
+ * half when the event omits it (a bare pi run carries neither). Shared by the budget_fallback
85
+ * and model_fallback lines so the pair's shape and defaults cannot drift between them. */
86
+ function providerModelPair(e, providerDefault, modelDefault) {
87
+ return `${textOr(e.provider, providerDefault)}/${textOr(e.model, modelDefault)}`;
88
+ }
89
+ /** An event's outcome string — a tick's `result`, or a merge's or build check's `status`,
73
90
  * whichever the event carries; undefined when neither does. The activity feeds' tone and
74
91
  * filter key (tone.ts's eventKind takes it beside the event type), also shipped as the
75
92
  * status payload's per-event `result` field, so the two surfaces cannot disagree on what a
@@ -86,13 +103,16 @@ export function eventResult(e) {
86
103
  export function outcomeText(e) {
87
104
  return eventResult(e) ?? "?";
88
105
  }
89
- /** Why a `tick_deferred` event fired, as the clause that can follow "deferred — ": scheduling's
90
- * reason ("backlog" or "no-work"). Shared by the event feed (eventMessage) and the failure
91
- * digest (describeStateChange) so the two cannot disagree on the deferral's cause. A missing or
106
+ /** Why a `run_deferred` event fired, as the clause that can follow "deferred — ": scheduling's
107
+ * reason ("backlog", "no-work" or "plans-full"). Shared by the event feed (eventMessage) and
108
+ * the failure digest (describeStateChange) so the two cannot disagree on the deferral's cause.
109
+ * A missing or
92
110
  * unknown value — a torn line, or an event written before the field existed — reads as the
93
111
  * no-work spelling, the only clause the pre-field builds could log. */
94
112
  export function deferredReasonText(e) {
95
- return e.reason === "backlog" ? "feature/bugfix backlog open" : "no work landed since last tick";
113
+ if (e.reason === "plans-full")
114
+ return "plan backlog at its target";
115
+ return e.reason === "backlog" ? "feature/bugfix backlog open" : "nothing merged since the last run";
96
116
  }
97
117
  /** Human one-liner for an event, shared by `logs`, `run` output, and the TUI activity pane:
98
118
  * the local `YYYY-MM-DD HH:MM:SS` stamp (formatTimestamp — the same zero-padded, locale-
@@ -109,11 +129,12 @@ export function formatEvent(e) {
109
129
  // sat right of every ASCII row's (the same unit mistake the aligned tables shed). The loop
110
130
  // itself goes through eventRole, so a torn or hand-edited event with no loop shows "?" like
111
131
  // the usage report and failure digest, never String(undefined)'s literal "undefined".
112
- const loop = padToWidth(eventRole(e), 9);
132
+ const source = eventRole(e);
133
+ const loop = padToWidth(source === TUMWATER_SOURCE ? "tumwater" : roleLabel(source), 10);
113
134
  return `${time} ${loop} ${eventMessage(e)}`;
114
135
  }
115
136
  /** One event as the dashboard's activity feed consumes it: `result` is a tick's outcome, a
116
- * landing's result, or a build check's status — whichever string the event carries. The event
137
+ * merge's result, or a build check's status — whichever string the event carries. The event
117
138
  * layer owns the event shape's projection, so both the status poll (ui/status-payload.ts) and
118
139
  * the slice endpoint (gui/gui-endpoints.ts's handleLogs) read the same fields, and a feed row's
119
140
  * shape cannot drift between them. */
@@ -121,7 +142,7 @@ export function eventFeedItem(e) {
121
142
  const result = eventResult(e);
122
143
  // eventRole renders a missing/empty loop as "?" like the reports, so a torn or hand-edited
123
144
  // event never shows a literal "undefined" loop chip in the dashboard's activity feed.
124
- return { ts: e.ts, loop: eventRole(e), type: e.type, ...(result === undefined ? {} : { result }), message: eventMessage(e) };
145
+ return { ts: e.ts, source: eventRole(e), type: e.type, ...(result === undefined ? {} : { result }), message: eventMessage(e) };
125
146
  }
126
147
  /** The feed's `--grep`/Search match rule: case-insensitive substring over the raw event type
127
148
  * prefixed to the rendered line — exactly the haystack the operator would otherwise read. One
@@ -135,64 +156,79 @@ export function eventMatchesGrep(e, patternLower) {
135
156
  * ships this beside the event's type and loop instead of making the page re-parse a line. */
136
157
  export function eventMessage(e) {
137
158
  switch (e.type) {
138
- case "tick_start":
139
- return `tick #${eventTick(e)} started${e.model !== undefined ? ` on ${textOr(e.model)}` : ""}`;
140
- case "tick_end": {
159
+ case "run_start":
160
+ return `run #${eventTick(e)} started${e.model !== undefined ? ` on ${textOr(e.model)}` : ""}`;
161
+ case "run_end": {
141
162
  // The payload that explains the outcome: summary for changed/refused/rejected/
142
163
  // review_error ticks, error (lastError) for error and merge-failed ones. Showing it
143
164
  // for every result that carries one keeps the event feed self-explanatory — a bare
144
165
  // "tick #N refused" would force operators to open the transcript for the reason.
145
166
  const extra = e.summary ? ` — ${textOr(e.summary)}` : e.error ? ` — ${textOr(e.error)}` : "";
146
167
  // Per-tick usage: where the day's spend went.
147
- return `tick #${eventTick(e)} ${outcomeText(e)}${extra}${usagePhrase(e)}`;
168
+ return `run #${eventTick(e)} ${eventTypeLabel(outcomeText(e))}${extra}${usagePhrase(e)}`;
148
169
  }
149
170
  case "merged":
150
171
  return `merged ${shortSha(e.commit)} to main — ${textOr(e.summary)}`;
151
- case "land_queued":
152
- // Merge queue 3/5: routine state change (the tick committed and the landing slot is
153
- // ahead) — no warning prefix. The landing's own events follow it.
154
- return `queued ${shortSha(e.commit)} for landing — ${textOr(e.summary)}`;
155
- case "landed":
156
- // The landing slot finished with the change on main; the usage is the landing's own
157
- // spend (reviewer + conflict resolution), omitted when zero so review-exempt landings
158
- // render bare — the same phrase tick_end carries for its run.
159
- return `landing of ${shortSha(e.commit)} complete${elapsed(e.durationMs)}${usagePhrase(e)}`;
160
- case "land_failed":
172
+ case "merge_queued":
173
+ // Merge queue 3/5: routine state change (the tick committed and the merge slot is
174
+ // ahead) — no warning prefix. The merge's own events follow it.
175
+ return `waiting to merge ${shortSha(e.commit)} — ${textOr(e.summary)}`;
176
+ case "merge_done":
177
+ // The merge slot finished with the change on main; the usage is the merge's own
178
+ // spend (reviewer + conflict resolution), omitted when zero so review-exempt merges
179
+ // render bare — the same phrase run_end carries for its run.
180
+ return `merge of ${shortSha(e.commit)} done${elapsed(e.durationMs)}${usagePhrase(e)}`;
181
+ case "merge_failed":
161
182
  // Routine failure detail: the review events themselves (review_rejected/review_failed)
162
183
  // or the merged/build_check lines carry the reason in the feed.
163
- return `landing of ${shortSha(e.commit)} did not land (${outcomeText(e)})${elapsed(e.durationMs)}`;
184
+ return `merge of ${shortSha(e.commit)} failed (${eventTypeLabel(outcomeText(e))})${elapsed(e.durationMs)}${typeof e.summary === "string" && e.summary.trim() ? ` — ${truncate(e.summary, 80)}` : ""}`;
164
185
  case "question_posted":
165
186
  // Routine operation (a loop asked the user something), not a warning.
166
- return `question posted: ${textOr(e.question)}`;
187
+ // The position is the `<n>` `questions answer` takes, so the notify page is actionable.
188
+ return `question posted: ${textOr(e.question)}${typeof e.position === "number" && Number.isInteger(e.position) && e.position >= 1 ? ` — question ${e.position} in \`tumwater questions\`; answer it with \`tumwater questions answer ${e.position} "<decision>"\`` : ""}`;
189
+ case "entry_refused":
190
+ // Routine operation (a refusal note merged), not a warning — like question_posted.
191
+ const files = Array.isArray(e.files) ? e.files.join(", ") : "";
192
+ const titles = stringList(e.entries);
193
+ if (titles.length === 0)
194
+ return `entry refused: ${textOr(e.reason)} (${files})`;
195
+ const more = titles.length > 1 ? ` (and ${titles.length - 1} more)` : "";
196
+ return `entry refused: "${truncate(titles[0], 80)}"${more}: ${textOr(e.reason)} (${files})`;
167
197
  case "wake":
168
198
  return `woke (${textOr(e.reason)})`;
169
- case "tick_deferred":
199
+ case "run_deferred":
170
200
  // Routine state change (need-based prioritization), like counters_reset — no warning
171
201
  // prefix. One per deferral episode; the tick's own events cover the episode's end.
172
202
  return `deferred — ${deferredReasonText(e)}`;
173
203
  case "orchestrator_start":
174
- return `orchestrator started (pid ${textOr(e.pid)}${e.build ? `, build ${shortSha(e.build)}` : ""})`;
204
+ return `tumwater started (pid ${textOr(e.pid)}${e.build ? `, version ${shortSha(e.build)}` : ""})`;
175
205
  case "orchestrator_stop":
176
206
  return ORCHESTRATOR_STOPPED;
177
207
  case "prompt_enqueued":
178
- return `user prompt queued: ${textOr(e.preview)}`;
208
+ return `message added to the inbox: ${textOr(e.preview)}`;
179
209
  case "prompt_cancelled":
180
210
  // Routine operation (the user removed a queued prompt), not a warning.
181
- return `user prompt cancelled: ${textOr(e.preview)}`;
211
+ return `message removed from the inbox: ${textOr(e.preview)}`;
182
212
  case "prompt_edited":
183
213
  // Routine operation (the user corrected a queued prompt), not a warning.
184
- return `user prompt edited: ${textOr(e.preview)}`;
214
+ return `inbox message edited: ${textOr(e.preview)}`;
215
+ case "prompt_topped":
216
+ // Routine operation (the user reordered a queued prompt), not a warning.
217
+ return `inbox message moved to the front: ${textOr(e.preview)}`;
218
+ case "prompt_taken":
219
+ // Routine: a tick dequeued the operator's request to run it.
220
+ return `inbox message taken by run #${eventTick(e)}: ${textOr(e.preview)}`;
185
221
  case "counters_reset": {
186
222
  // One role → the event is filed under that loop; several → one harness-level event
187
223
  // listing them.
188
224
  const roles = rolesPhrase(e.roles, "");
189
225
  const scope = roles ? ` for ${roles}` : "";
190
- return `counters reset${scope} (ticks, commits, tokens, cost)`;
226
+ return `counters reset${scope} (runs, merges, tokens, cost)`;
191
227
  }
192
- case "tick_aborted":
228
+ case "run_aborted":
193
229
  // Routine state change (the user stopped one loop's tick), like counters_reset — no
194
- // warning prefix. The resulting tick_end line carries the user_aborted outcome.
195
- return `tick aborted by user`;
230
+ // warning prefix. The resulting run_end line carries the user_aborted outcome.
231
+ return `run stopped by you`;
196
232
  case "review_start": {
197
233
  // A resumed review continues a timed-out reviewer's session (PLANS.md, review-retry part
198
234
  // 3/3). The verb names that, because a bare "reviewing" line reads as a from-scratch run;
@@ -209,7 +245,7 @@ export function eventMessage(e) {
209
245
  // one-liner read as the complete verdict — the full list rides in state.lastReview
210
246
  // into the author's next tick prompt, and this line is the operator's pointer to it.
211
247
  const more = reasons.length > 1
212
- ? ` (+${reasons.length - 1} more — the author's next tick carries every reason)`
248
+ ? ` (+${reasons.length - 1} more — the author's next run carries every reason)`
213
249
  : "";
214
250
  return `review rejected ${shortSha(e.head)} — ${firstReason(reasons)}${more}${elapsed(e.durationMs)}`;
215
251
  }
@@ -220,7 +256,7 @@ export function eventMessage(e) {
220
256
  // conflicted with it, dropped by its author, or exhausted its rounds.
221
257
  return `revision ${textOr(e.round)} ${textOr(e.action)} ${shortSha(e.sha)}`;
222
258
  case "conflict_handback": {
223
- // A change whose landings kept conflicting was handed back to its author with the markers
259
+ // A change whose merges kept conflicting was handed back to its author with the markers
224
260
  // in place (PLANS.md "Robust conflict landing, part 2/2"): queued, applied, or failed.
225
261
  const files = stringList(e.conflicted);
226
262
  const where = files.length > 0 ? `: ${files.join(", ")}` : "";
@@ -230,44 +266,46 @@ export function eventMessage(e) {
230
266
  case "build_check":
231
267
  // The deterministic check's cost, per run: scope names which gate paid (the pre-merge review
232
268
  // gate, the red-main baseline check of main itself, or the merge lock's post-rebase re-check
233
- // of the tree about to land). script carries the npm script name for an npm check but the
269
+ // of the tree about to merge). script carries the npm script name for an npm check but the
234
270
  // FULL configured command otherwise (checkScriptName in build/build-check.ts), so no "npm"
235
271
  // prefix is asserted here — the bare name reads correctly for both kinds ("npm test" would
236
272
  // misrender a cargo or make-based project's check).
237
- return `build check (${textOr(e.scope)}): ${textOr(e.script)} ${outcomeText(e)}${elapsed(e.durationMs)}`;
273
+ return `check (${textOr(e.scope)}): ${textOr(e.script)} ${eventTypeLabel(outcomeText(e))}${elapsed(e.durationMs)}`;
238
274
  case "dep_install": {
239
- // The root checkout's install catching up with a landed lockfile change — routine on
275
+ // The root checkout's install catching up with a merged lockfile change — routine on
240
276
  // success; a failure also rides its own warning, which says what happens meanwhile.
241
277
  const pkgs = stringList(e.packages).join(", ");
242
- return `root install (${pkgs}) ${outcomeText(e)}${e.error ? ` — ${textOr(e.error)}` : ""}${elapsed(e.durationMs)}`;
278
+ return `root install (${pkgs}) ${eventTypeLabel(outcomeText(e))}${e.error ? ` — ${textOr(e.error)}` : ""}${elapsed(e.durationMs)}`;
243
279
  }
244
280
  case "budget_warning":
245
281
  // The early page beside budget_paused: the cap is not reached yet, so say so — the
246
- // operator still has room to raise it or fix the fallback before the fleet stops.
247
- return `budget warning — ${budgetPhrase(e.spentUsd, e.capUsd)} of the daily cap spent; the gate is still open`;
282
+ // operator still has room to raise it or fix the fallback before the team stops.
283
+ return `budget warning — ${budgetPhrase(e.spentUsd, e.capUsd)} of the daily cap spent; runs still start`;
248
284
  case "budget_paused": {
249
285
  // Routine state change, like counters_reset — no warning prefix. A configured fallback
250
- // the gate refused is named here: it is the whole reason the fleet stopped instead of
286
+ // the gate refused is named here: it is the whole reason the team stopped instead of
251
287
  // switching over, and an operator reading the feed must be able to act on it. So is a
252
288
  // free fallback the breaker demoted because its ticks kept failing (BUGS.md 2026-09-20):
253
- // the backend, not the price, is what to fix, and the fleet retries it on its own.
289
+ // the backend, not the price, is what to fix, and the team retries it on its own.
290
+ // Like role_cap_paused, the line names the lift paths: a raised or removed cap, or the
291
+ // local day rolling over (the demoted branch's cool-down retry is the automatic third).
254
292
  const refused = e.fallbackRejected
255
- ? ` (fallback ${textOr(e.fallbackRejected)} is not a cost n/a model in pi's models.json)`
293
+ ? ` (over-cap model ${textOr(e.fallbackRejected)} is not a cost n/a model in pi's models.json)`
256
294
  : e.fallbackDemoted
257
- ? ` (fallback ${textOr(e.fallbackDemoted)} is not serving — ${textOr(e.failures)} consecutive ticks failed on it; one probe tick retries it after a cool-down)`
295
+ ? ` (over-cap model ${textOr(e.fallbackDemoted)} is not serving — ${textOr(e.failures)} runs failed in a row on it; one trial run retries it after a cool-down)`
258
296
  : "";
259
- return `budget paused — ${budgetPhrase(e.spentUsd, e.capUsd)} daily cost reached${refused}`;
297
+ return `budget paused — ${budgetPhrase(e.spentUsd, e.capUsd)} daily cost reached${refused}; roles start no new runs until maxDailyCostUsd is raised or removed in tumwater.json or the local day rolls over`;
260
298
  }
261
299
  case "budget_fallback": {
262
- // The cap is spent but the fleet keeps working: name the free model it switched to, the
300
+ // The cap is spent but the team keeps working: name the free model it switched to, the
263
301
  // one fact that distinguishes this from a pause. When the event carries the per-tier
264
302
  // map (tiers, part 7b/8 — only emitted when the tiers resolve to two or more distinct
265
303
  // pairs), list each tier's pair and let the borrowed ones' `(from …)` suffixes show a
266
304
  // strong-tier borrow at a glance; a tier-free event keeps today's single-pair text.
267
305
  const tierEntries = fallbackTierEntries(isJsonObject(e.tiers) ? e.tiers : undefined);
268
306
  if (tierEntries.length >= 2)
269
- return `budget fallback — ${budgetPhrase(e.spentUsd, e.capUsd)} daily cost reached; role loops continue on ${tierEntries.join(", ")} (cost n/a)`;
270
- return `budget fallback — ${budgetPhrase(e.spentUsd, e.capUsd)} daily cost reached; role loops continue on ${textOr(e.provider, "pi's default provider")}/${textOr(e.model, "pi's default model")} (cost n/a)`;
307
+ return `over-cap model — ${budgetPhrase(e.spentUsd, e.capUsd)} daily cost reached; roles continue on ${tierEntries.join(", ")} (cost n/a)`;
308
+ return `over-cap model — ${budgetPhrase(e.spentUsd, e.capUsd)} daily cost reached; roles continue on ${providerModelPair(e, "pi's default provider", "pi's default model")} (cost n/a)`;
271
309
  }
272
310
  case "budget_resumed":
273
311
  return `budget resumed (${budgetPhrase(e.spentUsd, e.capUsd)} today)`;
@@ -276,52 +314,78 @@ export function eventMessage(e) {
276
314
  // reopened. Their ticks end `aborted` and resume promptly on the primary, so an operator
277
315
  // reading the feed can connect the aborted ticks to this one line.
278
316
  const roles = rolesPhrase(e.roles, "?");
317
+ if (e.reason === "window")
318
+ return `claude subscription window reset: handed ${roles} back to claude`;
279
319
  return `budget reopened: handed ${roles} back to the primary`;
280
320
  }
281
- case "fleet_paused":
321
+ case "fallback_switched": {
322
+ // One line for the poll's changes: `<tier> → <pair> (<reason>)`, the primary as "primary".
323
+ const tiers = isJsonObject(e.tiers) ? e.tiers : {};
324
+ const parts = Object.entries(tiers).map(([tier, v]) => {
325
+ const t = isJsonObject(v) ? v : {};
326
+ return `${tier} → ${typeof t.to === "string" ? t.to : "primary"} (${textOr(t.reason)})`;
327
+ });
328
+ return `fallback: ${parts.join("; ")}`;
329
+ }
330
+ case "team_paused":
282
331
  // Routine state change, like counters_reset — no warning prefix.
283
- return `${fleetPausePhrase()} (director keeps running)`;
284
- case "fleet_resumed":
285
- return fleetResumePhrase();
332
+ return `${teamPausePhrase()} (director keeps running)`;
333
+ case "state_migrated":
334
+ return `state migrated — ${textOr(e.detail)}`;
335
+ case "team_resumed":
336
+ return teamResumePhrase();
286
337
  case "role_paused":
287
- // Routine state change, like fleet_paused — no warning prefix.
288
- return `${rolePausePhrase(textOr(e.role))} (the rest of the fleet keeps running)`;
338
+ // Routine state change, like team_paused — no warning prefix.
339
+ return `${rolePausePhrase(textOr(e.role))} (the rest of the team keeps running)`;
289
340
  case "role_resumed":
290
341
  return roleResumePhrase(textOr(e.role));
291
342
  case "role_streak_paused":
292
343
  // Routine-with-explanation, like rate_limit_hold — no warning prefix: the pause IS the
293
344
  // harness handling the failure. Names the streak depth and the lift command, so the
294
- // operator knows why the loop stopped and what to do about it.
295
- return `role ${textOr(e.role)} paused — ${textOr(e.streak)} ticks failed in a row; fix the cause and resume it (tumwater resume --role <id>)`;
345
+ // operator knows why the loop stopped and what to do about it; names the last error
346
+ // when one is recorded.
347
+ return `role ${textOr(e.role)} paused — ${textOr(e.streak)} runs failed in a row${typeof e.lastError === "string" && e.lastError.trim() ? ` — last error: ${squash(e.lastError, 120)}` : ""}; fix the cause and resume it (tumwater resume --role <id>)`;
296
348
  case "role_cap_warning":
297
349
  // The per-role early page beside role_cap_paused: the cap is not reached yet, so say so —
298
350
  // the operator still has room to raise it before the loop stops.
299
- return `role ${textOr(e.role)} at ${budgetPhrase(e.spentUsd, e.capUsd)} of its daily cap — it keeps ticking; raise or remove the cap in tumwater.json before it pauses`;
351
+ return `role ${textOr(e.role)} at ${budgetPhrase(e.spentUsd, e.capUsd)} of its daily cap — it keeps running; raise or remove the cap in tumwater.json before it pauses`;
300
352
  case "role_cap_paused":
301
353
  // Routine state change, like budget_paused — no warning prefix: the pause IS the harness
302
354
  // holding the role's own spend line. Names the role, its spend vs its cap, and the two
303
355
  // lift paths, so the operator knows why the loop stopped and what to do about it.
304
- return `role ${textOr(e.role)} paused — ${budgetPhrase(e.spentUsd, e.capUsd)} of its daily cap spent; it starts no new ticks until the cap is raised or removed in tumwater.json or the local day rolls over`;
356
+ return `role ${textOr(e.role)} paused — ${budgetPhrase(e.spentUsd, e.capUsd)} of its daily cap spent; it starts no new runs until the cap is raised or removed in tumwater.json or the local day rolls over`;
305
357
  case "role_cap_resumed":
306
- return `role ${textOr(e.role)} resumed — it is under its daily cap again and ticks again`;
358
+ return `role ${textOr(e.role)} resumed — it is under its daily cap again and runs again`;
359
+ case "main_red_hold":
360
+ // The message already names the SHA, script, and consequence; the problem tone comes
361
+ // from PROBLEM_EVENTS.
362
+ return textOr(e.message);
307
363
  case "maintenance_quota_hold":
308
364
  // Routine state change, like role_cap_paused — no warning prefix: the hold IS the harness
309
- // keeping maintenance landings in proportion to work. Names the window's counts so the
365
+ // keeping maintenance merges in proportion to work. Names the window's counts so the
310
366
  // operator can see why the hygiene loops stopped and what would re-open them.
311
- return `maintenance quota reached — ${textOr(e.maint)} maintenance landings against an allowance of ${textOr(e.allowance)} in the last 24h (${textOr(e.work)} work); the maintenance loops are held until the window rolls over`;
367
+ return `waiting: maintenance allowance used — ${textOr(e.maint)} maintenance merges against an allowance of ${textOr(e.allowance)} in the last 24h (${textOr(e.work)} work); the maintenance roles wait until the window rolls over`;
312
368
  case "maintenance_quota_resumed":
313
- return `maintenance quota re-opened — ${textOr(e.maint)} maintenance landings against an allowance of ${textOr(e.allowance)} in the last 24h; the maintenance loops tick again`;
369
+ return `maintenance allowance reopened — ${textOr(e.maint)} maintenance merges against an allowance of ${textOr(e.allowance)} in the last 24h; the maintenance roles run again`;
314
370
  case "bootstrap_complete":
315
- // Routine state change, like counters_reset — no warning prefix: the fleet just finished
371
+ // Routine state change, like counters_reset — no warning prefix: the team just finished
316
372
  // its bootstrap and the maintenance loops are admitted.
317
- return `bootstrap complete — ${textOr(e.plansDone)}/${textOr(e.untilPlansDone)} plans done; the maintenance loops now tick`;
373
+ return `bootstrap complete — ${textOr(e.plansDone)}/${textOr(e.untilPlansDone)} plans done; the maintenance roles now run`;
318
374
  case "disk_low":
319
- // Routine state change, like fleet_paused — no warning prefix: the hold IS the harness
375
+ // Routine state change, like team_paused — no warning prefix: the hold IS the harness
320
376
  // handling the low disk, and naming the free space and the floor tells the operator
321
377
  // whether to clear space or lower diskHoldGB in tumwater.json.
322
- return `disk low — ${gigabytes(e.freeGB)} free on the worktrees volume (floor ${gigabytes(e.holdGB)}); new work is held until space recovers`;
378
+ return `waiting: low disk — ${gigabytes(e.freeGB)} free on the workspaces volume (floor ${gigabytes(e.holdGB)}); new work waits until space recovers`;
379
+ case "quiet_hours_started": {
380
+ // Routine hold, like disk_low — no warning prefix: the window is configured, and the line
381
+ // says what stops and what keeps running. pollQuietHoursGate may send no window.
382
+ return `waiting: quiet hours${windowSuffix(e)} — roles start no new runs until the window ends; the director still runs`;
383
+ }
384
+ case "quiet_hours_ended": {
385
+ return `quiet hours ended${windowSuffix(e)} — roles start runs again`;
386
+ }
323
387
  case "disk_ok":
324
- return `disk recovered — ${gigabytes(e.freeGB)} free on the worktrees volume; new work starts again`;
388
+ return `disk recovered — ${gigabytes(e.freeGB)} free on the workspaces volume; new work starts again`;
325
389
  case "disk_reclaim": {
326
390
  // Routine maintenance, no warning prefix: the pass freed space so the hold need not
327
391
  // engage. Naming the mode, the worktrees, and the delta tells the operator what and why.
@@ -330,38 +394,56 @@ export function eventMessage(e) {
330
394
  return `disk reclaim (${textOr(e.mode, "pressure")}) — freed ${gigabytes(e.freedGB)} from ${from}; ${gigabytes(e.freeGB)} free now`;
331
395
  }
332
396
  case "rate_limit_hold": {
333
- // Routine state change, like fleet_paused — no warning prefix: the hold IS the harness
334
- // handling the storm. Names who saw the failure and when the fleet re-opens on its own.
397
+ // Routine state change, like team_paused — no warning prefix: the hold IS the harness
398
+ // handling the storm. Names who saw the failure and when the team re-opens on its own.
335
399
  // The rate-limit kind keeps the wording every historical event has; a backend-failure
336
400
  // kind names itself instead, since "429" would be a lie about a connection error.
337
401
  const roles = rolesPhrase(e.roles, "several roles");
338
402
  if (e.kind && e.kind !== "rate-limit")
339
- return `backend hold (${backendKindPhrase(e.kind)})${providerAt(e)} — ${roles} hit backend failures; role loops on ${providerScope(e)} start nothing new ${holdPhrase(e.holdMs, e.escalation)} (director keeps running)`;
340
- return `429 hold${providerAt(e)} — ${roles} rate-limited by the provider; role loops on ${providerScope(e)} start nothing new ${holdPhrase(e.holdMs, e.escalation)} (director keeps running)`;
403
+ return `waiting: backend failing (${backendKindPhrase(e.kind)})${providerAt(e)} — ${roles} hit backend failures; roles on ${providerScope(e)} start no new runs ${holdPhrase(e.holdMs, e.escalation)} (director keeps running)`;
404
+ return `waiting: provider limit${providerAt(e)} — ${roles} rate-limited by the provider; roles on ${providerScope(e)} start no new runs ${holdPhrase(e.holdMs, e.escalation)} (director keeps running)`;
341
405
  }
342
406
  case "rate_limit_resumed": {
343
- // The ended hold's kind rides the resumed event (pollFleetHold logs it), so the lift
407
+ // The ended hold's kind rides the resumed event (pollTeamHold logs it), so the lift
344
408
  // names what actually ended — the same split as the hold line above: "429 hold lifted"
345
409
  // after a connection-error hold would be a lie about a connection error.
346
410
  if (e.kind && e.kind !== "rate-limit")
347
- return `backend hold lifted (${backendKindPhrase(e.kind)})${providerAt(e)} — role loops on ${providerScope(e)} tick again`;
348
- return `429 hold lifted${providerAt(e)} — role loops on ${providerScope(e)} tick again`;
411
+ return `backend recovered (${backendKindPhrase(e.kind)})${providerAt(e)} — roles on ${providerScope(e)} run again`;
412
+ return `provider limit lifted${providerAt(e)} — roles on ${providerScope(e)} run again`;
349
413
  }
350
414
  case "claude_window_warning":
351
415
  // Routine-with-explanation, like rate_limit_hold — no warning prefix: the account is
352
416
  // telling us the window is filling, not failing. It names the window and when it resets.
353
- return `claude subscription window ${textOr(e.windowType)} near its limit${windowResetPhrase(e)} — subscription claude seams keep ticking`;
417
+ return `claude subscription window ${textOr(e.windowType)} near its limit${windowResetPhrase(e)} — subscription claude model jobs keep running`;
418
+ case "claude_window_reserved": {
419
+ const used = finiteNumber(e.utilization, null);
420
+ const line = finiteNumber(e.reserve, null);
421
+ const pct = (f) => (f === null ? "?" : `${Math.round(f * 100)}%`);
422
+ return `claude subscription window ${textOr(e.windowType)} at ${pct(used)}, past its ${pct(line)} reserve${windowResetPhrase(e)} — the team leaves it to you (director keeps running)`;
423
+ }
354
424
  case "claude_window_limited":
355
425
  // Routine-with-explanation, like rate_limit_hold — no warning prefix: waiting is the
356
426
  // harness handling the account's own throttle. Names the window, the reset, and which
357
427
  // seams stop, so the operator knows why claude held and what keeps running.
358
- return `claude subscription window ${textOr(e.windowType)} rejected — claude seams start nothing new${windowResetPhrase(e)} (director keeps running)`;
428
+ // With a fallback the work moves rather than waits: `tiers` names where each moved tier's
429
+ // work went (the budget poll's window move), the director and any tier with no fallback
430
+ // still wait.
431
+ {
432
+ const moved = fallbackTierEntries(isJsonObject(e.tiers) ? e.tiers : undefined);
433
+ if (moved.length > 0)
434
+ return `claude subscription window ${textOr(e.windowType)} rejected — claude model jobs move to ${moved.join(", ")}${windowResetPhrase(e)} (director keeps waiting)`;
435
+ }
436
+ return `waiting: claude subscription window ${textOr(e.windowType)} rejected — claude model jobs wait${windowResetPhrase(e)} (director keeps running)`;
359
437
  case "claude_window_reset":
360
- return `claude subscription window reset — claude seams tick again`;
438
+ return `claude subscription window reset — claude model jobs run again`;
361
439
  case "max_concurrent_changed": {
362
440
  // Routine state change, like counters_reset — no warning prefix.
363
441
  return `maxConcurrent changed: ${textOr(e.from)} → ${textOr(e.to)}`;
364
442
  }
443
+ case "provider_concurrency_changed": {
444
+ // Routine state change, its maxConcurrent sibling's per-provider form — no warning prefix.
445
+ return `concurrency for ${textOr(e.provider)} changed: ${textOr(e.from)} → ${textOr(e.to)}`;
446
+ }
365
447
  case "retention_changed": {
366
448
  // Routine state change, like its maxConcurrent sibling — no warning prefix.
367
449
  return `sessionRetentionDays changed: ${textOr(e.from)} → ${textOr(e.to)}`;
@@ -376,53 +458,95 @@ export function eventMessage(e) {
376
458
  // The live-edit sibling of config_changed: names the new selector, the one fact the key
377
459
  // list cannot show. The per-role diffs stay structured on the event.
378
460
  return `model changed — now ${textOr(e.to, "pi's default")}`;
461
+ case "backend_binary":
462
+ // The binary check for a newly routed backend: found is the routine verdict; a missing
463
+ // binary is the warning.
464
+ return e.found === false
465
+ ? `warning: newly routed ${textOr(e.agent)} backend — its binary was not found`
466
+ : `newly routed ${textOr(e.agent)} backend checked — binary found`;
379
467
  case "model_fallback_started":
380
468
  // Routine-with-explanation, like rate_limit_hold: the switch IS the harness handling the
381
469
  // failure. Names the pair the role now runs on and why the primary was abandoned.
382
- return `model fallback — ${textOr(e.provider, "pi's default")}/${textOr(e.model, "pi's default")} engaged (primary failing: ${textOr(e.reason, "backend failure")})`;
470
+ return `model fallback — ${providerModelPair(e, "pi's default", "pi's default")} engaged (primary failing: ${textOr(e.reason, "backend failure")})`;
383
471
  case "model_fallback_ended":
384
472
  // The return counterpart: names the pair ticks resume on and how long the episode ran.
385
- return `model fallback ended — back on ${textOr(e.provider, "pi's default")}/${textOr(e.model, "pi's default")} after ${minutes(e.durationMs)}m`;
473
+ return `model fallback ended — back on ${providerModelPair(e, "pi's default", "pi's default")} after ${minutes(e.durationMs)}m`;
474
+ case "opencode_free_model_billed":
475
+ // The promotion ended (plans/opencode-backend.md §7, part 6c/7): a run billed what the
476
+ // catalog called free, so the team now prices that model as paid from here on. No warning
477
+ // prefix — the demotion is the harness handling the spend — but it names the model and the
478
+ // cost so the operator sees why a badge stopped saying "cost n/a".
479
+ return `opencode ${textOr(e.model, "model")} billed ${usd(finiteNumber(e.costUsd, 0))} — demoted from free to priced`;
480
+ case "image_blind": {
481
+ // Routine-with-explanation like the model-fallback events — no warning prefix: the
482
+ // run was stopped (or a gate run noted) because the model cannot see the image. Names
483
+ // the file, the model, and whether the tick failed on it or a gate run only logged it.
484
+ const action = e.action === "logged"
485
+ ? " (gate run logged)"
486
+ : e.action === "fail"
487
+ ? " — run failed"
488
+ : "";
489
+ return `blind image read: ${textOr(e.path)} on ${textOr(e.model, "the model")}${action}`;
490
+ }
491
+ case "image_reroute":
492
+ // The tick's work moves to a model that can read the image (plans/image-input.md part
493
+ // 4/5). Routine-with-explanation — no warning prefix: names the hop the way the
494
+ // handoff reads, and whether the traits-of-attachment preflight avoided a failed tick.
495
+ return e.preflight === true
496
+ ? `image attachment routed to ${textOr(e.to, "the target")} (${textOr(e.tier, "a peer tier")}) to read its image — no failed run`
497
+ : `image rerouted to ${textOr(e.to, "the target")} (${textOr(e.tier, "a peer tier")}) — next run goes there after a blind read on ${textOr(e.from, "the prior model")}`;
386
498
  case "build_stale":
387
- // Self-hosting fleets only (src/redeploy/redeploy.ts): the code main describes is not the
499
+ // Self-hosting teams only (src/redeploy/redeploy.ts): the code main describes is not the
388
500
  // code running. Not a warning prefix — a stale build is a state, and auto-restart resolves
389
501
  // it.
390
- return `build ${shortSha(e.build)} is stale — main ${shortSha(e.head)} is ${plural(finiteNumber(e.aheadCommits, 0), "commit")} ahead in src/`;
502
+ return `update available (${plural(finiteNumber(e.aheadCommits, 0), "commit")} behind) — restarts when idle`;
391
503
  case "restart_pending":
392
- return `restart pending — main ${shortSha(e.head)} is green; compiling and draining in-flight ticks (no new ticks start)`;
504
+ return `restart pending — main ${shortSha(e.head)} is green; finishing current runs (no new runs start)`;
393
505
  case "restart": {
394
- const aborted = finiteNumber(e.abortedTicks, 0);
395
- return `restarting onto build ${shortSha(e.to)} (drained ${minutes(e.drainedMs)}m${aborted > 0 ? `, ${aborted} tick(s) will resume on the new build` : ""})`;
506
+ const aborted = finiteNumber(e.abortedRuns, 0);
507
+ return `restarting onto update ${shortSha(e.to)} (finished current work in ${minutes(e.finishedMs)}m${aborted > 0 ? `, ${aborted} run(s) resume after the restart` : ""})`;
396
508
  }
397
509
  case "restart_refused":
398
510
  // The restart is refused, not failed: the running build stays and the gate is re-asked
399
511
  // every poll, so the line names both builds and what the operator must repair.
400
- return `restart onto build ${shortSha(e.to)} refused — the new build could not start here: ${textOr(e.reason)}; staying on build ${shortSha(e.from)} until it can`;
512
+ return `restart onto update ${shortSha(e.to)} refused — the new version could not start here: ${textOr(e.reason)}; staying on version ${shortSha(e.from)} until it can`;
401
513
  case "restart_blocked":
402
514
  // The restart is blocked, not refused: a verdict latched this head, so the line names
403
515
  // what failed and that only main moving can end it.
404
- return `restart blocked for main ${shortSha(e.to)} — ${textOr(e.reason)}; staying on build ${shortSha(e.from)} until main moves`;
516
+ return `restart blocked for main ${shortSha(e.to)} — ${textOr(e.reason)}; staying on version ${shortSha(e.from)} until main moves`;
517
+ case "restart_forced":
518
+ // The operator's own override: names the running build, the one fact the type code lacks.
519
+ return `restart forced by you — the update skips its cooldown (running version ${shortSha(e.build)})`;
405
520
  case "supervisor_exit": {
406
- // The fleet is DOWN and nothing will bring it back: the one line that must say so, since
521
+ // The team is DOWN and nothing will bring it back: the one line that must say so, since
407
522
  // the dead generation's own stderr reached only the supervisor's terminal.
408
523
  const how = e.signal ? `was killed by ${textOr(e.signal)}` : `exited ${textOr(e.code)}`;
409
- return `fleet down — generation ${textOr(e.generation)} ${how}${e.reason ? `: ${textOr(e.reason)}` : ""}; the supervisor exited (restart with \`tumwater run\`)`;
524
+ return `tumwater is down — restart ${textOr(e.generation)} ${how}${e.reason ? `: ${textOr(e.reason)}` : ""}; start it again with \`tumwater start\``;
410
525
  }
411
526
  case "resume":
412
527
  // Two causes share the resume machinery; the line names the real one so an operator
413
528
  // reading the feed can tell a restart from a loop fighting the context ceiling, and a
414
529
  // cross-agent handoff from either — the handoff keeps the worktree but starts fresh.
415
530
  return e.cause === "cut-off"
416
- ? "resuming the run cut off at the context ceiling (compacted pi session, same worktree)"
531
+ ? "resuming the run cut off at the context ceiling (compacted pi session, same workspace)"
417
532
  : e.cause === "handoff"
418
- ? "handing the interrupted tick off to a fresh session on another agent (same worktree, uncommitted edits kept)"
419
- : "resuming the tick a shutdown interrupted (same pi session and worktree)";
533
+ ? "handing the interrupted run off to a fresh session on another agent (same workspace, uncommitted edits kept)"
534
+ : e.cause === "image-reroute"
535
+ ? "handing the blind-read run off to a fresh session on an image-capable model (same workspace, uncommitted edits kept)"
536
+ : "resuming the run a shutdown interrupted (same agent session and workspace)";
420
537
  case "slot_wait": {
421
538
  // Routine-with-explanation, like rate_limit_hold — no warning prefix: waiting is the pool
422
539
  // working as designed. Names the wait, the pool it waited on, and how many slots are
423
540
  // pinned, so the operator can raise worktreeSlots with the numbers in hand.
424
- const purpose = e.purpose === "vet" ? "vet" : "tick";
425
- return `waited ${shortSpanPhrase(finiteNumber(e.waitedMs, 0))} for a free worktree slot (${textOr(e.role)} ${purpose}; ${textOr(e.slots)} slots, ${textOr(e.pinned)} pinned)`;
541
+ const purpose = e.purpose === "verify" ? "check" : "run";
542
+ return `waited ${shortSpanPhrase(finiteNumber(e.waitedMs, 0))} to start (${textOr(e.role)} ${purpose}; ${textOr(e.slots)} workspaces, ${textOr(e.pinned)} pinned)`;
543
+ }
544
+ case "provider_wait": {
545
+ // Routine-with-explanation, like slot_wait: waiting for a provider/team slot is the cap
546
+ // working as designed, so no warning prefix. Names the wait and the key (or the run cap
547
+ // when the seam has none), so the operator can raise maxConcurrentPerProvider with the
548
+ // numbers in hand.
549
+ return `waited ${shortSpanPhrase(finiteNumber(e.waitedMs, 0))} for a ${typeof e.key === "string" && e.key.length > 0 ? `${e.key} slot` : "run slot"} (${textOr(e.role)})`;
426
550
  }
427
551
  case "claim":
428
552
  // Parallel work instances (plans/parallel-work-instances.md "Observability", part 6/7):
@@ -434,7 +558,7 @@ export function eventMessage(e) {
434
558
  // Priority headroom (part 7/7): a reserve appearing or vanishing is the scheduler working
435
559
  // as designed; it names the permits withheld for the work tier so an operator can see why
436
560
  // maintenance concurrency dipped.
437
- return `${finiteNumber(e.reserve, 0)} permit(s) reserved for work loops that have work to take`;
561
+ return `${finiteNumber(e.reserve, 0)} permit(s) reserved for work roles that have work to take`;
438
562
  case "warning":
439
563
  return `warning: ${textOr(e.message)}`;
440
564
  default: