tumwater 0.4.0 → 0.4.2

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 (291) hide show
  1. package/README.md +4 -6
  2. package/dist/build-info.json +2 -2
  3. package/dist/src/agent/agent-backend.js +5 -2
  4. package/dist/src/agent/backend-traits.js +21 -0
  5. package/dist/src/agent/claude/claude-backend.js +1 -1
  6. package/dist/src/agent/claude/claude-stream.js +181 -182
  7. package/dist/src/agent/claude/{write-guard-hook.js → claude-write-guard-hook.js} +1 -1
  8. package/dist/src/agent/claude/claude-write-guard.js +5 -4
  9. package/dist/src/agent/codex/codex-backend.js +1 -1
  10. package/dist/src/agent/codex/codex-rollout.js +48 -45
  11. package/dist/src/agent/codex/codex-stream.js +269 -215
  12. package/dist/src/agent/codex/{write-guard-hook.js → codex-write-guard-hook.js} +2 -1
  13. package/dist/src/agent/codex/codex-write-guard.js +3 -3
  14. package/dist/src/agent/image-input.js +18 -87
  15. package/dist/src/agent/notes-shim.js +2 -2
  16. package/dist/src/agent/opencode/opencode-backend.js +2 -2
  17. package/dist/src/agent/opencode/opencode-catalog.js +2 -2
  18. package/dist/src/agent/opencode/opencode-config.js +2 -2
  19. package/dist/src/agent/opencode/opencode-stream.js +101 -92
  20. package/dist/src/agent/run-agent.js +76 -382
  21. package/dist/src/agent/run-settle.js +25 -15
  22. package/dist/src/agent/run-spawn.js +226 -0
  23. package/dist/src/agent/run-stream.js +185 -0
  24. package/dist/src/agent/session-ledger.js +16 -0
  25. package/dist/src/agent/write-guard.js +5 -3
  26. package/dist/src/backlog/backlog-archive.js +86 -170
  27. package/dist/src/backlog/backlog-cli-view.js +89 -70
  28. package/dist/src/backlog/backlog-eligibility.js +51 -59
  29. package/dist/src/backlog/backlog-file-bugs.js +133 -0
  30. package/dist/src/backlog/backlog-index-data.js +159 -0
  31. package/dist/src/backlog/backlog-index.js +7 -5
  32. package/dist/src/backlog/backlog-md.js +39 -28
  33. package/dist/src/backlog/backlog-release.js +21 -36
  34. package/dist/src/backlog/backlog-render.js +1 -1
  35. package/dist/src/backlog/backlog-structure.js +14 -145
  36. package/dist/src/backlog/backlog-write.js +31 -18
  37. package/dist/src/backlog/backlog.js +14 -4
  38. package/dist/src/backlog/question-answer.js +33 -15
  39. package/dist/src/baseline/main-baseline.js +71 -26
  40. package/dist/src/baseline/main-guardrails.js +76 -15
  41. package/dist/src/baseline/main-red.js +45 -94
  42. package/dist/src/baseline/main-watch.js +9 -3
  43. package/dist/src/brief/brief.js +40 -0
  44. package/dist/src/budget/budget-fallback-view.js +50 -8
  45. package/dist/src/budget/budget-wiring.js +15 -11
  46. package/dist/src/budget/budget.js +3 -2
  47. package/dist/src/build/build-check-events.js +4 -2
  48. package/dist/src/build/build-check-report.js +2 -1
  49. package/dist/src/build/build-check-scoped.js +70 -41
  50. package/dist/src/change/change-data.js +4 -2
  51. package/dist/src/cli/cli-args.js +9 -0
  52. package/dist/src/cli/cli-command-args.js +199 -98
  53. package/dist/src/cli/cli-flag-specs.js +6 -4
  54. package/dist/src/cli/cli-marker-commands.js +2 -2
  55. package/dist/src/cli/cli-query-commands.js +41 -6
  56. package/dist/src/cli/cli-review-commands.js +2 -1
  57. package/dist/src/cli/cli-role-command.js +46 -92
  58. package/dist/src/cli/cli-start.js +123 -47
  59. package/dist/src/cli/follow-view.js +3 -1
  60. package/dist/src/cli/help.js +19 -12
  61. package/dist/src/cli/log-commands.js +128 -107
  62. package/dist/src/cli/question-commands.js +82 -63
  63. package/dist/src/cli.js +396 -343
  64. package/dist/src/collections.js +46 -9
  65. package/dist/src/config/config-fallbacks.js +0 -4
  66. package/dist/src/config/config-live.js +179 -147
  67. package/dist/src/config/config-request.js +176 -51
  68. package/dist/src/config/config-validation-roles.js +271 -0
  69. package/dist/src/config/config-validation.js +10 -205
  70. package/dist/src/config/config-views.js +2 -7
  71. package/dist/src/config/config-write.js +205 -175
  72. package/dist/src/config/config.js +2 -1
  73. package/dist/src/doctor/doctor-backlog-bugs.js +24 -122
  74. package/dist/src/doctor/doctor-backlog-plans.js +24 -25
  75. package/dist/src/doctor/doctor-backlog.js +34 -42
  76. package/dist/src/doctor/doctor-check-names.js +4 -2
  77. package/dist/src/doctor/doctor-checks.js +16 -4
  78. package/dist/src/doctor/doctor-claude-checks.js +2 -2
  79. package/dist/src/doctor/doctor-codex-checks.js +9 -20
  80. package/dist/src/doctor/doctor-contention-checks.js +52 -19
  81. package/dist/src/doctor/doctor-holds.js +22 -20
  82. package/dist/src/doctor/doctor-model-checks.js +8 -184
  83. package/dist/src/doctor/doctor-notify.js +2 -5
  84. package/dist/src/doctor/doctor-opencode-checks.js +90 -41
  85. package/dist/src/doctor/doctor-principles.js +26 -0
  86. package/dist/src/doctor/doctor-project-checks.js +69 -19
  87. package/dist/src/doctor/doctor-role-notes.js +33 -0
  88. package/dist/src/doctor/doctor.js +13 -6
  89. package/dist/src/doctor/probe.js +1 -1
  90. package/dist/src/events/event-format.js +307 -162
  91. package/dist/src/events/event-labels.js +0 -1
  92. package/dist/src/events/event-legacy.js +1 -1
  93. package/dist/src/events/event-window.js +1 -1
  94. package/dist/src/events/events.js +15 -2
  95. package/dist/src/events/notify.js +1 -1
  96. package/dist/src/failure/failure-cluster.js +4 -3
  97. package/dist/src/failure/failure-data.js +126 -94
  98. package/dist/src/failure/failure-render.js +58 -17
  99. package/dist/src/failure/failure-state-change.js +308 -27
  100. package/dist/src/failure/time-spend.js +139 -106
  101. package/dist/src/files/files.js +7 -0
  102. package/dist/src/files/json-object.js +15 -2
  103. package/dist/src/gates/budget-gates.js +90 -68
  104. package/dist/src/gates/gate-polls.js +92 -61
  105. package/dist/src/gates/role-cap-gates.js +1 -1
  106. package/dist/src/gates/startup-gate.js +28 -28
  107. package/dist/src/gates/streak-gate.js +1 -1
  108. package/dist/src/git/git.js +1 -1
  109. package/dist/src/git/slot-salvage.js +9 -9
  110. package/dist/src/git/slots-state.js +12 -3
  111. package/dist/src/git/worktree-pool-startup.js +127 -0
  112. package/dist/src/git/worktree-pool.js +1 -120
  113. package/dist/src/gui/gui-admin-endpoints.js +3 -4
  114. package/dist/src/gui/gui-args.js +1 -1
  115. package/dist/src/gui/gui-backlog-commands.js +20 -11
  116. package/dist/src/gui/gui-backlog-endpoints.js +2 -8
  117. package/dist/src/gui/gui-config-commands.js +51 -76
  118. package/dist/src/gui/gui-endpoint-commands.js +4 -2
  119. package/dist/src/gui/gui-endpoint-common.js +4 -3
  120. package/dist/src/gui/gui-history-endpoints.js +36 -27
  121. package/dist/src/gui/gui-operator-commands.js +43 -17
  122. package/dist/src/gui/gui-prompt-commands.js +27 -21
  123. package/dist/src/gui/gui-quarantine-commands.js +30 -0
  124. package/dist/src/gui/gui-report-endpoints.js +13 -9
  125. package/dist/src/gui/gui-server.js +5 -3
  126. package/dist/src/gui/gui-team-commands.js +1 -1
  127. package/dist/src/history/history-data.js +7 -1
  128. package/dist/src/history/history-render.js +15 -10
  129. package/dist/src/history/history.js +103 -68
  130. package/dist/src/inbox/inbox-submit.js +1 -5
  131. package/dist/src/inbox/pending-prompt.js +3 -2
  132. package/dist/src/inbox/prompt-commands.js +194 -161
  133. package/dist/src/init/init.js +104 -25
  134. package/dist/src/merge/merge-core.js +1 -1
  135. package/dist/src/merge/merge-drain.js +1 -1
  136. package/dist/src/merge/merge-git.js +5 -5
  137. package/dist/src/merge/{merge-post-review.js → merge-post-review-drain.js} +1 -1
  138. package/dist/src/merge/merge-queue.js +2 -1
  139. package/dist/src/merge/merge-to-main.js +1 -1
  140. package/dist/src/merge/merge-verify.js +3 -2
  141. package/dist/src/operator/operator-commands.js +84 -52
  142. package/dist/src/operator/operator-intent.js +26 -3
  143. package/dist/src/operator/retire.js +2 -1
  144. package/dist/src/operator/revert.js +18 -12
  145. package/dist/src/orchestrator/orchestrator-poll.js +2 -2
  146. package/dist/src/orchestrator/orchestrator.js +1 -1
  147. package/dist/src/orchestrator/retention.js +6 -5
  148. package/dist/src/paths.js +10 -10
  149. package/dist/src/pi/pi-bin.js +2 -2
  150. package/dist/src/pi/pi-models.js +9 -0
  151. package/dist/src/pi/pi-stream.js +63 -41
  152. package/dist/src/pi/pi-watchdogs.js +84 -49
  153. package/dist/src/pi-extension/context-shake.js +55 -31
  154. package/dist/src/pi-extension/full-output.js +2 -3
  155. package/dist/src/process/process.js +24 -1
  156. package/dist/src/process/run-marker.js +2 -2
  157. package/dist/src/process/supervisor.js +24 -5
  158. package/dist/src/prompt/gate-notes.js +41 -16
  159. package/dist/src/prompt/notes-backlog.js +22 -29
  160. package/dist/src/prompt/principles.js +11 -0
  161. package/dist/src/prompt/prompt-followup.js +4 -12
  162. package/dist/src/prompt/prompt.js +15 -12
  163. package/dist/src/redeploy/redeploy-probes.js +11 -1
  164. package/dist/src/redeploy/redeployer.js +54 -7
  165. package/dist/src/redeploy/self-reload.js +7 -2
  166. package/dist/src/report/report-data.js +224 -320
  167. package/dist/src/report/report-fold.js +23 -231
  168. package/dist/src/report/report-main-red.js +70 -3
  169. package/dist/src/report/report-render.js +181 -48
  170. package/dist/src/report/report-since-data.js +218 -0
  171. package/dist/src/report/report-usage-fold.js +288 -0
  172. package/dist/src/report/report.js +2 -1
  173. package/dist/src/review/known-flakes.js +22 -3
  174. package/dist/src/review/post-merge-review-accept.js +3 -2
  175. package/dist/src/review/post-merge-review.js +16 -14
  176. package/dist/src/review/quarantine-render.js +29 -11
  177. package/dist/src/review/review-precheck.js +2 -1
  178. package/dist/src/roles/role-catalog.js +30 -29
  179. package/dist/src/roles/role-guidance.js +26 -43
  180. package/dist/src/roles/role-render.js +19 -0
  181. package/dist/src/roles/role-view.js +19 -5
  182. package/dist/src/roles/roles.js +2 -22
  183. package/dist/src/run/ci-parity.js +5 -6
  184. package/dist/src/run/feedback-filings.js +10 -1
  185. package/dist/src/run/qa-coverage.js +4 -4
  186. package/dist/src/run/run-apply.js +14 -2
  187. package/dist/src/run/run-detail-data.js +50 -10
  188. package/dist/src/run/run-detail-render.js +32 -1
  189. package/dist/src/run/run-detail.js +44 -22
  190. package/dist/src/run/run-dismiss.js +43 -0
  191. package/dist/src/run/run-finalize.js +75 -46
  192. package/dist/src/run/run-prompt.js +271 -187
  193. package/dist/src/run/run-resume.js +32 -31
  194. package/dist/src/run/run-stage.js +116 -64
  195. package/dist/src/run/run-timing.js +18 -4
  196. package/dist/src/run/run-verdict.js +119 -83
  197. package/dist/src/run/stage-backlog-checks.js +4 -31
  198. package/dist/src/run/stage-check.js +4 -12
  199. package/dist/src/run/stage-checks-shared.js +15 -0
  200. package/dist/src/run/stage-name-checks.js +3 -13
  201. package/dist/src/run/stage-path-checks.js +26 -29
  202. package/dist/src/runner/leftover.js +13 -4
  203. package/dist/src/runner/revision.js +2 -2
  204. package/dist/src/runner/runner-fallback.js +1 -1
  205. package/dist/src/runner/runner-outcome.js +0 -5
  206. package/dist/src/runner/runner-pi.js +72 -49
  207. package/dist/src/runner/runner-run.js +19 -102
  208. package/dist/src/runner/runner-state-load.js +0 -10
  209. package/dist/src/runner/runner.js +1 -32
  210. package/dist/src/scheduling/claims.js +4 -11
  211. package/dist/src/status/status-data.js +140 -91
  212. package/dist/src/status/status-polls.js +41 -5
  213. package/dist/src/status/status-role-notes.js +53 -0
  214. package/dist/src/status/status-roles.js +10 -37
  215. package/dist/src/text/datetime.js +8 -1
  216. package/dist/src/text/format.js +13 -1
  217. package/dist/src/text/phrases.js +16 -2
  218. package/dist/src/text/text.js +12 -0
  219. package/dist/src/ui/alert-feeds.js +115 -0
  220. package/dist/src/ui/badges.js +3 -4
  221. package/dist/src/ui/gui/gui-client-boot.js +78 -57
  222. package/dist/src/ui/gui/gui-client-drawer-config.js +8 -3
  223. package/dist/src/ui/gui/gui-client-drawer.js +18 -2
  224. package/dist/src/ui/gui/gui-client-feed.js +3 -8
  225. package/dist/src/ui/gui/gui-client-history.js +16 -16
  226. package/dist/src/ui/gui/gui-client-model.js +9 -9
  227. package/dist/src/ui/gui/gui-client-operator.js +6 -10
  228. package/dist/src/ui/gui/gui-client-pending.js +4 -4
  229. package/dist/src/ui/gui/gui-client-report-chart.js +111 -10
  230. package/dist/src/ui/gui/gui-client-report.js +70 -11
  231. package/dist/src/ui/gui/gui-client-roles.js +52 -12
  232. package/dist/src/ui/gui/gui-client-settings.js +31 -26
  233. package/dist/src/ui/gui/gui-client-team.js +28 -2
  234. package/dist/src/ui/gui/gui-client.js +33 -4
  235. package/dist/src/ui/gui/gui-icons.js +7 -1
  236. package/dist/src/ui/progress-data.js +2 -2
  237. package/dist/src/ui/run-progress-model.js +2 -2
  238. package/dist/src/ui/status-model.js +26 -16
  239. package/dist/src/ui/status-payload.js +149 -326
  240. package/dist/src/ui/status-render.js +131 -80
  241. package/dist/src/ui/team-alert-backlog.js +236 -0
  242. package/dist/src/ui/team-alert-shared.js +92 -0
  243. package/dist/src/ui/team-alert-status.js +326 -0
  244. package/dist/src/ui/team-alert-tail.js +238 -0
  245. package/dist/src/ui/team-alerts.js +16 -984
  246. package/dist/src/ui/transcript-tail.js +137 -94
  247. package/dist/src/ui/tui/tui-backlog.js +1 -1
  248. package/dist/src/ui/tui/tui-editor-answer.js +54 -44
  249. package/dist/src/ui/tui/tui-editor-attach.js +74 -63
  250. package/dist/src/ui/tui/tui-editor-backends.js +62 -56
  251. package/dist/src/ui/tui/tui-editor-brief.js +64 -54
  252. package/dist/src/ui/tui/tui-editor-budget.js +106 -89
  253. package/dist/src/ui/tui/tui-editor-config.js +2 -4
  254. package/dist/src/ui/tui/tui-editor-ctx.js +12 -1
  255. package/dist/src/ui/tui/tui-editor-entry.js +171 -153
  256. package/dist/src/ui/tui/tui-editor-instances.js +2 -1
  257. package/dist/src/ui/tui/tui-editor-instructions.js +50 -107
  258. package/dist/src/ui/tui/tui-editor-interval.js +39 -112
  259. package/dist/src/ui/tui/tui-editor-notes.js +28 -84
  260. package/dist/src/ui/tui/tui-editor-pause.js +210 -183
  261. package/dist/src/ui/tui/tui-editor-prompt.js +46 -35
  262. package/dist/src/ui/tui/tui-editor-queued.js +74 -64
  263. package/dist/src/ui/tui/tui-editor-quiet.js +122 -108
  264. package/dist/src/ui/tui/tui-editor-release.js +85 -0
  265. package/dist/src/ui/tui/tui-editor-retire.js +57 -46
  266. package/dist/src/ui/tui/tui-editor-role-enabled.js +90 -80
  267. package/dist/src/ui/tui/tui-editor-role-file.js +87 -0
  268. package/dist/src/ui/tui/tui-editor-role-key.js +50 -42
  269. package/dist/src/ui/tui/tui-editor-search.js +70 -58
  270. package/dist/src/ui/tui/tui-editor-session.js +3 -0
  271. package/dist/src/ui/tui/tui-editors.js +4 -1
  272. package/dist/src/ui/tui/tui-frame.js +7 -3
  273. package/dist/src/ui/tui/tui-history.js +12 -10
  274. package/dist/src/ui/tui/tui-keys.js +6 -1
  275. package/dist/src/ui/tui/tui-pane-activity.js +62 -50
  276. package/dist/src/ui/tui/tui-pane-backlog.js +4 -4
  277. package/dist/src/ui/tui/tui-pane-ctx.js +1 -1
  278. package/dist/src/ui/tui/tui-pane-health.js +45 -26
  279. package/dist/src/ui/tui/tui-pane-history.js +296 -264
  280. package/dist/src/ui/tui/tui-pane-report.js +74 -57
  281. package/dist/src/ui/tui/tui-pane-session.js +6 -2
  282. package/dist/src/ui/tui/tui-pane-transcript.js +158 -131
  283. package/dist/src/ui/tui/tui-pane.js +201 -174
  284. package/dist/src/ui/tui/tui-panes.js +211 -148
  285. package/dist/src/ui/tui/tui.js +4 -4
  286. package/dist/src/verdict/fix-claim.js +18 -6
  287. package/dist/src/verdict/refusal.js +32 -7
  288. package/package.json +1 -1
  289. package/dist/src/doctor/image-probe.js +0 -257
  290. /package/dist/src/gui/{http-body.js → gui-http-body.js} +0 -0
  291. /package/dist/src/merge/{backlog-conflicts.js → merge-backlog-conflicts.js} +0 -0
@@ -3,7 +3,7 @@ import { eventRole, eventRun, eventUsage } from "./event-read.js";
3
3
  import { eventTypeLabel } from "./event-labels.js";
4
4
  import { roleLabel } from "../roles/role-labels.js";
5
5
  import { TUMWATER_SOURCE } from "./events.js";
6
- import { orchestratorStopPhrase, backendKindPhrase, budgetPhrase, firstReason, teamPausePhrase, teamResumePhrase, holdPhrase, plural, rolePausePhrase, roleResumePhrase, rolesPhrase, shortSpanPhrase, PAUSE_EXPIRED_SUFFIX } from "../text/phrases.js";
6
+ import { orchestratorStopPhrase, backendKindPhrase, budgetPhrase, firstReason, teamPausePhrase, teamResumePhrase, holdPhrase, plural, rolePausePhrase, roleResumePhrase, rolesPhrase, shortSpanPhrase, rendersAsBackendFailure, PAUSE_EXPIRED_SUFFIX } from "../text/phrases.js";
7
7
  import { compactTokens, formatGB, shortSha, usd } from "../text/format.js";
8
8
  import { padToWidth } from "../text/text-width.js";
9
9
  import { isNonBlankString, joinWithMore, squash, textOr, truncate } from "../text/text.js";
@@ -54,7 +54,7 @@ function elapsed(ms) {
54
54
  * missing or non-numeric reads as "0.0 GB", never "NaN" — the same corrupt-field rule
55
55
  * budgetPhrase applies to spentUsd/capUsd, since HarnessEvent's index signature hands every
56
56
  * field over unvalidated. The rounding and unit are text/format.ts's formatGB. */
57
- function gigabytes(v) {
57
+ export function gigabytes(v) {
58
58
  return formatGB(finiteNumber(v, 0));
59
59
  }
60
60
  /** A duration in whole minutes. A torn or hand-edited value that is missing or non-numeric
@@ -81,7 +81,7 @@ function pauseSuffix(until, reason) {
81
81
  /** `, resets HH:MM` for a subscription-window event carrying resetsAt, or "" when the field is
82
82
  * absent or non-numeric (a torn line, or a window the account did not stamp) — the same
83
83
  * corrupt-field rule the other optional-field renderers apply. */
84
- function windowResetPhrase(e) {
84
+ export function windowResetPhrase(e) {
85
85
  const at = finiteNumber(e.resetsAt, null);
86
86
  return at === null ? "" : `, resets ${formatClock(at)}`;
87
87
  }
@@ -89,7 +89,7 @@ function windowResetPhrase(e) {
89
89
  * the field is absent or blank (pollQuietHoursGate may send no window, so a routine hold must
90
90
  * not fabricate a pair). Shared by the started and ended lines so the two cannot drift on the
91
91
  * window's presence or format. */
92
- function windowSuffix(e) {
92
+ export function windowSuffix(e) {
93
93
  return isNonBlankString(e.window) ? ` (${textOr(e.window)})` : "";
94
94
  }
95
95
  /** The backend a hold event names, rendered as " at <provider>" when one is configured
@@ -108,7 +108,7 @@ function providerScope(e) {
108
108
  * later backend's own id), falling back to "claude" for an event written before the window
109
109
  * events carried a `backend`. A claude-only team's lines read exactly as the older
110
110
  * `claude_window_*` lines did. */
111
- function backendLabel(e) {
111
+ export function backendLabel(e) {
112
112
  return typeof e.backend === "string" && e.backend !== "" ? e.backend : "claude";
113
113
  }
114
114
  /** The provider/model pair a fallback-ish line names, as "provider/model", defaulting each
@@ -182,10 +182,7 @@ export function eventFeedItem(e) {
182
182
  export function eventMatchesGrep(e, patternLower) {
183
183
  return `${e.type} ${formatEvent(e)}`.toLowerCase().includes(patternLower);
184
184
  }
185
- /** An event's message alone — formatEvent's line without the time and role columns. The GUI's
186
- * activity feed renders the time and role itself (relative ages, role chips), so the payload
187
- * ships this beside the event's type and role instead of making the page re-parse a line. */
188
- export function eventMessage(e) {
185
+ function runEventMessage(e) {
189
186
  switch (e.type) {
190
187
  case "run_start":
191
188
  return `run #${eventRun(e)} started${e.model !== undefined ? ` on ${textOr(e.model)}` : ""}`;
@@ -198,6 +195,53 @@ export function eventMessage(e) {
198
195
  // Per-run usage: where the day's spend went.
199
196
  return `run #${eventRun(e)} ${eventTypeLabel(outcomeText(e))}${extra}${usagePhrase(e)}`;
200
197
  }
198
+ case "run_deferred":
199
+ // Routine state change (need-based prioritization), like counters_reset — no warning
200
+ // prefix. One per deferral episode; the run's own events cover the episode's end.
201
+ return `deferred — ${deferredReasonText(e)}`;
202
+ case "run_aborted":
203
+ // Routine state change (the user stopped one role's run), like counters_reset — no
204
+ // warning prefix. The resulting run_end line carries the user_aborted outcome.
205
+ return `run stopped by you`;
206
+ case "resume": {
207
+ // Two causes share the resume machinery; the line names the real one so an operator
208
+ // reading the feed can tell a restart from a role fighting the context ceiling, and a
209
+ // cross-agent handoff from either — the handoff keeps the worktree but starts fresh. A
210
+ // run that got a salvaged patch back (src/git/slot-salvage.ts) names the patch.
211
+ const restored = e.restored === undefined ? "" : ` — uncommitted edits restored from ${textOr(e.restored)}`;
212
+ return ((e.cause === "cut-off"
213
+ ? "resuming the run cut off at the context ceiling (compacted pi session, same workspace)"
214
+ : e.cause === "handoff"
215
+ ? "handing the interrupted run off to a fresh session on another agent (same workspace, uncommitted edits kept)"
216
+ : e.cause === "salvaged"
217
+ ? "handing the work an earlier run left unsaved to a fresh session"
218
+ : "resuming the run a shutdown interrupted (same agent session and workspace)") + restored);
219
+ }
220
+ case "wake":
221
+ return `woke (${textOr(e.reason)})`;
222
+ case "counters_reset": {
223
+ // One role → the event is filed under that role; several → one harness-level event
224
+ // listing them.
225
+ const roles = rolesPhrase(e.roles, "");
226
+ const scope = roles ? ` for ${roles}` : "";
227
+ return `counters reset${scope} (runs, merges, tokens, cost)`;
228
+ }
229
+ case "claim":
230
+ // Parallel work instances (plans/parallel-work-instances.md "Observability", part 6/7):
231
+ // the assignment/release is the harness working as designed, so no warning prefix.
232
+ return e.action === "released"
233
+ ? `released "${textOr(e.title)}" (${textOr(e.reason, "?")})`
234
+ : `assigned "${textOr(e.title)}"`;
235
+ case "permit_reserve":
236
+ // Priority headroom (part 7/7): a reserve appearing or vanishing is the scheduler working
237
+ // as designed; it names the permits withheld for the work tier so an operator can see why
238
+ // maintenance concurrency dipped.
239
+ return `${plural(finiteNumber(e.reserve, 0), "permit")} reserved for work roles that have work to take`;
240
+ default: return textOr(e.type);
241
+ }
242
+ }
243
+ function mergeEventMessage(e) {
244
+ switch (e.type) {
201
245
  case "merged":
202
246
  return `merged ${shortSha(e.commit)} to main — ${textOr(e.summary)}`;
203
247
  case "merge_queued":
@@ -226,58 +270,46 @@ export function eventMessage(e) {
226
270
  // The event names its reason itself (writeMergeOutcome); the review/check events still
227
271
  // carry the full text. An old event without `reason` renders the bare outcome.
228
272
  return `merge of ${shortSha(e.commit)} failed (${eventTypeLabel(outcomeText(e))}${isNonBlankString(e.reason) ? `: ${truncate(e.reason.trim(), 120)}` : ""})${elapsed(e.durationMs)}${isNonBlankString(e.summary) ? ` — ${truncate(e.summary, 80)}` : ""}`;
229
- case "question_posted":
230
- // Routine operation (a role asked the user something), not a warning.
231
- // The position is the `<n>` `questions answer` takes, so the notify page is actionable.
232
- 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>"\`` : ""}`;
233
- case "entry_refused":
234
- // Routine operation (a refusal note merged), not a warning — like question_posted.
235
- const files = Array.isArray(e.files) ? e.files.join(", ") : "";
236
- const titles = stringList(e.entries);
237
- if (titles.length === 0)
238
- return `entry refused: ${textOr(e.reason)} (${files})`;
239
- const more = titles.length > 1 ? ` (and ${titles.length - 1} more)` : "";
240
- return `entry refused: "${truncate(titles[0], 80)}"${more}: ${textOr(e.reason)} (${files})`;
241
- case "issue_filed":
242
- // The feedback observer's filing (plans/feedback-role.md 2/2) is a visible, auditable
243
- // result — routine operation like question_posted, not a warning. The number is the
244
- // issue the operator can open; a torn event without it reads "?" never "undefined".
245
- return `filed issue #${typeof e.issue === "number" ? e.issue : "?"} ${textOr(e.title)} on znation/tumwater`;
246
- case "wake":
247
- return `woke (${textOr(e.reason)})`;
248
- case "run_deferred":
249
- // Routine state change (need-based prioritization), like counters_reset — no warning
250
- // prefix. One per deferral episode; the run's own events cover the episode's end.
251
- return `deferred — ${deferredReasonText(e)}`;
252
- case "orchestrator_start":
253
- return `tumwater started (pid ${textOr(e.pid)}${e.build ? `, version ${shortSha(e.build)}` : ""})`;
254
- case "orchestrator_stop":
255
- return orchestratorStopPhrase(e.cause);
256
- case "prompt_enqueued":
257
- return `message added to the inbox: ${textOr(e.preview)}`;
258
- case "prompt_cancelled":
259
- // Routine operation (the user removed a queued prompt), not a warning.
260
- return `message removed from the inbox: ${textOr(e.preview)}`;
261
- case "prompt_edited":
262
- // Routine operation (the user corrected a queued prompt), not a warning.
263
- return `inbox message edited: ${textOr(e.preview)}`;
264
- case "prompt_topped":
265
- // Routine operation (the user reordered a queued prompt), not a warning.
266
- return `inbox message moved to the front: ${textOr(e.preview)}`;
267
- case "prompt_taken":
268
- // Routine: a run dequeued the operator's request to run it.
269
- return `inbox message taken by run #${eventRun(e)}: ${textOr(e.preview)}`;
270
- case "counters_reset": {
271
- // One role → the event is filed under that role; several → one harness-level event
272
- // listing them.
273
- const roles = rolesPhrase(e.roles, "");
274
- const scope = roles ? ` for ${roles}` : "";
275
- return `counters reset${scope} (runs, merges, tokens, cost)`;
273
+ case "conflict_handback": {
274
+ // A change whose merges kept conflicting was handed back to its author with the markers
275
+ // in place (PLANS.md "Robust conflict landing, part 2/2"): queued, applied, or failed.
276
+ const files = stringList(e.conflicted);
277
+ const where = files.length > 0 ? `: ${files.join(", ")}` : "";
278
+ const why = e.reason === "revision" ? " (revision)" : "";
279
+ return `conflict hand-back ${textOr(e.action)} ${shortSha(e.sha)}${why}${where}`;
276
280
  }
277
- case "run_aborted":
278
- // Routine state change (the user stopped one role's run), like counters_reset — no
279
- // warning prefix. The resulting run_end line carries the user_aborted outcome.
280
- return `run stopped by you`;
281
+ case "operator_revert": {
282
+ const undid = typeof e.runner === "string" && e.runner !== "" ? `, undid ${e.runner}` : "";
283
+ return `reverted ${shortSha(String(e.commit ?? ""))} (by operator) as ${shortSha(String(e.revert ?? ""))}${undid}`; // vocabulary-ok: the deliberate-revert writer is the operator role
284
+ }
285
+ case "main_check": {
286
+ const failing = Array.isArray(e.failingTests) ? e.failingTests.map(String) : [];
287
+ // The commit count renders only when it is a real number over 1 — the corrupt-field rule
288
+ // this file applies to every event number (a hand-edited line's quoted "3" or "0x10" reads
289
+ // as no covered count, not as a coerced parenthetical).
290
+ const coveredN = finiteNumber(e.covered, null);
291
+ const covered = coveredN !== null && coveredN > 1 ? ` (${coveredN} commits)` : "";
292
+ const verdict = e.status === "failed" ? "failed" : "passed";
293
+ const names = failing.length > 0 ? `: ${joinWithMore(failing, 3, "; ")}` : "";
294
+ return `main ${shortSha(String(e.sha ?? ""))}${covered} full check ${verdict}${names}`;
295
+ }
296
+ case "main_culprit": {
297
+ const by = typeof e.runner === "string" && e.runner !== "" ? ` (merged by ${e.runner})` : "";
298
+ return `main turned red at ${shortSha(String(e.commit ?? ""))}${by}`;
299
+ }
300
+ case "main_red_hold":
301
+ // The message already names the SHA, script, and consequence; the problem tone comes
302
+ // from PROBLEM_EVENTS.
303
+ return textOr(e.message);
304
+ default: return textOr(e.type);
305
+ }
306
+ }
307
+ /** " after merge" when the review is a post-merge review (review.afterMerge), else "". */
308
+ function afterMergeSuffix(e) {
309
+ return e.afterMerge === true ? " after merge" : "";
310
+ }
311
+ function reviewEventMessage(e) {
312
+ switch (e.type) {
281
313
  case "review_start": {
282
314
  // A resumed review continues a timed-out reviewer's session (PLANS.md, review-retry part
283
315
  // 3/3). The verb names that, because a bare "reviewing" line reads as a from-scratch run;
@@ -290,7 +322,7 @@ export function eventMessage(e) {
290
322
  return `${e.resumed ? "review resumed" : "reviewing"} ${shortSha(e.head)}${revision}${tier}${model}`;
291
323
  }
292
324
  case "review_verdict":
293
- return `review approved ${shortSha(e.head)}${e.afterMerge === true ? " after merge" : ""}${e.reason ? ` — ${textOr(e.reason)}` : ""}${elapsed(e.durationMs)}`;
325
+ return `review approved ${shortSha(e.head)}${afterMergeSuffix(e)}${e.reason ? ` — ${textOr(e.reason)}` : ""}${elapsed(e.durationMs)}`;
294
326
  case "review_rejected": {
295
327
  const reasons = stringList(e.reasons);
296
328
  // Only the first reason renders; when more exist, say so instead of letting the
@@ -299,7 +331,7 @@ export function eventMessage(e) {
299
331
  const more = reasons.length > 1
300
332
  ? ` (+${reasons.length - 1} more — the author's next run carries every reason)`
301
333
  : "";
302
- return `review rejected ${shortSha(e.head)}${e.afterMerge === true ? " after merge" : ""} — ${firstReason(reasons)}${more}${elapsed(e.durationMs)}`;
334
+ return `review rejected ${shortSha(e.head)}${afterMergeSuffix(e)} — ${firstReason(reasons)}${more}${elapsed(e.durationMs)}`;
303
335
  }
304
336
  case "review_failed":
305
337
  return `review failed for ${shortSha(e.head)}: ${textOr(e.message)} (commit kept for re-review)${elapsed(e.durationMs)}`;
@@ -307,28 +339,47 @@ export function eventMessage(e) {
307
339
  // A rejected change's revision round (plans/revise-rejected.md): applied onto current main,
308
340
  // conflicted with it, dropped by its author, or exhausted its rounds.
309
341
  return `revision ${textOr(e.round)} ${textOr(e.action)} ${shortSha(e.sha)}`;
310
- case "conflict_handback": {
311
- // A change whose merges kept conflicting was handed back to its author with the markers
312
- // in place (PLANS.md "Robust conflict landing, part 2/2"): queued, applied, or failed.
313
- const files = stringList(e.conflicted);
314
- const where = files.length > 0 ? `: ${files.join(", ")}` : "";
315
- const why = e.reason === "revision" ? " (revision)" : "";
316
- return `conflict hand-back ${textOr(e.action)} ${shortSha(e.sha)}${why}${where}`;
317
- }
318
- case "build_check":
319
- // The deterministic check's cost, per run: scope names which gate paid (the pre-merge review
320
- // gate, the red-main baseline check of main itself, or the merge lock's post-rebase re-check
321
- // of the tree about to merge). script carries the npm script name for an npm check but the
322
- // FULL configured command otherwise (checkScriptName in build/build-check.ts), so no "npm"
323
- // prefix is asserted here — the bare name reads correctly for both kinds ("npm test" would
324
- // misrender a cargo or make-based project's check).
325
- return `check (${textOr(e.scope)}): ${textOr(e.script)} ${eventTypeLabel(outcomeText(e))}${elapsed(e.durationMs)}`;
326
- case "dep_install": {
327
- // The root checkout's install catching up with a merged lockfile change — routine on
328
- // success; a failure also rides its own warning, which says what happens meanwhile.
329
- const pkgs = stringList(e.packages).join(", ");
330
- return `root install (${pkgs}) ${eventTypeLabel(outcomeText(e))}${e.error ? ` — ${textOr(e.error)}` : ""}${elapsed(e.durationMs)}`;
331
- }
342
+ default: return textOr(e.type);
343
+ }
344
+ }
345
+ function inboxEventMessage(e) {
346
+ switch (e.type) {
347
+ case "prompt_enqueued":
348
+ return `message added to the inbox: ${textOr(e.preview)}`;
349
+ case "prompt_cancelled":
350
+ // Routine operation (the user removed a queued prompt), not a warning.
351
+ return `message removed from the inbox: ${textOr(e.preview)}`;
352
+ case "prompt_edited":
353
+ // Routine operation (the user corrected a queued prompt), not a warning.
354
+ return `inbox message edited: ${textOr(e.preview)}`;
355
+ case "prompt_topped":
356
+ // Routine operation (the user reordered a queued prompt), not a warning.
357
+ return `inbox message moved to the front: ${textOr(e.preview)}`;
358
+ case "prompt_taken":
359
+ // Routine: a run dequeued the operator's request to run it.
360
+ return `inbox message taken by run #${eventRun(e)}: ${textOr(e.preview)}`;
361
+ case "question_posted":
362
+ // Routine operation (a role asked the user something), not a warning.
363
+ // The position is the `<n>` `questions answer` takes, so the notify page is actionable.
364
+ 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>"\`` : ""}`;
365
+ case "entry_refused":
366
+ // Routine operation (a refusal note merged), not a warning — like question_posted.
367
+ const files = Array.isArray(e.files) ? e.files.join(", ") : "";
368
+ const titles = stringList(e.entries);
369
+ if (titles.length === 0)
370
+ return `entry refused: ${textOr(e.reason)} (${files})`;
371
+ const more = titles.length > 1 ? ` (and ${titles.length - 1} more)` : "";
372
+ return `entry refused: "${truncate(titles[0], 80)}"${more}: ${textOr(e.reason)} (${files})`;
373
+ case "issue_filed":
374
+ // The feedback observer's filing (plans/feedback-role.md 2/2) is a visible, auditable
375
+ // result — routine operation like question_posted, not a warning. The number is the
376
+ // issue the operator can open; a torn event without it reads "?" never "undefined".
377
+ return `filed issue #${typeof e.issue === "number" ? e.issue : "?"} ${textOr(e.title)} on znation/tumwater`;
378
+ default: return textOr(e.type);
379
+ }
380
+ }
381
+ function budgetEventMessage(e) {
382
+ switch (e.type) {
332
383
  case "budget_warning":
333
384
  // The early page beside budget_paused: the cap is not reached yet, so say so — the
334
385
  // operator still has room to raise it or fix the fallback before the team stops.
@@ -379,11 +430,27 @@ export function eventMessage(e) {
379
430
  });
380
431
  return `fallback: ${parts.join("; ")}`;
381
432
  }
433
+ case "model_fallback_started":
434
+ // Routine-with-explanation, like rate_limit_hold: the switch IS the harness handling the
435
+ // failure. Names the pair the role now runs on and why the primary was abandoned.
436
+ return `model fallback — ${providerModelPair(e, "pi's default", "pi's default")} engaged (primary failing: ${textOr(e.reason, "backend failure")})`;
437
+ case "model_fallback_ended":
438
+ // The return counterpart: names the pair runs resume on and how long the episode ran.
439
+ return `model fallback ended — back on ${providerModelPair(e, "pi's default", "pi's default")} after ${minutes(e.durationMs)}m`;
440
+ case "opencode_free_model_billed":
441
+ // The promotion ended (plans/opencode-backend.md §7, part 6c/7): a run billed what the
442
+ // catalog called free, so the team now prices that model as paid from here on. No warning
443
+ // prefix — the demotion is the harness handling the spend — but it names the model and the
444
+ // cost so the operator sees why a badge stopped saying "cost n/a".
445
+ return `opencode ${textOr(e.model, "model")} billed ${usd(finiteNumber(e.costUsd, 0))} — demoted from free to priced`;
446
+ default: return textOr(e.type);
447
+ }
448
+ }
449
+ function holdEventMessage(e) {
450
+ switch (e.type) {
382
451
  case "team_paused":
383
452
  // Routine state change, like counters_reset — no warning prefix.
384
453
  return `${teamPausePhrase()}${pauseSuffix(e.until, e.reason)} (director keeps running)`;
385
- case "state_migrated":
386
- return `state migrated — ${textOr(e.detail)}`;
387
454
  case "team_resumed":
388
455
  return teamResumePhrase() + (e.expired === true ? PAUSE_EXPIRED_SUFFIX : "");
389
456
  case "role_paused":
@@ -408,25 +475,6 @@ export function eventMessage(e) {
408
475
  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`;
409
476
  case "role_cap_resumed":
410
477
  return `role ${textOr(e.role)} resumed — it is under its daily cap again and runs again`;
411
- case "main_check": {
412
- const failing = Array.isArray(e.failingTests) ? e.failingTests.map(String) : [];
413
- const covered = Number(e.covered) > 1 ? ` (${textOr(e.covered)} commits)` : "";
414
- const verdict = e.status === "failed" ? "failed" : "passed";
415
- const names = failing.length > 0 ? `: ${joinWithMore(failing, 3, "; ")}` : "";
416
- return `main ${shortSha(String(e.sha ?? ""))}${covered} full check ${verdict}${names}`;
417
- }
418
- case "main_culprit": {
419
- const by = typeof e.runner === "string" && e.runner !== "" ? ` (merged by ${e.runner})` : "";
420
- return `main turned red at ${shortSha(String(e.commit ?? ""))}${by}`;
421
- }
422
- case "operator_revert": {
423
- const undid = typeof e.runner === "string" && e.runner !== "" ? `, undid ${e.runner}` : "";
424
- return `reverted ${shortSha(String(e.commit ?? ""))} (by operator) as ${shortSha(String(e.revert ?? ""))}${undid}`; // vocabulary-ok: the deliberate-revert writer is the operator role
425
- }
426
- case "main_red_hold":
427
- // The message already names the SHA, script, and consequence; the problem tone comes
428
- // from PROBLEM_EVENTS.
429
- return textOr(e.message);
430
478
  case "maintenance_quota_hold":
431
479
  // Routine state change, like role_cap_paused — no warning prefix: the hold IS the harness
432
480
  // keeping maintenance merges in proportion to work. Names the window's counts so the
@@ -438,19 +486,16 @@ export function eventMessage(e) {
438
486
  // Routine state change, like counters_reset — no warning prefix: the team just finished
439
487
  // its bootstrap and the maintenance roles are admitted.
440
488
  return `bootstrap complete — ${textOr(e.plansDone)}/${textOr(e.untilPlansDone)} plans done; the maintenance roles now run`;
489
+ default: return textOr(e.type);
490
+ }
491
+ }
492
+ function resourceEventMessage(e) {
493
+ switch (e.type) {
441
494
  case "disk_low":
442
495
  // Routine state change, like team_paused — no warning prefix: the hold IS the harness
443
496
  // handling the low disk, and naming the free space and the floor tells the operator
444
497
  // whether to clear space or lower diskHoldGB in tumwater.json.
445
498
  return `waiting: low disk — ${gigabytes(e.freeGB)} free on the workspaces volume (floor ${gigabytes(e.holdGB)}); new work waits until space recovers`;
446
- case "quiet_hours_started": {
447
- // Routine hold, like disk_low — no warning prefix: the window is configured, and the line
448
- // says what stops and what keeps running. pollQuietHoursGate may send no window.
449
- return `waiting: quiet hours${windowSuffix(e)} — roles start no new runs until the window ends; the director still runs`;
450
- }
451
- case "quiet_hours_ended": {
452
- return `quiet hours ended${windowSuffix(e)} — roles start runs again`;
453
- }
454
499
  case "disk_ok":
455
500
  return `disk recovered — ${gigabytes(e.freeGB)} free on the workspaces volume; new work starts again`;
456
501
  case "disk_reclaim": {
@@ -460,13 +505,21 @@ export function eventMessage(e) {
460
505
  const from = names.length > 0 ? names.join(", ") : "?";
461
506
  return `disk reclaim (${textOr(e.mode, "pressure")}) — freed ${gigabytes(e.freedGB)} from ${from}; ${gigabytes(e.freeGB)} free now`;
462
507
  }
508
+ case "quiet_hours_started": {
509
+ // Routine hold, like disk_low — no warning prefix: the window is configured, and the line
510
+ // says what stops and what keeps running. pollQuietHoursGate may send no window.
511
+ return `waiting: quiet hours${windowSuffix(e)} — roles start no new runs until the window ends; the director still runs`;
512
+ }
513
+ case "quiet_hours_ended": {
514
+ return `quiet hours ended${windowSuffix(e)} — roles start runs again`;
515
+ }
463
516
  case "rate_limit_hold": {
464
517
  // Routine state change, like team_paused — no warning prefix: the hold IS the harness
465
518
  // handling the storm. Names who saw the failure and when the team re-opens on its own.
466
519
  // The rate-limit kind keeps the wording every historical event has; a backend-failure
467
520
  // kind names itself instead, since "429" would be a lie about a connection error.
468
521
  const roles = rolesPhrase(e.roles, "several roles");
469
- if (e.kind && e.kind !== "rate-limit")
522
+ if (rendersAsBackendFailure(e.kind))
470
523
  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)`;
471
524
  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)`;
472
525
  }
@@ -474,7 +527,7 @@ export function eventMessage(e) {
474
527
  // The ended hold's kind rides the resumed event (pollTeamHold logs it), so the lift
475
528
  // names what actually ended — the same split as the hold line above: "429 hold lifted"
476
529
  // after a connection-error hold would be a lie about a connection error.
477
- if (e.kind && e.kind !== "rate-limit")
530
+ if (rendersAsBackendFailure(e.kind))
478
531
  return `backend recovered (${backendKindPhrase(e.kind)})${providerAt(e)} — roles on ${providerScope(e)} run again`;
479
532
  return `provider limit lifted${providerAt(e)} — roles on ${providerScope(e)} run again`;
480
533
  }
@@ -506,6 +559,11 @@ export function eventMessage(e) {
506
559
  return `waiting: ${backendLabel(e)} subscription window ${textOr(e.windowType)} rejected — ${backendLabel(e)} model jobs wait${windowResetPhrase(e)} (director keeps running)`;
507
560
  case "subscription_window_reset":
508
561
  return `${backendLabel(e)} subscription window reset — ${backendLabel(e)} model jobs run again`;
562
+ default: return textOr(e.type);
563
+ }
564
+ }
565
+ function configEventMessage(e) {
566
+ switch (e.type) {
509
567
  case "max_concurrent_changed": {
510
568
  // Routine state change, like counters_reset — no warning prefix.
511
569
  return `maxConcurrent changed: ${textOr(e.from)} → ${textOr(e.to)}`;
@@ -534,19 +592,17 @@ export function eventMessage(e) {
534
592
  return e.found === false
535
593
  ? `warning: newly routed ${textOr(e.agent)} backend — its binary was not found`
536
594
  : `newly routed ${textOr(e.agent)} backend checked — binary found`;
537
- case "model_fallback_started":
538
- // Routine-with-explanation, like rate_limit_hold: the switch IS the harness handling the
539
- // failure. Names the pair the role now runs on and why the primary was abandoned.
540
- return `model fallback — ${providerModelPair(e, "pi's default", "pi's default")} engaged (primary failing: ${textOr(e.reason, "backend failure")})`;
541
- case "model_fallback_ended":
542
- // The return counterpart: names the pair runs resume on and how long the episode ran.
543
- return `model fallback ended — back on ${providerModelPair(e, "pi's default", "pi's default")} after ${minutes(e.durationMs)}m`;
544
- case "opencode_free_model_billed":
545
- // The promotion ended (plans/opencode-backend.md §7, part 6c/7): a run billed what the
546
- // catalog called free, so the team now prices that model as paid from here on. No warning
547
- // prefix — the demotion is the harness handling the spend — but it names the model and the
548
- // cost so the operator sees why a badge stopped saying "cost n/a".
549
- return `opencode ${textOr(e.model, "model")} billed ${usd(finiteNumber(e.costUsd, 0))} — demoted from free to priced`;
595
+ case "state_migrated":
596
+ return `state migrated — ${textOr(e.detail)}`;
597
+ case "orchestrator_start":
598
+ return `tumwater started (pid ${textOr(e.pid)}${e.build ? `, version ${shortSha(e.build)}` : ""})`;
599
+ case "orchestrator_stop":
600
+ return orchestratorStopPhrase(e.cause);
601
+ default: return textOr(e.type);
602
+ }
603
+ }
604
+ function imageRestartEventMessage(e) {
605
+ switch (e.type) {
550
606
  case "image_blind": {
551
607
  // Routine-with-explanation like the model-fallback events — no warning prefix: the
552
608
  // run was stopped (or a gate run noted) because the model cannot see the image. Names
@@ -558,13 +614,6 @@ export function eventMessage(e) {
558
614
  : "";
559
615
  return `blind image read: ${textOr(e.path)} on ${textOr(e.model, "the model")}${action}`;
560
616
  }
561
- case "image_reroute":
562
- // The run's work moves to a model that can read the image (plans/image-input.md part
563
- // 4/5). Routine-with-explanation — no warning prefix: names the hop the way the
564
- // handoff reads, and whether the traits-of-attachment preflight avoided a failed run.
565
- return e.preflight === true
566
- ? `image attachment routed to ${textOr(e.to, "the target")} (${textOr(e.tier, "a peer tier")}) to read its image — no failed run`
567
- : `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")}`;
568
617
  case "build_stale":
569
618
  // Self-hosting teams only (src/redeploy/redeploy.ts): the code main describes is not the
570
619
  // code running. Not a warning prefix — a stale build is a state, and auto-restart resolves
@@ -574,7 +623,7 @@ export function eventMessage(e) {
574
623
  return `restart pending — main ${shortSha(e.head)} is green; finishing current runs (no new runs start)`;
575
624
  case "restart": {
576
625
  const aborted = finiteNumber(e.abortedRuns, 0);
577
- return `restarting onto update ${shortSha(e.to)} (finished current work in ${minutes(e.finishedMs)}m${aborted > 0 ? `, ${aborted} run(s) resume after the restart` : ""})`;
626
+ return `restarting onto update ${shortSha(e.to)} (finished current work in ${minutes(e.finishedMs)}m${aborted > 0 ? `, ${plural(aborted, "run")} resume after the restart` : ""})`;
578
627
  }
579
628
  case "restart_refused":
580
629
  // The restart is refused, not failed: the running build stays and the gate is re-asked
@@ -593,21 +642,24 @@ export function eventMessage(e) {
593
642
  const how = e.signal ? `was killed by ${textOr(e.signal)}` : `exited ${textOr(e.code)}`;
594
643
  return `tumwater is down — restart ${textOr(e.generation)} ${how}${e.reason ? `: ${textOr(e.reason)}` : ""}; start it again with \`tumwater start\``;
595
644
  }
596
- case "resume": {
597
- // Two causes share the resume machinery; the line names the real one so an operator
598
- // reading the feed can tell a restart from a role fighting the context ceiling, and a
599
- // cross-agent handoff from either — the handoff keeps the worktree but starts fresh. A
600
- // run that got a salvaged patch back (src/git/slot-salvage.ts) names the patch.
601
- const restored = e.restored === undefined ? "" : ` — uncommitted edits restored from ${textOr(e.restored)}`;
602
- return ((e.cause === "cut-off"
603
- ? "resuming the run cut off at the context ceiling (compacted pi session, same workspace)"
604
- : e.cause === "handoff"
605
- ? "handing the interrupted run off to a fresh session on another agent (same workspace, uncommitted edits kept)"
606
- : e.cause === "image-reroute"
607
- ? "handing the blind-read run off to a fresh session on an image-capable model (same workspace, uncommitted edits kept)"
608
- : e.cause === "salvaged"
609
- ? "handing the work an earlier run left unsaved to a fresh session"
610
- : "resuming the run a shutdown interrupted (same agent session and workspace)") + restored);
645
+ default: return textOr(e.type);
646
+ }
647
+ }
648
+ function checkWaitEventMessage(e) {
649
+ switch (e.type) {
650
+ case "build_check":
651
+ // The deterministic check's cost, per run: scope names which gate paid (the pre-merge review
652
+ // gate, the red-main baseline check of main itself, or the merge lock's post-rebase re-check
653
+ // of the tree about to merge). script carries the npm script name for an npm check but the
654
+ // FULL configured command otherwise (checkScriptName in build/build-check.ts), so no "npm"
655
+ // prefix is asserted here — the bare name reads correctly for both kinds ("npm test" would
656
+ // misrender a cargo or make-based project's check).
657
+ return `check (${textOr(e.scope)}): ${textOr(e.script)} ${eventTypeLabel(outcomeText(e))}${elapsed(e.durationMs)}`;
658
+ case "dep_install": {
659
+ // The root checkout's install catching up with a merged lockfile change — routine on
660
+ // success; a failure also rides its own warning, which says what happens meanwhile.
661
+ const pkgs = stringList(e.packages).join(", ");
662
+ return `root install (${pkgs}) ${eventTypeLabel(outcomeText(e))}${e.error ? ` — ${textOr(e.error)}` : ""}${elapsed(e.durationMs)}`;
611
663
  }
612
664
  case "slot_wait": {
613
665
  // Routine-with-explanation, like rate_limit_hold — no warning prefix: waiting is the pool
@@ -644,19 +696,112 @@ export function eventMessage(e) {
644
696
  const behind = heldBy.length > 0 ? `; held by ${heldBy.join(", ")}${limit === null ? "" : ` of maxConcurrentChecks ${limit}`}` : "";
645
697
  return `waited ${shortSpanPhrase(finiteNumber(e.waitedMs, 0))} for a check slot (${textOr(e.role)} ${textOr(e.scope)} check${behind})`;
646
698
  }
647
- case "claim":
648
- // Parallel work instances (plans/parallel-work-instances.md "Observability", part 6/7):
649
- // the assignment/release is the harness working as designed, so no warning prefix.
650
- return e.action === "released"
651
- ? `released "${textOr(e.title)}" (${textOr(e.reason, "?")})`
652
- : `assigned "${textOr(e.title)}"`;
653
- case "permit_reserve":
654
- // Priority headroom (part 7/7): a reserve appearing or vanishing is the scheduler working
655
- // as designed; it names the permits withheld for the work tier so an operator can see why
656
- // maintenance concurrency dipped.
657
- return `${finiteNumber(e.reserve, 0)} permit(s) reserved for work roles that have work to take`;
699
+ default: return textOr(e.type);
700
+ }
701
+ }
702
+ function warningEventMessage(e) {
703
+ switch (e.type) {
658
704
  case "warning":
659
705
  return `warning: ${textOr(e.message)}`;
706
+ default: return textOr(e.type);
707
+ }
708
+ }
709
+ /** An event's message alone — formatEvent's line without the time and role columns. The GUI's
710
+ * activity feed renders the time and role itself (relative ages, role chips), so the payload
711
+ * ships this beside the event's type and role instead of making the page re-parse a line.
712
+ * Dispatch only: each event family's line is built by a module-private <family>EventMessage
713
+ * switch (runEventMessage, mergeEventMessage, reviewEventMessage, inboxEventMessage,
714
+ * budgetEventMessage, holdEventMessage, resourceEventMessage, configEventMessage,
715
+ * imageRestartEventMessage, checkWaitEventMessage, warningEventMessage). */
716
+ export function eventMessage(e) {
717
+ switch (e.type) {
718
+ case "run_start":
719
+ case "run_end":
720
+ case "run_deferred":
721
+ case "run_aborted":
722
+ case "resume":
723
+ case "wake":
724
+ case "counters_reset":
725
+ case "claim":
726
+ case "permit_reserve": return runEventMessage(e);
727
+ case "merged":
728
+ case "merge_queued":
729
+ case "merge_cancelled":
730
+ case "merge_topped":
731
+ case "merge_done":
732
+ case "merge_failed":
733
+ case "conflict_handback":
734
+ case "operator_revert":
735
+ case "main_check":
736
+ case "main_culprit":
737
+ case "main_red_hold": return mergeEventMessage(e);
738
+ case "review_start":
739
+ case "review_verdict":
740
+ case "review_rejected":
741
+ case "review_failed":
742
+ case "revision": return reviewEventMessage(e);
743
+ case "prompt_enqueued":
744
+ case "prompt_cancelled":
745
+ case "prompt_edited":
746
+ case "prompt_topped":
747
+ case "prompt_taken":
748
+ case "question_posted":
749
+ case "entry_refused":
750
+ case "issue_filed": return inboxEventMessage(e);
751
+ case "budget_warning":
752
+ case "budget_paused":
753
+ case "budget_fallback":
754
+ case "budget_resumed":
755
+ case "budget_handback":
756
+ case "fallback_switched":
757
+ case "model_fallback_started":
758
+ case "model_fallback_ended":
759
+ case "opencode_free_model_billed": return budgetEventMessage(e);
760
+ case "team_paused":
761
+ case "team_resumed":
762
+ case "role_paused":
763
+ case "role_resumed":
764
+ case "role_streak_paused":
765
+ case "role_cap_warning":
766
+ case "role_cap_paused":
767
+ case "role_cap_resumed":
768
+ case "maintenance_quota_hold":
769
+ case "maintenance_quota_resumed":
770
+ case "bootstrap_complete": return holdEventMessage(e);
771
+ case "disk_low":
772
+ case "disk_ok":
773
+ case "disk_reclaim":
774
+ case "quiet_hours_started":
775
+ case "quiet_hours_ended":
776
+ case "rate_limit_hold":
777
+ case "rate_limit_resumed":
778
+ case "subscription_window_warning":
779
+ case "subscription_window_reserved":
780
+ case "subscription_window_limited":
781
+ case "subscription_window_reset": return resourceEventMessage(e);
782
+ case "max_concurrent_changed":
783
+ case "provider_concurrency_changed":
784
+ case "retention_changed":
785
+ case "config_changed":
786
+ case "model_changed":
787
+ case "backend_binary":
788
+ case "state_migrated":
789
+ case "orchestrator_start":
790
+ case "orchestrator_stop": return configEventMessage(e);
791
+ case "image_blind":
792
+ case "build_stale":
793
+ case "restart_pending":
794
+ case "restart":
795
+ case "restart_refused":
796
+ case "restart_blocked":
797
+ case "restart_forced":
798
+ case "supervisor_exit": return imageRestartEventMessage(e);
799
+ case "build_check":
800
+ case "dep_install":
801
+ case "slot_wait":
802
+ case "provider_wait":
803
+ case "check_wait": return checkWaitEventMessage(e);
804
+ case "warning": return warningEventMessage(e);
660
805
  default:
661
806
  // An unrecognized type is normally a newer build's string and renders as-is; a torn or
662
807
  // hand-edited event object with no type at all must read the "?" stand-in, never