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
@@ -7,16 +7,24 @@
7
7
  * and a roleCell-sliced role, so no unbounded field reaches the page. */
8
8
  import { truncateExample } from "./failure-cluster.js";
9
9
  import { cutSplitsSurrogatePair, isNonBlankString } from "../text/text.js";
10
- import { finiteNumber, stringList } from "../files/json-object.js";
11
- import { orchestratorStopPhrase, backendKindPhrase, budgetPhrase, teamPausePhrase, teamResumePhrase, holdPhrase, plural, rolePausePhrase, roleResumePhrase, rolesPhrase, shortSpanPhrase, PAUSE_EXPIRED_SUFFIX } from "../text/phrases.js";
10
+ import { finiteNumber, isJsonObject, stringList } from "../files/json-object.js";
11
+ import { orchestratorStopPhrase, backendKindPhrase, budgetPhrase, teamPausePhrase, teamResumePhrase, holdPhrase, plural, rolePausePhrase, roleResumePhrase, rolesPhrase, shortSpanPhrase, rendersAsBackendFailure, PAUSE_EXPIRED_SUFFIX } from "../text/phrases.js";
12
12
  import { shortSha } from "../text/format.js";
13
- import { deferredReasonText, untilClockSuffix } from "../events/event-format.js";
13
+ import { deferredReasonText, untilClockSuffix, gigabytes, windowSuffix, windowResetPhrase, backendLabel } from "../events/event-format.js";
14
+ import { claudeUtilizationPct } from "../agent/claude/claude-account.js";
14
15
  /** The transition events the digest replays: the decisions the harness made about itself (the
15
16
  * cap/team gates and the team hold, the error-streak breaker's pause, the role's own daily
16
17
  * cap, model fallbacks, live-config edits, self-hosted redeploys, need-based deferrals,
17
- * orchestrator lifecycle). They are the evidence the selfcheck role's load-bearing
18
- * rule asks it to judge — whether the harness's RESPONSE to a failure was wrong
19
- * (plans/telemetry-role.md) — so they sit beside the outcomes rather than being dropped. */
18
+ * orchestrator lifecycle, and the gate holds/lifts that stop and restart work — low disk,
19
+ * quiet hours, the maintenance allowance and the subscription-window reserve). Everything the
20
+ * set omits is deliberately excluded and named in STATE_CHANGE_EXCLUDED_TYPES; the two sets
21
+ * partition EVENT_TYPE_LABELS' keys, so a new event type must land in exactly one of them and
22
+ * cannot ship unclassified. The wiring twins `fallback_switched` and `budget_handback` are the
23
+ * run-side consequence of `budget_resumed` — the budget/window reopening that aborts in-flight
24
+ * fallback runs so their next run continues on the primary. The rest are the evidence the
25
+ * selfcheck role's load-bearing rule asks it to
26
+ * judge — whether the harness's RESPONSE to a failure was wrong (plans/telemetry-role.md) —
27
+ * so they sit beside the outcomes rather than being dropped. */
20
28
  export const STATE_CHANGE_TYPES = new Set([
21
29
  "budget_paused",
22
30
  "budget_fallback",
@@ -44,6 +52,81 @@ export const STATE_CHANGE_TYPES = new Set([
44
52
  "orchestrator_start",
45
53
  "orchestrator_stop",
46
54
  "supervisor_exit",
55
+ "disk_low",
56
+ "disk_ok",
57
+ "quiet_hours_started",
58
+ "quiet_hours_ended",
59
+ "maintenance_quota_hold",
60
+ "maintenance_quota_resumed",
61
+ "subscription_window_reserved",
62
+ "subscription_window_limited",
63
+ "subscription_window_reset",
64
+ "provider_concurrency_changed",
65
+ "model_changed",
66
+ "bootstrap_complete",
67
+ "fallback_switched",
68
+ "budget_handback",
69
+ ]);
70
+ /** The event types the digest deliberately does not replay, one per EVENT_TYPE_LABELS key that
71
+ * is not a STATE_CHANGE_TYPES member. Together with STATE_CHANGE_TYPES it partitions
72
+ * EVENT_TYPE_LABELS' keys, so a new event type must be classified in one set or the other — the
73
+ * disjoint-and-covering commitment the digest's Team state changes section rests on. Grouped by
74
+ * reason, one line per group (the run-outcome half, the per-run advisories and the observations
75
+ * were named in the older comment; the state-adjacent group is newly listed). */
76
+ export const STATE_CHANGE_EXCLUDED_TYPES = new Set([
77
+ // Run, merge and main-watch outcomes and per-run bookkeeping.
78
+ "run_start",
79
+ "run_end",
80
+ "merge_queued",
81
+ "merge_cancelled",
82
+ "merge_topped",
83
+ "merge_done",
84
+ "merge_failed",
85
+ "merged",
86
+ "counters_reset",
87
+ "run_aborted",
88
+ "resume",
89
+ "wake",
90
+ "operator_revert",
91
+ "build_check",
92
+ "dep_install",
93
+ "budget_warning",
94
+ "role_cap_warning",
95
+ "main_check",
96
+ "main_culprit",
97
+ "main_red_hold",
98
+ "warning",
99
+ // Reviews and revisions.
100
+ "review_start",
101
+ "review_verdict",
102
+ "review_rejected",
103
+ "review_failed",
104
+ "revision",
105
+ "conflict_handback",
106
+ // Inbox and backlog.
107
+ "question_posted",
108
+ "issue_filed",
109
+ "entry_refused",
110
+ "prompt_enqueued",
111
+ "prompt_cancelled",
112
+ "prompt_edited",
113
+ "prompt_topped",
114
+ "prompt_taken",
115
+ // Per-run advisories and assignments.
116
+ "claim",
117
+ "permit_reserve",
118
+ "slot_wait",
119
+ "provider_wait",
120
+ "check_wait",
121
+ // Observations and actions.
122
+ "disk_reclaim",
123
+ "subscription_window_warning",
124
+ "image_blind",
125
+ // State-adjacent but not self-decisions.
126
+ "state_migrated",
127
+ "restart_forced",
128
+ "backend_binary",
129
+ "opencode_free_model_billed",
47
130
  ]);
48
131
  /** The Team state changes section's caps: newest N transitions, each line's payload capped at
49
132
  * STATE_CHANGE_MAX (a marked word-boundary cut per truncateExample), each free field within it
@@ -53,12 +136,33 @@ export const STATE_CHANGE_TYPES = new Set([
53
136
  export const STATE_CHANGE_TOP = 6;
54
137
  const STATE_CHANGE_MAX = 72;
55
138
  const STATE_CHANGE_FIELD_MAX = 24;
139
+ /** Join a list of `items` into one comma/`;`-separated line that fits STATE_CHANGE_MAX: pop
140
+ * tail items until `prefix + items.join(separator)` — with a `… +N more <noun>` marker naming
141
+ * the drop, or a bare `bare` fallback once none remain — stays under the cap, so the cut never
142
+ * lands mid-item and the marker survives the final pass. Shared by the fallback_switched and
143
+ * config_changed lines, which both bounded-drop a hand-edited/list payload that can overshoot
144
+ * the cap (BUGS.md 2026-10-01), so the loop shape and marker wording cannot drift between them. */
145
+ function joinToCap(bare, prefix, separator, noun, items) {
146
+ let text = items.length > 0 ? prefix + items.join(separator) : bare;
147
+ let hidden = 0;
148
+ while (text.length > STATE_CHANGE_MAX) {
149
+ items.pop();
150
+ hidden += 1;
151
+ text =
152
+ items.length > 0
153
+ ? `${prefix}${items.join(separator)} … +${hidden} more ${noun}`
154
+ : `${bare} … +${hidden} more ${noun}`;
155
+ }
156
+ return text;
157
+ }
56
158
  /** A free event field, sliced so one hand-edited or future payload cannot blow the section's
57
159
  * byte budget. Takes unknown because HarnessEvent carries its fields loosely typed. A cut that
58
160
  * would land between an astral character's two UTF-16 units backs off one unit — dropping the
59
161
  * whole character rather than leaving a lone high surrogate the terminal renders as a
60
162
  * replacement box (text.ts's truncate rule, shared rather than re-derived). */
61
163
  function field(v) {
164
+ if (v === undefined || v === null)
165
+ return ""; // A torn event must never render "undefined".
62
166
  const s = String(v);
63
167
  let cut = STATE_CHANGE_FIELD_MAX;
64
168
  if (cutSplitsSurrogatePair(s, cut))
@@ -71,13 +175,18 @@ function field(v) {
71
175
  function providerModel(ev) {
72
176
  return `${field(ev.provider ?? "pi default")}/${field(ev.model ?? "pi default")}`;
73
177
  }
74
- /** A compact, bounded one-liner for one harness decision event, for the Team state changes
75
- * section. Every free string is sliced (field) and the whole line is capped again at
76
- * STATE_CHANGE_MAX so the digest's byte bound holds for any event shape — the cap cut falls on
77
- * a word boundary and carries truncateExample's `… (+N chars)` marker, so a cut line can never
78
- * read as complete or stand mid-word where a key or value should be; the render adds the
79
- * timestamp and a roleCell-sliced role, so no unbounded field reaches the page. */
80
- export function describeStateChange(ev) {
178
+ /** A utilization fraction as its whole-number percent, read through finiteNumber so a torn or
179
+ * hand-edited subscription event renders 0% rather than NaN — the same corrupt-field rule the
180
+ * feed's own subscription lines apply. */
181
+ function pct(v) {
182
+ return claudeUtilizationPct(finiteNumber(v, 0));
183
+ }
184
+ /** A per-family builder of one state-change line, one per event family, so the budget, pause,
185
+ * model-wiring, provider-hold, live-config, update, wait and lifecycle wordings are each found
186
+ * by name. Each is a total function over any HarnessEvent — its `default` returns the bare type
187
+ * name, unreachable through the dispatcher below — and each returns the untruncated line; the
188
+ * caller truncates exactly once, so the byte bound and its marker apply as before. */
189
+ function budgetStateChange(ev) {
81
190
  let text;
82
191
  switch (ev.type) {
83
192
  case "budget_paused": {
@@ -99,6 +208,14 @@ export function describeStateChange(ev) {
99
208
  case "budget_resumed":
100
209
  text = `budget resumed (${budgetPhrase(ev.spentUsd, ev.capUsd)} today)`;
101
210
  break;
211
+ default:
212
+ return ev.type;
213
+ }
214
+ return text;
215
+ }
216
+ function pauseStateChange(ev) {
217
+ let text;
218
+ switch (ev.type) {
102
219
  case "team_paused":
103
220
  text = teamPausePhrase() + untilClockSuffix(ev.until);
104
221
  if (isNonBlankString(ev.reason))
@@ -125,6 +242,14 @@ export function describeStateChange(ev) {
125
242
  case "role_cap_resumed":
126
243
  text = `role ${field(ev.role ?? "?")} resumed — under its daily cap again`;
127
244
  break;
245
+ default:
246
+ return ev.type;
247
+ }
248
+ return text;
249
+ }
250
+ function modelStateChange(ev) {
251
+ let text;
252
+ switch (ev.type) {
128
253
  case "model_fallback_started":
129
254
  // The role tripped off its primary onto its fallback after provider-class failures.
130
255
  text = `model fallback — ${providerModel(ev)} engaged (${field(ev.reason ?? "backend failure")})`;
@@ -134,13 +259,54 @@ export function describeStateChange(ev) {
134
259
  // off-model episode ran.
135
260
  text = `model fallback ended — back on ${providerModel(ev)} after ${shortSpanPhrase(finiteNumber(ev.durationMs, 0))}`;
136
261
  break;
262
+ case "provider_concurrency_changed":
263
+ text = `${field(ev.provider)} concurrency ${field(ev.from)} → ${field(ev.to)}`;
264
+ break;
265
+ case "model_changed":
266
+ // A model_cleared (or a roles.<id>.model-only) live edit arrives with `to: null`;
267
+ // name pi's default the way the feed does so the digest never renders a bare
268
+ // `model changed — now `.
269
+ text = `model changed — now ${field(ev.to ?? "pi's default")}`;
270
+ break;
271
+ case "fallback_switched": {
272
+ // A tier's fallback pair or step changed (or returned to the primary) this poll. Null
273
+ // `to`/`from` read `primary`, matching the feed's fallback_switched line. Whole parts
274
+ // only, dropped from the tail with a marker when the tier list overshoots the cap — the
275
+ // same bounded-drop rule config_changed uses, because a hand-edited `tiers` object can
276
+ // carry more entries than MODEL_TIERS; an empty or torn object reads `fallback switched`.
277
+ const tiers = isJsonObject(ev.tiers) ? ev.tiers : {};
278
+ const parts = Object.entries(tiers).map(([tier, v]) => {
279
+ const t = isJsonObject(v) ? v : {};
280
+ return `${field(tier)}: ${field(t.from ?? "primary")} → ${field(t.to ?? "primary")} (${field(t.reason ?? "available")})`;
281
+ });
282
+ text = joinToCap("fallback switched", "fallback switched — ", "; ", "tiers", parts);
283
+ break;
284
+ }
285
+ case "budget_handback":
286
+ // The budget/window reopened and aborted in-flight fallback runs; their next run
287
+ // continues on the primary. Mirrors the feed's budget_handback wording in the digest's
288
+ // compact form; the window's backend is not named since subscription_window_reset
289
+ // already carries it.
290
+ text =
291
+ ev.reason === "window"
292
+ ? `window reset — handed ${rolesPhrase(ev.roles, "?", 4, field)} back to the primary`
293
+ : `budget reopened — handed ${rolesPhrase(ev.roles, "?", 4, field)} back to the primary`;
294
+ break;
295
+ default:
296
+ return ev.type;
297
+ }
298
+ return text;
299
+ }
300
+ function providerHoldStateChange(ev) {
301
+ let text;
302
+ switch (ev.type) {
137
303
  case "rate_limit_hold": {
138
304
  // Same split as the event feed's rendering (event-format.ts): a rate-limit hold keeps
139
305
  // the 429 wording; a backend-failure kind names itself. The role list caps at four and
140
306
  // each role is byte-sliced through field() so the section's bound holds.
141
307
  const roles = rolesPhrase(ev.roles, "?", 4, field);
142
308
  text =
143
- ev.kind && ev.kind !== "rate-limit"
309
+ rendersAsBackendFailure(ev.kind)
144
310
  ? `backend hold (${backendKindPhrase(ev.kind)}) ${holdPhrase(ev.holdMs, ev.escalation)} — ${roles}`
145
311
  : `429 hold ${holdPhrase(ev.holdMs, ev.escalation)} — ${roles}`;
146
312
  break;
@@ -149,10 +315,18 @@ export function describeStateChange(ev) {
149
315
  // Same split as the hold line above (and the event feed's rendering): the resumed event
150
316
  // carries the ended hold's kind, so a backend hold's lift names itself.
151
317
  text =
152
- ev.kind && ev.kind !== "rate-limit"
318
+ rendersAsBackendFailure(ev.kind)
153
319
  ? `backend hold lifted (${backendKindPhrase(ev.kind)}) — roles run again`
154
320
  : "429 hold lifted — roles run again";
155
321
  break;
322
+ default:
323
+ return ev.type;
324
+ }
325
+ return text;
326
+ }
327
+ function liveConfigStateChange(ev) {
328
+ let text;
329
+ switch (ev.type) {
156
330
  case "max_concurrent_changed":
157
331
  text = `maxConcurrent ${field(ev.from)} → ${field(ev.to)}`;
158
332
  break;
@@ -166,18 +340,17 @@ export function describeStateChange(ev) {
166
340
  // marker naming the drop — fits the section cap, so the final STATE_CHANGE_MAX pass
167
341
  // below can never cut the marker or a key mid-word.
168
342
  const keys = stringList(ev.keys).map(field);
169
- text = keys.length > 0 ? `config changed: ${keys.join(", ")}` : "config changed";
170
- let hidden = 0;
171
- while (text.length > STATE_CHANGE_MAX) {
172
- keys.pop();
173
- hidden += 1;
174
- text =
175
- keys.length > 0
176
- ? `config changed: ${keys.join(", ")} … +${hidden} more keys`
177
- : `config changed … +${hidden} more keys`;
178
- }
343
+ text = joinToCap("config changed", "config changed: ", ", ", "keys", keys);
179
344
  break;
180
345
  }
346
+ default:
347
+ return ev.type;
348
+ }
349
+ return text;
350
+ }
351
+ function updateStateChange(ev) {
352
+ let text;
353
+ switch (ev.type) {
181
354
  case "build_stale":
182
355
  // The digest's 72-char line cap (§ STATE_CHANGE_MAX) cannot hold the full alert phrase
183
356
  // "restarts after cooldown; work continues" beside the ahead-count prefix, so it says
@@ -197,9 +370,54 @@ export function describeStateChange(ev) {
197
370
  case "restart_blocked":
198
371
  text = `restart blocked — ${field(ev.reason)}; staying on build ${shortSha(ev.from)}`;
199
372
  break;
373
+ default:
374
+ return ev.type;
375
+ }
376
+ return text;
377
+ }
378
+ function waitStateChange(ev) {
379
+ let text;
380
+ switch (ev.type) {
200
381
  case "run_deferred":
201
382
  text = `deferred — ${deferredReasonText(ev)}`;
202
383
  break;
384
+ case "disk_low":
385
+ // The disk floor stopped the team from starting runs; the free space and floor read
386
+ // through the feed's own gigabytes so the digest and the event feed cannot drift.
387
+ text = `waiting: low disk — ${gigabytes(ev.freeGB)} free (floor ${gigabytes(ev.holdGB)})`;
388
+ break;
389
+ case "disk_ok":
390
+ text = `disk recovered — ${gigabytes(ev.freeGB)} free`;
391
+ break;
392
+ case "quiet_hours_started":
393
+ text = `waiting: quiet hours${windowSuffix(ev)}`;
394
+ break;
395
+ case "quiet_hours_ended":
396
+ text = `quiet hours ended${windowSuffix(ev)}`;
397
+ break;
398
+ case "maintenance_quota_hold":
399
+ text = `waiting: maintenance allowance used — ${field(ev.maint)}/${field(ev.allowance)} in 24h (${field(ev.work)} work)`;
400
+ break;
401
+ case "maintenance_quota_resumed":
402
+ text = `maintenance allowance reopened — ${field(ev.maint)}/${field(ev.allowance)} in 24h`;
403
+ break;
404
+ case "subscription_window_reserved":
405
+ text = `waiting: ${backendLabel(ev)} window ${field(ev.windowType)} at ${pct(ev.utilization)}% past its ${pct(ev.reserve)}% reserve`;
406
+ break;
407
+ case "subscription_window_limited":
408
+ text = `waiting: ${backendLabel(ev)} window ${field(ev.windowType)} rejected${windowResetPhrase(ev)}`;
409
+ break;
410
+ case "subscription_window_reset":
411
+ text = `${backendLabel(ev)} window reset — model jobs run again`;
412
+ break;
413
+ default:
414
+ return ev.type;
415
+ }
416
+ return text;
417
+ }
418
+ function lifecycleStateChange(ev) {
419
+ let text;
420
+ switch (ev.type) {
203
421
  case "orchestrator_start":
204
422
  text = `tumwater started (pid ${field(ev.pid)}${ev.build ? `, build ${shortSha(ev.build)}` : ""})`;
205
423
  break;
@@ -209,8 +427,71 @@ export function describeStateChange(ev) {
209
427
  case "supervisor_exit":
210
428
  text = `tumwater down — restart ${field(ev.generation)} ${ev.signal ? `killed by ${field(ev.signal)}` : `exited ${field(ev.code)}`}${ev.reason ? `: ${field(ev.reason)}` : ""}`;
211
429
  break;
430
+ case "bootstrap_complete":
431
+ text = `bootstrap complete — ${field(ev.plansDone)}/${field(ev.untilPlansDone)} plans done`;
432
+ break;
433
+ default:
434
+ return ev.type;
435
+ }
436
+ return text;
437
+ }
438
+ /** A compact, bounded one-liner for one harness decision event, for the Team state changes
439
+ * section. Every free string is sliced (field) and the whole line is capped again at
440
+ * STATE_CHANGE_MAX so the digest's byte bound holds for any event shape — the cap cut falls on
441
+ * a word boundary and carries truncateExample's `… (+N chars)` marker, so a cut line can never
442
+ * read as complete or stand mid-word where a key or value should be; the render adds the
443
+ * timestamp and a roleCell-sliced role, so no unbounded field reaches the page. */
444
+ export function describeStateChange(ev) {
445
+ switch (ev.type) {
446
+ case "budget_paused":
447
+ case "budget_fallback":
448
+ case "budget_resumed":
449
+ return truncateExample(budgetStateChange(ev), STATE_CHANGE_MAX);
450
+ case "team_paused":
451
+ case "team_resumed":
452
+ case "role_paused":
453
+ case "role_resumed":
454
+ case "role_streak_paused":
455
+ case "role_cap_paused":
456
+ case "role_cap_resumed":
457
+ return truncateExample(pauseStateChange(ev), STATE_CHANGE_MAX);
458
+ case "model_fallback_started":
459
+ case "model_fallback_ended":
460
+ case "provider_concurrency_changed":
461
+ case "model_changed":
462
+ case "fallback_switched":
463
+ case "budget_handback":
464
+ return truncateExample(modelStateChange(ev), STATE_CHANGE_MAX);
465
+ case "rate_limit_hold":
466
+ case "rate_limit_resumed":
467
+ return truncateExample(providerHoldStateChange(ev), STATE_CHANGE_MAX);
468
+ case "max_concurrent_changed":
469
+ case "retention_changed":
470
+ case "config_changed":
471
+ return truncateExample(liveConfigStateChange(ev), STATE_CHANGE_MAX);
472
+ case "build_stale":
473
+ case "restart_pending":
474
+ case "restart":
475
+ case "restart_refused":
476
+ case "restart_blocked":
477
+ return truncateExample(updateStateChange(ev), STATE_CHANGE_MAX);
478
+ case "run_deferred":
479
+ case "disk_low":
480
+ case "disk_ok":
481
+ case "quiet_hours_started":
482
+ case "quiet_hours_ended":
483
+ case "maintenance_quota_hold":
484
+ case "maintenance_quota_resumed":
485
+ case "subscription_window_reserved":
486
+ case "subscription_window_limited":
487
+ case "subscription_window_reset":
488
+ return truncateExample(waitStateChange(ev), STATE_CHANGE_MAX);
489
+ case "orchestrator_start":
490
+ case "orchestrator_stop":
491
+ case "supervisor_exit":
492
+ case "bootstrap_complete":
493
+ return truncateExample(lifecycleStateChange(ev), STATE_CHANGE_MAX);
212
494
  default:
213
- text = ev.type;
495
+ return truncateExample(ev.type, STATE_CHANGE_MAX);
214
496
  }
215
- return truncateExample(text, STATE_CHANGE_MAX);
216
497
  }