tumwater 0.4.2 → 0.4.4

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 (354) hide show
  1. package/README.md +6 -6
  2. package/dist/build-info.json +2 -2
  3. package/dist/src/agent/agent-backend.js +6 -6
  4. package/dist/src/agent/{run-agent.js → agent-run.js} +27 -13
  5. package/dist/src/agent/{run-settle.js → agent-settle.js} +48 -24
  6. package/dist/src/agent/{run-spawn.js → agent-spawn.js} +21 -12
  7. package/dist/src/agent/{run-stream.js → agent-stream.js} +4 -4
  8. package/dist/src/agent/backend-traits.js +2 -2
  9. package/dist/src/agent/claude/claude-account.js +6 -27
  10. package/dist/src/agent/claude/claude-backend.js +3 -3
  11. package/dist/src/agent/claude/claude-env.js +27 -31
  12. package/dist/src/agent/claude/claude-stream.js +111 -54
  13. package/dist/src/agent/codex/codex-account.js +8 -0
  14. package/dist/src/agent/codex/codex-args.js +5 -5
  15. package/dist/src/agent/codex/codex-backend.js +2 -2
  16. package/dist/src/agent/codex/codex-env.js +28 -15
  17. package/dist/src/agent/codex/codex-hook-trust.js +39 -20
  18. package/dist/src/agent/codex/codex-rollout.js +93 -56
  19. package/dist/src/agent/codex/codex-stream.js +1 -1
  20. package/dist/src/agent/env-scrub.js +18 -0
  21. package/dist/src/agent/image-input.js +68 -11
  22. package/dist/src/agent/notes-shim.js +2 -2
  23. package/dist/src/agent/opencode/opencode-backend.js +26 -24
  24. package/dist/src/agent/opencode/opencode-catalog.js +85 -36
  25. package/dist/src/agent/opencode/opencode-config.js +32 -14
  26. package/dist/src/agent/opencode/opencode-env.js +12 -13
  27. package/dist/src/agent/opencode/opencode-stream.js +47 -25
  28. package/dist/src/agent/session-ledger.js +23 -27
  29. package/dist/src/agent/subscription-account.js +2 -83
  30. package/dist/src/backlog/backlog-archive.js +3 -4
  31. package/dist/src/backlog/backlog-cli-view.js +81 -41
  32. package/dist/src/backlog/backlog-cli.js +57 -26
  33. package/dist/src/backlog/backlog-eligibility.js +56 -122
  34. package/dist/src/backlog/backlog-md.js +5 -5
  35. package/dist/src/backlog/backlog-parts.js +65 -27
  36. package/dist/src/backlog/{question-answer.js → backlog-question-answer.js} +1 -1
  37. package/dist/src/backlog/backlog-release.js +12 -12
  38. package/dist/src/backlog/backlog-structure.js +114 -61
  39. package/dist/src/backlog/backlog.js +3 -2
  40. package/dist/src/baseline/main-baseline.js +148 -84
  41. package/dist/src/baseline/main-guardrails.js +2 -2
  42. package/dist/src/baseline/main-watch.js +84 -50
  43. package/dist/src/brief/brief.js +26 -13
  44. package/dist/src/budget/budget-fallback-view.js +100 -243
  45. package/dist/src/budget/budget-wiring.js +29 -65
  46. package/dist/src/budget/budget.js +4 -5
  47. package/dist/src/budget/fallback-breaker.js +2 -2
  48. package/dist/src/build/build-check-detect.js +43 -14
  49. package/dist/src/build/build-check-events.js +2 -2
  50. package/dist/src/build/build-check-report.js +10 -0
  51. package/dist/src/build/build-check-scoped.js +40 -18
  52. package/dist/src/build/build-check.js +13 -5
  53. package/dist/src/build/build-stage.js +6 -4
  54. package/dist/src/change/change-data.js +1 -4
  55. package/dist/src/cli/cli-command-args.js +89 -52
  56. package/dist/src/cli/cli-completion-command.js +75 -0
  57. package/dist/src/cli/{config-commands.js → cli-config-commands.js} +82 -71
  58. package/dist/src/cli/cli-doctor-command.js +103 -0
  59. package/dist/src/cli/cli-flag-specs.js +42 -22
  60. package/dist/src/cli/cli-init.js +36 -14
  61. package/dist/src/cli/{log-commands.js → cli-log-commands.js} +43 -23
  62. package/dist/src/cli/{notify-command.js → cli-notify-command.js} +2 -2
  63. package/dist/src/cli/cli-query-commands.js +2 -2
  64. package/dist/src/cli/{question-commands.js → cli-question-commands.js} +86 -60
  65. package/dist/src/cli/cli-repo-gate.js +2 -2
  66. package/dist/src/cli/cli-role-command.js +2 -41
  67. package/dist/src/cli/cli-start-summary.js +4 -3
  68. package/dist/src/cli/cli-start-supervisor.js +190 -0
  69. package/dist/src/cli/cli-start.js +94 -244
  70. package/dist/src/cli/completion.js +85 -49
  71. package/dist/src/cli/follow-view.js +20 -2
  72. package/dist/src/cli/help.js +6 -13
  73. package/dist/src/cli.js +70 -176
  74. package/dist/src/collections.js +39 -27
  75. package/dist/src/concurrency/check-slots.js +70 -0
  76. package/dist/src/concurrency/lock.js +23 -1
  77. package/dist/src/concurrency/run-permit.js +49 -0
  78. package/dist/src/concurrency/semaphore.js +46 -250
  79. package/dist/src/config/config-editable-keys.js +4 -7
  80. package/dist/src/config/config-fallbacks.js +81 -101
  81. package/dist/src/config/config-field-checks.js +66 -37
  82. package/dist/src/config/config-keys.js +66 -40
  83. package/dist/src/config/config-live.js +4 -28
  84. package/dist/src/config/{model-selector.js → config-model-selector.js} +4 -4
  85. package/dist/src/config/config-request.js +163 -102
  86. package/dist/src/config/config-schema.js +10 -12
  87. package/dist/src/config/config-selector-checks.js +1 -1
  88. package/dist/src/config/config-validation-agents.js +2 -2
  89. package/dist/src/config/config-validation-roles.js +8 -72
  90. package/dist/src/config/config-validation.js +4 -39
  91. package/dist/src/config/config-views.js +26 -50
  92. package/dist/src/config/config-write.js +128 -102
  93. package/dist/src/config/config.js +5 -14
  94. package/dist/src/doctor/doctor-backlog-bugs.js +7 -6
  95. package/dist/src/doctor/doctor-backlog-plans.js +53 -120
  96. package/dist/src/doctor/doctor-backlog.js +16 -14
  97. package/dist/src/doctor/doctor-check-names.js +3 -6
  98. package/dist/src/doctor/doctor-checks.js +8 -8
  99. package/dist/src/doctor/doctor-claude-checks.js +70 -36
  100. package/dist/src/doctor/doctor-codex-checks.js +6 -6
  101. package/dist/src/doctor/doctor-contention-checks.js +57 -117
  102. package/dist/src/doctor/doctor-fallback-checks.js +48 -26
  103. package/dist/src/doctor/doctor-holds.js +8 -47
  104. package/dist/src/doctor/doctor-install.js +1 -1
  105. package/dist/src/doctor/doctor-launch-services.js +1 -1
  106. package/dist/src/doctor/doctor-model-checks.js +178 -94
  107. package/dist/src/doctor/doctor-notify.js +2 -2
  108. package/dist/src/doctor/doctor-opencode-checks.js +5 -4
  109. package/dist/src/doctor/doctor-orphans.js +122 -21
  110. package/dist/src/doctor/{outcome.js → doctor-outcome.js} +7 -0
  111. package/dist/src/doctor/doctor-pi-checks.js +2 -2
  112. package/dist/src/doctor/doctor-principles.js +3 -3
  113. package/dist/src/doctor/doctor-project-checks.js +24 -11
  114. package/dist/src/doctor/doctor-role-notes.js +1 -1
  115. package/dist/src/doctor/doctor.js +8 -11
  116. package/dist/src/events/event-format.js +44 -64
  117. package/dist/src/events/event-labels.js +1 -5
  118. package/dist/src/events/event-legacy.js +3 -5
  119. package/dist/src/events/{notify.js → event-notify.js} +6 -6
  120. package/dist/src/events/event-read.js +1 -1
  121. package/dist/src/events/event-window.js +1 -1
  122. package/dist/src/events/events.js +8 -0
  123. package/dist/src/failure/failure-cluster.js +9 -18
  124. package/dist/src/failure/failure-data.js +12 -8
  125. package/dist/src/failure/failure-render.js +46 -36
  126. package/dist/src/failure/failure-state-change.js +54 -120
  127. package/dist/src/failure/{time-spend.js → failure-time-spend.js} +117 -84
  128. package/dist/src/files/files.js +30 -0
  129. package/dist/src/files/json-object.js +8 -8
  130. package/dist/src/files/stat-cache.js +12 -8
  131. package/dist/src/gates/budget-gates.js +85 -84
  132. package/dist/src/gates/gate-polls.js +32 -74
  133. package/dist/src/gates/pause-gates.js +16 -2
  134. package/dist/src/gates/readiness.js +1 -1
  135. package/dist/src/gates/role-cap-gates.js +44 -35
  136. package/dist/src/gates/{startup-gate.js → startup-gates.js} +9 -8
  137. package/dist/src/gates/{streak-gate.js → streak-gates.js} +75 -52
  138. package/dist/src/git/git-diff.js +6 -2
  139. package/dist/src/git/git.js +75 -45
  140. package/dist/src/git/worktree-pool.js +3 -6
  141. package/dist/src/gui/gui-admin-endpoints.js +3 -3
  142. package/dist/src/gui/gui-args.js +12 -0
  143. package/dist/src/gui/gui-backlog-commands.js +4 -4
  144. package/dist/src/gui/gui-backlog-endpoints.js +61 -41
  145. package/dist/src/gui/gui-command.js +43 -24
  146. package/dist/src/gui/gui-config-commands.js +2 -64
  147. package/dist/src/gui/gui-endpoint-commands.js +1 -1
  148. package/dist/src/gui/gui-endpoint-common.js +19 -1
  149. package/dist/src/gui/gui-history-endpoints.js +100 -52
  150. package/dist/src/gui/gui-operator-commands.js +43 -48
  151. package/dist/src/gui/gui-prompt-commands.js +25 -25
  152. package/dist/src/gui/gui-report-endpoints.js +1 -1
  153. package/dist/src/gui/gui-server.js +1 -2
  154. package/dist/src/gui/gui-team-commands.js +2 -1
  155. package/dist/src/history/history-data.js +71 -44
  156. package/dist/src/history/history-queued.js +49 -27
  157. package/dist/src/history/history.js +54 -36
  158. package/dist/src/inbox/prompt-commands.js +59 -42
  159. package/dist/src/init/init.js +88 -40
  160. package/dist/src/merge/merge-backlog-conflicts.js +1 -1
  161. package/dist/src/merge/merge-check-failures.js +12 -9
  162. package/dist/src/merge/merge-core.js +7 -32
  163. package/dist/src/merge/merge-drain.js +11 -13
  164. package/dist/src/merge/merge-pipeline.js +6 -7
  165. package/dist/src/merge/merge-post-review-drain.js +11 -14
  166. package/dist/src/merge/merge-queue-render.js +3 -3
  167. package/dist/src/merge/merge-requeue.js +1 -1
  168. package/dist/src/merge/merge-slot.js +0 -8
  169. package/dist/src/merge/merge-stack.js +17 -18
  170. package/dist/src/merge/merge-to-main.js +13 -58
  171. package/dist/src/merge/merge-verify.js +12 -21
  172. package/dist/src/notes-cli.js +52 -42
  173. package/dist/src/operator/operator-commands.js +80 -47
  174. package/dist/src/operator/operator-intent.js +3 -3
  175. package/dist/src/orchestrator/orchestrator-launch.js +31 -63
  176. package/dist/src/orchestrator/orchestrator-poll.js +44 -69
  177. package/dist/src/orchestrator/orchestrator-scheduling.js +5 -9
  178. package/dist/src/orchestrator/orchestrator.js +8 -21
  179. package/dist/src/orchestrator/scheduling-admit.js +101 -68
  180. package/dist/src/orchestrator/scheduling-scope.js +18 -27
  181. package/dist/src/orchestrator/scheduling-skip.js +10 -14
  182. package/dist/src/orchestrator/startup-migrations.js +1 -1
  183. package/dist/src/paths.js +10 -9
  184. package/dist/src/pi/pi-args.js +5 -2
  185. package/dist/src/pi/pi-backend.js +2 -2
  186. package/dist/src/pi/pi-bin.js +3 -3
  187. package/dist/src/pi/pi-event-line.js +28 -1
  188. package/dist/src/pi/pi-listed-models.js +13 -0
  189. package/dist/src/pi/pi-models.js +43 -29
  190. package/dist/src/pi/pi-settings.js +43 -20
  191. package/dist/src/pi/pi-stream.js +41 -8
  192. package/dist/src/pi/pi-watchdogs.js +43 -15
  193. package/dist/src/pi/pi.js +2 -2
  194. package/dist/src/pi-extension/context-shake.js +32 -20
  195. package/dist/src/pi-extension/kill-guard.js +69 -0
  196. package/dist/src/process/process-group.js +51 -26
  197. package/dist/src/process/process.js +20 -12
  198. package/dist/src/process/run-marker.js +33 -15
  199. package/dist/src/process/supervisor.js +40 -28
  200. package/dist/src/prompt/dialect.js +1 -1
  201. package/dist/src/prompt/gate-notes.js +2 -2
  202. package/dist/src/prompt/gate-prompts.js +1 -44
  203. package/dist/src/prompt/notes-backlog.js +66 -37
  204. package/dist/src/prompt/principles.js +6 -4
  205. package/dist/src/prompt/prompt-followup.js +12 -4
  206. package/dist/src/prompt/prompt.js +88 -38
  207. package/dist/src/redeploy/redeployer.js +21 -1
  208. package/dist/src/report/report-data.js +53 -37
  209. package/dist/src/report/report-fold.js +16 -9
  210. package/dist/src/report/report-render.js +21 -194
  211. package/dist/src/report/report-since-data.js +4 -3
  212. package/dist/src/report/report-since-render.js +173 -0
  213. package/dist/src/report/report-usage-fold.js +8 -8
  214. package/dist/src/report/report.js +46 -22
  215. package/dist/src/review/diff-rules.js +35 -21
  216. package/dist/src/review/post-merge-review-render.js +9 -3
  217. package/dist/src/review/post-merge-review.js +48 -41
  218. package/dist/src/review/review-precheck.js +14 -4
  219. package/dist/src/review/review.js +48 -170
  220. package/dist/src/review/secret-leaks.js +26 -16
  221. package/dist/src/roles/legacy-role-migration.js +6 -3
  222. package/dist/src/roles/role-catalog.js +11 -3
  223. package/dist/src/roles/role-guidance.js +16 -12
  224. package/dist/src/roles/role-render.js +7 -46
  225. package/dist/src/roles/role-view.js +10 -33
  226. package/dist/src/run/acceptance-greps.js +88 -45
  227. package/dist/src/run/ci-parity.js +19 -8
  228. package/dist/src/run/run-apply.js +12 -15
  229. package/dist/src/run/run-detail-data.js +121 -68
  230. package/dist/src/run/run-detail-render.js +92 -35
  231. package/dist/src/run/run-detail.js +72 -41
  232. package/dist/src/run/run-prompt.js +63 -50
  233. package/dist/src/run/run-resume.js +76 -29
  234. package/dist/src/run/run-stage.js +141 -90
  235. package/dist/src/run/run-timing.js +8 -2
  236. package/dist/src/run/run-usage.js +31 -1
  237. package/dist/src/run/run-verdict.js +128 -50
  238. package/dist/src/run/stage-check.js +60 -50
  239. package/dist/src/run/stage-checks-shared.js +28 -0
  240. package/dist/src/run/stage-name-checks.js +111 -92
  241. package/dist/src/run/stage-path-checks.js +69 -42
  242. package/dist/src/run/stage-prose-checks.js +55 -40
  243. package/dist/src/runner/{leftover.js → runner-leftover.js} +9 -26
  244. package/dist/src/runner/runner-outcome.js +7 -2
  245. package/dist/src/runner/runner-pi.js +9 -22
  246. package/dist/src/runner/runner-recovery.js +5 -13
  247. package/dist/src/runner/{revision.js → runner-revision.js} +1 -2
  248. package/dist/src/runner/runner-run.js +103 -50
  249. package/dist/src/runner/runner-state-load.js +18 -28
  250. package/dist/src/runner/runner-state.js +0 -14
  251. package/dist/src/runner/runner.js +41 -25
  252. package/dist/src/scheduling/backoff.js +8 -3
  253. package/dist/src/scheduling/claim-entries.js +2 -1
  254. package/dist/src/scheduling/claims.js +0 -4
  255. package/dist/src/scheduling/scheduling.js +53 -66
  256. package/dist/src/status/status-budget.js +4 -43
  257. package/dist/src/status/status-data.js +35 -39
  258. package/dist/src/status/status-role-notes.js +2 -2
  259. package/dist/src/status/status-roles.js +9 -18
  260. package/dist/src/team/{error-storm.js → team-error-storm.js} +1 -1
  261. package/dist/src/team/{failure-spread.js → team-failure-spread.js} +5 -5
  262. package/dist/src/team/team-hold.js +105 -70
  263. package/dist/src/team/{orchestrator-info.js → team-orchestrator-info.js} +55 -36
  264. package/dist/src/team/team-polls.js +53 -201
  265. package/dist/src/team/{reclaim.js → team-reclaim.js} +61 -31
  266. package/dist/src/team/team-state.js +4 -13
  267. package/dist/src/text/datetime.js +16 -7
  268. package/dist/src/text/markdown.js +1 -1
  269. package/dist/src/text/phrases.js +29 -15
  270. package/dist/src/text/suggest.js +1 -1
  271. package/dist/src/text/text.js +6 -5
  272. package/dist/src/ui/alert-feeds.js +46 -111
  273. package/dist/src/ui/badges.js +17 -88
  274. package/dist/src/ui/gui/gui-client-boot.js +2 -8
  275. package/dist/src/ui/gui/gui-client-composer.js +1 -1
  276. package/dist/src/ui/gui/gui-client-drawer-config.js +8 -33
  277. package/dist/src/ui/gui/gui-client-drawer.js +1 -1
  278. package/dist/src/ui/gui/gui-client-feed.js +1 -1
  279. package/dist/src/ui/gui/gui-client-history.js +28 -14
  280. package/dist/src/ui/gui/gui-client-model.js +0 -12
  281. package/dist/src/ui/gui/gui-client-operator.js +7 -5
  282. package/dist/src/ui/gui/gui-client-pending.js +7 -6
  283. package/dist/src/ui/gui/gui-client-report-chart.js +5 -5
  284. package/dist/src/ui/gui/gui-client-report.js +1 -1
  285. package/dist/src/ui/gui/gui-client-roles.js +0 -1
  286. package/dist/src/ui/gui/gui-client-settings.js +43 -84
  287. package/dist/src/ui/gui/gui-client-team.js +12 -6
  288. package/dist/src/ui/gui/gui-client.js +7 -1
  289. package/dist/src/ui/progress-data.js +107 -44
  290. package/dist/src/ui/run-progress-model.js +20 -4
  291. package/dist/src/ui/status-model.js +2 -24
  292. package/dist/src/ui/status-payload.js +11 -30
  293. package/dist/src/ui/status-render.js +13 -24
  294. package/dist/src/ui/team-alert-backlog.js +14 -204
  295. package/dist/src/ui/team-alert-shared.js +6 -29
  296. package/dist/src/ui/team-alert-status.js +9 -170
  297. package/dist/src/ui/team-alert-tail.js +19 -215
  298. package/dist/src/ui/team-alerts.js +8 -15
  299. package/dist/src/ui/transcript-run.js +27 -16
  300. package/dist/src/ui/tui/tui-backlog.js +2 -2
  301. package/dist/src/ui/tui/tui-editor-answer.js +12 -10
  302. package/dist/src/ui/tui/tui-editor-attach.js +4 -3
  303. package/dist/src/ui/tui/tui-editor-backends.js +4 -3
  304. package/dist/src/ui/tui/tui-editor-brief.js +6 -9
  305. package/dist/src/ui/tui/tui-editor-budget.js +4 -3
  306. package/dist/src/ui/tui/tui-editor-config.js +80 -68
  307. package/dist/src/ui/tui/tui-editor-entry.js +12 -20
  308. package/dist/src/ui/tui/tui-editor-pause.js +51 -57
  309. package/dist/src/ui/tui/tui-editor-prompt.js +9 -13
  310. package/dist/src/ui/tui/tui-editor-queued.js +11 -14
  311. package/dist/src/ui/tui/tui-editor-quiet.js +4 -3
  312. package/dist/src/ui/tui/tui-editor-release.js +4 -3
  313. package/dist/src/ui/tui/tui-editor-retire.js +7 -7
  314. package/dist/src/ui/tui/tui-editor-role-enabled.js +4 -3
  315. package/dist/src/ui/tui/tui-editor-role-file.js +4 -4
  316. package/dist/src/ui/tui/tui-editor-role-key.js +4 -3
  317. package/dist/src/ui/tui/tui-editor-role-model.js +97 -15
  318. package/dist/src/ui/tui/tui-editor-search.js +4 -3
  319. package/dist/src/ui/tui/tui-editor-session.js +17 -4
  320. package/dist/src/ui/tui/tui-editors.js +5 -10
  321. package/dist/src/ui/tui/tui-frame.js +182 -134
  322. package/dist/src/ui/tui/tui-guard.js +20 -0
  323. package/dist/src/ui/tui/tui-input.js +167 -98
  324. package/dist/src/ui/tui/tui-keymap.js +51 -29
  325. package/dist/src/ui/tui/tui-keys.js +1 -1
  326. package/dist/src/ui/tui/tui-pane-activity.js +5 -4
  327. package/dist/src/ui/tui/tui-pane-backlog.js +74 -36
  328. package/dist/src/ui/tui/tui-pane-ctx.js +1 -1
  329. package/dist/src/ui/tui/tui-pane-health.js +252 -230
  330. package/dist/src/ui/tui/tui-pane-history.js +67 -74
  331. package/dist/src/ui/tui/tui-pane-pending.js +288 -247
  332. package/dist/src/ui/tui/tui-pane-queued.js +25 -27
  333. package/dist/src/ui/tui/tui-pane-report.js +2 -1
  334. package/dist/src/ui/tui/tui-pane-session.js +15 -10
  335. package/dist/src/ui/tui/tui-pane-transcript.js +19 -31
  336. package/dist/src/ui/tui/tui-pane.js +14 -11
  337. package/dist/src/ui/tui/tui-panes.js +38 -33
  338. package/dist/src/ui/tui/tui.js +4 -7
  339. package/dist/src/verdict/fix-claim.js +107 -56
  340. package/dist/src/verdict/refusal.js +1 -1
  341. package/dist/src/verdict/reply-contract.js +3 -4
  342. package/package.json +5 -5
  343. package/dist/src/concurrency/provider-lease.js +0 -105
  344. package/dist/src/review/review-followup.js +0 -87
  345. package/dist/src/review/review-run.js +0 -395
  346. package/dist/src/runner/model-fallback.js +0 -76
  347. package/dist/src/runner/runner-fallback.js +0 -66
  348. package/dist/src/ui/tui/tui-editor-claude-reserve.js +0 -24
  349. package/dist/src/ui/tui/tui-editor-provider-caps.js +0 -27
  350. /package/dist/src/cli/{brief-command.js → cli-brief-command.js} +0 -0
  351. /package/dist/src/doctor/{probe.js → doctor-probe.js} +0 -0
  352. /package/dist/src/gates/{disk-gate.js → disk-gates.js} +0 -0
  353. /package/dist/src/operator/{retire.js → operator-retire.js} +0 -0
  354. /package/dist/src/operator/{revert.js → operator-revert.js} +0 -0
@@ -1,4 +1,3 @@
1
- import { claudeUtilizationPct } from "../agent/claude/claude-account.js";
2
1
  import { eventRole, eventRun, eventUsage } from "./event-read.js";
3
2
  import { eventTypeLabel } from "./event-labels.js";
4
3
  import { roleLabel } from "../roles/role-labels.js";
@@ -50,6 +49,14 @@ function elapsed(ms) {
50
49
  return "";
51
50
  return ` (in ${shortSpanPhrase(n)})`;
52
51
  }
52
+ /** The `waited <duration>` head of the three wait-family events (slot_wait, provider_wait,
53
+ * check_wait) — each rendered its own `waited ${shortSpanPhrase(finiteNumber(e.waitedMs, 0))}`
54
+ * inline, so the corrupt-field rule and the s/m bucketing cannot drift between them. Each
55
+ * caller keeps its own `to start`/`for a run slot`/`for a check slot` tail and its own
56
+ * parenthetical after it. */
57
+ function waitedSpan(waitedMs) {
58
+ return `waited ${shortSpanPhrase(finiteNumber(waitedMs, 0))}`;
59
+ }
53
60
  /** A disk event's GB field as "12.3 GB" text. A torn or hand-edited event whose value is
54
61
  * missing or non-numeric reads as "0.0 GB", never "NaN" — the same corrupt-field rule
55
62
  * budgetPhrase applies to spentUsd/capUsd, since HarnessEvent's index signature hands every
@@ -111,9 +118,16 @@ function providerScope(e) {
111
118
  export function backendLabel(e) {
112
119
  return typeof e.backend === "string" && e.backend !== "" ? e.backend : "claude";
113
120
  }
121
+ /** A subscription-window event's window, as "<backend> subscription window <type>" — the head
122
+ * every `subscription_window_*` line names (windowType is a torn/older event's only nullable
123
+ * half, so `reset` and the shaped lines differ only in what follows this prefix). One home so
124
+ * the four lines cannot drift on the backend's label or the window-type word. */
125
+ function subscriptionWindowPhrase(e) {
126
+ return `${backendLabel(e)} subscription window ${textOr(e.windowType)}`;
127
+ }
114
128
  /** The provider/model pair a fallback-ish line names, as "provider/model", defaulting each
115
129
  * half when the event omits it (a bare pi run carries neither). Shared by the budget_fallback
116
- * and model_fallback lines so the pair's shape and defaults cannot drift between them. */
130
+ * line so the pair's shape and defaults cannot drift. */
117
131
  function providerModelPair(e, providerDefault, modelDefault) {
118
132
  return `${textOr(e.provider, providerDefault)}/${textOr(e.model, modelDefault)}`;
119
133
  }
@@ -177,7 +191,7 @@ export function eventFeedItem(e) {
177
191
  }
178
192
  /** The feed's `--grep`/Search match rule: case-insensitive substring over the raw event type
179
193
  * prefixed to the rendered line — exactly the haystack the operator would otherwise read. One
180
- * home shared by the CLI's `tumwater logs --grep` (cli/log-commands.ts) and the dashboard's
194
+ * home shared by the CLI's `tumwater logs --grep` (cli/cli-log-commands.ts) and the dashboard's
181
195
  * /api/logs search, so the two surfaces cannot disagree about what a pattern matches. */
182
196
  export function eventMatchesGrep(e, patternLower) {
183
197
  return `${e.type} ${formatEvent(e)}`.toLowerCase().includes(patternLower);
@@ -213,9 +227,11 @@ function runEventMessage(e) {
213
227
  ? "resuming the run cut off at the context ceiling (compacted pi session, same workspace)"
214
228
  : e.cause === "handoff"
215
229
  ? "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);
230
+ : e.cause === "image-reroute"
231
+ ? "handing the blind-read run off to a fresh session on an image-capable model (same workspace, uncommitted edits kept)"
232
+ : e.cause === "salvaged"
233
+ ? "handing the work an earlier run left unsaved to a fresh session"
234
+ : "resuming the run a shutdown interrupted (same agent session and workspace)") + restored);
219
235
  }
220
236
  case "wake":
221
237
  return `woke (${textOr(e.reason)})`;
@@ -304,22 +320,19 @@ function mergeEventMessage(e) {
304
320
  default: return textOr(e.type);
305
321
  }
306
322
  }
307
- /** " after merge" when the review is a post-merge review (review.afterMerge), else "". */
323
+ /** " after merge" when the review is a post-merge review, else "". */
308
324
  function afterMergeSuffix(e) {
309
325
  return e.afterMerge === true ? " after merge" : "";
310
326
  }
311
327
  function reviewEventMessage(e) {
312
328
  switch (e.type) {
313
329
  case "review_start": {
314
- // A resumed review continues a timed-out reviewer's session (PLANS.md, review-retry part
315
- // 3/3). The verb names that, because a bare "reviewing" line reads as a from-scratch run;
316
- // the revision and model suffixes are unchanged and independent of the flag.
317
- // afterMerge is the tier flag reviewMergedCommit writes for a review.afterMerge role; the
330
+ // afterMerge is the tier flag reviewMergedCommit writes for a post-merge review; the
318
331
  // lineage pins review_start/review_verdict/review_rejected to the same discriminator.
319
332
  const tier = e.afterMerge === true ? " after merge" : " before merge";
320
333
  const revision = e.revision !== undefined ? ` (revision ${textOr(e.revision)})` : "";
321
334
  const model = e.model !== undefined ? ` on ${textOr(e.model)}` : "";
322
- return `${e.resumed ? "review resumed" : "reviewing"} ${shortSha(e.head)}${revision}${tier}${model}`;
335
+ return `reviewing ${shortSha(e.head)}${revision}${tier}${model}`;
323
336
  }
324
337
  case "review_verdict":
325
338
  return `review approved ${shortSha(e.head)}${afterMergeSuffix(e)}${e.reason ? ` — ${textOr(e.reason)}` : ""}${elapsed(e.durationMs)}`;
@@ -430,13 +443,6 @@ function budgetEventMessage(e) {
430
443
  });
431
444
  return `fallback: ${parts.join("; ")}`;
432
445
  }
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
446
  case "opencode_free_model_billed":
441
447
  // The promotion ended (plans/opencode-backend.md §7, part 6c/7): a run billed what the
442
448
  // catalog called free, so the team now prices that model as paid from here on. No warning
@@ -531,19 +537,6 @@ function resourceEventMessage(e) {
531
537
  return `backend recovered (${backendKindPhrase(e.kind)})${providerAt(e)} — roles on ${providerScope(e)} run again`;
532
538
  return `provider limit lifted${providerAt(e)} — roles on ${providerScope(e)} run again`;
533
539
  }
534
- case "subscription_window_warning": {
535
- // Routine-with-explanation, like rate_limit_hold — no warning prefix: the account is
536
- // telling us the window is filling, not failing. It names the window and when it resets.
537
- const used = finiteNumber(e.utilization, null);
538
- const fill = used === null ? "" : ` (at ${claudeUtilizationPct(used)}% used)`;
539
- return `${backendLabel(e)} subscription window ${textOr(e.windowType)} near its limit${fill}${windowResetPhrase(e)} — subscription ${backendLabel(e)} model jobs keep running`;
540
- }
541
- case "subscription_window_reserved": {
542
- const used = finiteNumber(e.utilization, null);
543
- const line = finiteNumber(e.reserve, null);
544
- const pct = (f) => (f === null ? "?" : `${claudeUtilizationPct(f)}%`);
545
- return `${backendLabel(e)} subscription window ${textOr(e.windowType)} at ${pct(used)}, past its ${pct(line)} reserve${windowResetPhrase(e)} — the team leaves it to you (director keeps running)`;
546
- }
547
540
  case "subscription_window_limited":
548
541
  // Routine-with-explanation, like rate_limit_hold — no warning prefix: waiting is the
549
542
  // harness handling the account's own throttle. Names the window, the reset, and which
@@ -554,9 +547,9 @@ function resourceEventMessage(e) {
554
547
  {
555
548
  const moved = fallbackTierEntries(isJsonObject(e.tiers) ? e.tiers : undefined);
556
549
  if (moved.length > 0)
557
- return `${backendLabel(e)} subscription window ${textOr(e.windowType)} rejected — ${backendLabel(e)} model jobs move to ${moved.join(", ")}${windowResetPhrase(e)} (director keeps waiting)`;
550
+ return `${subscriptionWindowPhrase(e)} rejected — ${backendLabel(e)} model jobs move to ${moved.join(", ")}${windowResetPhrase(e)} (director keeps waiting)`;
558
551
  }
559
- return `waiting: ${backendLabel(e)} subscription window ${textOr(e.windowType)} rejected — ${backendLabel(e)} model jobs wait${windowResetPhrase(e)} (director keeps running)`;
552
+ return `waiting: ${subscriptionWindowPhrase(e)} rejected — ${backendLabel(e)} model jobs wait${windowResetPhrase(e)} (director keeps running)`;
560
553
  case "subscription_window_reset":
561
554
  return `${backendLabel(e)} subscription window reset — ${backendLabel(e)} model jobs run again`;
562
555
  default: return textOr(e.type);
@@ -568,10 +561,6 @@ function configEventMessage(e) {
568
561
  // Routine state change, like counters_reset — no warning prefix.
569
562
  return `maxConcurrent changed: ${textOr(e.from)} → ${textOr(e.to)}`;
570
563
  }
571
- case "provider_concurrency_changed": {
572
- // Routine state change, its maxConcurrent sibling's per-provider form — no warning prefix.
573
- return `concurrency for ${textOr(e.provider)} changed: ${textOr(e.from)} → ${textOr(e.to)}`;
574
- }
575
564
  case "retention_changed": {
576
565
  // Routine state change, like its maxConcurrent sibling — no warning prefix.
577
566
  return `sessionRetentionDays changed: ${textOr(e.from)} → ${textOr(e.to)}`;
@@ -604,9 +593,9 @@ function configEventMessage(e) {
604
593
  function imageRestartEventMessage(e) {
605
594
  switch (e.type) {
606
595
  case "image_blind": {
607
- // Routine-with-explanation like the model-fallback events — no warning prefix: the
608
- // run was stopped (or a gate run noted) because the model cannot see the image. Names
609
- // the file, the model, and whether the run failed on it or a gate run only logged it.
596
+ // Routine-with-explanation — no warning prefix: the run was stopped (or a gate run
597
+ // noted) because the model cannot see the image. Names the file, the model, and
598
+ // whether the run failed on it or a gate run only logged it.
610
599
  const action = e.action === "logged"
611
600
  ? " (gate run logged)"
612
601
  : e.action === "fail"
@@ -614,6 +603,13 @@ function imageRestartEventMessage(e) {
614
603
  : "";
615
604
  return `blind image read: ${textOr(e.path)} on ${textOr(e.model, "the model")}${action}`;
616
605
  }
606
+ case "image_reroute":
607
+ // The run's work moves to a model that can read the image (plans/image-input.md part
608
+ // 4/5). Routine-with-explanation — no warning prefix: names the hop the way the
609
+ // handoff reads, and whether the traits-of-attachment preflight avoided a failed run.
610
+ return e.preflight === true
611
+ ? `image attachment routed to ${textOr(e.to, "the target")} (${textOr(e.tier, "a peer tier")}) to read its image — no failed run`
612
+ : `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")}`;
617
613
  case "build_stale":
618
614
  // Self-hosting teams only (src/redeploy/redeploy.ts): the code main describes is not the
619
615
  // code running. Not a warning prefix — a stale build is a state, and auto-restart resolves
@@ -666,25 +662,13 @@ function checkWaitEventMessage(e) {
666
662
  // working as designed. Names the wait, the pool it waited on, and how many slots are
667
663
  // pinned, so the operator can raise worktreeSlots with the numbers in hand.
668
664
  const purpose = e.purpose === "verify" ? "check" : "run";
669
- return `waited ${shortSpanPhrase(finiteNumber(e.waitedMs, 0))} to start (${textOr(e.role)} ${purpose}; ${textOr(e.slots)} workspaces, ${textOr(e.pinned)} pinned)`;
665
+ return `${waitedSpan(e.waitedMs)} to start (${textOr(e.role)} ${purpose}; ${textOr(e.slots)} workspaces, ${textOr(e.pinned)} pinned)`;
670
666
  }
671
667
  case "provider_wait": {
672
- // Routine-with-explanation, like slot_wait: waiting for a provider/team slot is the cap
673
- // working as designed, so no warning prefix. Names the wait and the key (or the run cap
674
- // when the seam has none), so the operator can raise maxConcurrentPerProvider with the
675
- // numbers in hand. An event carrying `cap` names the cap that held the run and its limit
676
- // (a team-cap wait names no key); one without `cap` (logged before it existed) renders
677
- // the key-or-run-slot line alone.
678
- const span = shortSpanPhrase(finiteNumber(e.waitedMs, 0));
679
- const limit = finiteNumber(e.limit, Number.NaN);
680
- const hasKey = typeof e.key === "string" && e.key.length > 0;
681
- if (e.cap === "provider" && hasKey) {
682
- return `waited ${span} for a ${e.key} slot (${textOr(e.role)}${Number.isFinite(limit) ? `; maxConcurrentPerProvider.${e.key} is ${limit}` : ""})`;
683
- }
684
- if (e.cap === "team") {
685
- return `waited ${span} for a run slot (${textOr(e.role)}${Number.isFinite(limit) ? `; maxConcurrent is ${limit}` : ""})`;
686
- }
687
- return `waited ${span} for a ${hasKey ? `${e.key} slot` : "run slot"} (${textOr(e.role)})`;
668
+ // Routine-with-explanation, like slot_wait: waiting for the team slot is the cap
669
+ // working as designed, so no warning prefix. Names the wait, so the operator can size
670
+ // maxConcurrent with the number in hand.
671
+ return `${waitedSpan(e.waitedMs)} for a run slot (${textOr(e.role)})`;
688
672
  }
689
673
  case "check_wait":
690
674
  // Routine-with-explanation, like slot_wait: waiting for a check permit is the cap working
@@ -694,7 +678,7 @@ function checkWaitEventMessage(e) {
694
678
  const heldBy = stringList(e.heldBy);
695
679
  const limit = finiteNumber(e.limit, null);
696
680
  const behind = heldBy.length > 0 ? `; held by ${heldBy.join(", ")}${limit === null ? "" : ` of maxConcurrentChecks ${limit}`}` : "";
697
- return `waited ${shortSpanPhrase(finiteNumber(e.waitedMs, 0))} for a check slot (${textOr(e.role)} ${textOr(e.scope)} check${behind})`;
681
+ return `${waitedSpan(e.waitedMs)} for a check slot (${textOr(e.role)} ${textOr(e.scope)} check${behind})`;
698
682
  }
699
683
  default: return textOr(e.type);
700
684
  }
@@ -754,8 +738,6 @@ export function eventMessage(e) {
754
738
  case "budget_resumed":
755
739
  case "budget_handback":
756
740
  case "fallback_switched":
757
- case "model_fallback_started":
758
- case "model_fallback_ended":
759
741
  case "opencode_free_model_billed": return budgetEventMessage(e);
760
742
  case "team_paused":
761
743
  case "team_resumed":
@@ -775,12 +757,9 @@ export function eventMessage(e) {
775
757
  case "quiet_hours_ended":
776
758
  case "rate_limit_hold":
777
759
  case "rate_limit_resumed":
778
- case "subscription_window_warning":
779
- case "subscription_window_reserved":
780
760
  case "subscription_window_limited":
781
761
  case "subscription_window_reset": return resourceEventMessage(e);
782
762
  case "max_concurrent_changed":
783
- case "provider_concurrency_changed":
784
763
  case "retention_changed":
785
764
  case "config_changed":
786
765
  case "model_changed":
@@ -789,6 +768,7 @@ export function eventMessage(e) {
789
768
  case "orchestrator_start":
790
769
  case "orchestrator_stop": return configEventMessage(e);
791
770
  case "image_blind":
771
+ case "image_reroute":
792
772
  case "build_stale":
793
773
  case "restart_pending":
794
774
  case "restart":
@@ -70,20 +70,16 @@ export const EVENT_TYPE_LABELS = {
70
70
  check_wait: "waited for a check slot",
71
71
  rate_limit_hold: "waiting: provider limit",
72
72
  rate_limit_resumed: "provider limit lifted",
73
- subscription_window_reserved: "usage window reserved",
74
- subscription_window_warning: "usage window warning",
75
73
  subscription_window_limited: "waiting: usage window",
76
74
  subscription_window_reset: "usage window reset",
77
75
  max_concurrent_changed: "concurrency changed",
78
- provider_concurrency_changed: "provider concurrency changed",
79
76
  retention_changed: "retention changed",
80
77
  config_changed: "settings changed",
81
78
  model_changed: "model changed",
82
79
  backend_binary: "backend checked",
83
- model_fallback_started: "fallback model started",
84
- model_fallback_ended: "fallback model ended",
85
80
  opencode_free_model_billed: "free model billed",
86
81
  image_blind: "image not seen",
82
+ image_reroute: "image sent to another model",
87
83
  build_stale: "update available",
88
84
  restart_pending: "update waits for idle",
89
85
  restart: "restarted onto update",
@@ -25,8 +25,6 @@ const LEGACY_EVENT_TYPES = {
25
25
  land_failed: "merge_failed", // vocabulary-ok: legacy event name read from old logs
26
26
  // retired-name-ok:end
27
27
  // retired-name-ok:start old subscription-window event names
28
- claude_window_reserved: "subscription_window_reserved", // vocabulary-ok: legacy event name read from old logs
29
- claude_window_warning: "subscription_window_warning", // vocabulary-ok: legacy event name read from old logs
30
28
  claude_window_limited: "subscription_window_limited", // vocabulary-ok: legacy event name read from old logs
31
29
  claude_window_reset: "subscription_window_reset", // vocabulary-ok: legacy event name read from old logs
32
30
  // retired-name-ok:end
@@ -34,7 +32,7 @@ const LEGACY_EVENT_TYPES = {
34
32
  /** The ordered field rewrites; later parts of the series append here, and they run in order. */
35
33
  const LEGACY_EVENT_REWRITES = [
36
34
  {
37
- // The run number rode `tick` on run_start, run_end, prompt_taken, image_blind.
35
+ // The run number rode `tick` on run_start, run_end, prompt_taken, image_blind, image_reroute.
38
36
  name: "tick-to-run", // retired-name-ok: legacy name
39
37
  apply(ev) {
40
38
  const e = ev;
@@ -110,8 +108,8 @@ const LEGACY_EVENT_REWRITES = [
110
108
  },
111
109
  },
112
110
  {
113
- // A retired role id on an old line reads as the role's current id. The table is empty
114
- // until a switch part adds a row, so this changes nothing yet.
111
+ // A retired role id on an old line reads as the role's current id. The table grows a row
112
+ // per retired role as future switch parts rename; a row already present applies here.
115
113
  name: "legacy-role-ids",
116
114
  apply(ev) {
117
115
  const e = ev;
@@ -10,12 +10,12 @@ import { childEnv } from "../process/process.js";
10
10
  *
11
11
  * Scope, stated plainly: the notifier subscribes via subscribeEvents (src/events/events.ts), which
12
12
  * only sees events THIS process logs. Every allowlisted type but `supervisor_exit` is logged by
13
- * the orchestrator's own process (budget-gates.ts, gate-polls.ts → streak-gate.ts,
14
- * role-cap-gates.ts and disk-gate.ts, src/baseline/main-red.ts, src/team/team-polls.ts,
13
+ * the orchestrator's own process (budget-gates.ts, gate-polls.ts → streak-gates.ts,
14
+ * role-cap-gates.ts and disk-gates.ts, src/baseline/main-red.ts, src/team/team-polls.ts,
15
15
  * merge-slot.ts, merge-questions.ts,
16
16
  * src/redeploy/redeployer.ts). `supervisor_exit` is logged by the supervisor after the
17
- * orchestrator is gone, so src/cli/cli-start.ts pages it directly with notifyTeamDown; events
18
- * appended by
17
+ * orchestrator is gone, so src/cli/cli-start-supervisor.ts pages it directly with notifyTeamDown;
18
+ * events appended by
19
19
  * operator CLI commands go straight to the file and are out of scope by design.
20
20
  *
21
21
  * One knob, an opinionated fixed allowlist, no per-event selection — a command that can post
@@ -45,7 +45,7 @@ export const NOTIFY_EVENT_TYPES = [
45
45
  export const NOTIFY_MIN_GAP_MS = 60_000;
46
46
  /** The child environment the notify hook runs with: the three TUMWATER_EVENT_* variables over
47
47
  * the harness's own environment. One home for the variable names, shared by the notifier's
48
- * spawn and the `tumwater notify --test` diagnostic (src/cli/notify-command.ts), so the two
48
+ * spawn and the `tumwater notify --test` diagnostic (src/cli/cli-notify-command.ts), so the two
49
49
  * cannot drift on what a hook sees. */
50
50
  export function notifyCommandEnv(type, source, message) {
51
51
  return childEnv({
@@ -76,7 +76,7 @@ export function notifyExitWarning(type, code, signal) {
76
76
  * not found", 126 its "found but not executable" — most often the configured `notify` command
77
77
  * is a typo or an uninstalled/non-executable binary, so a bare "code 127" hides the fix the
78
78
  * gloss names. Shared by the notifier's warning (notifyExitWarning above) and `tumwater
79
- * notify --test`'s diagnostic (src/cli/notify-command.ts) so both surfaces name the same fix
79
+ * notify --test`'s diagnostic (src/cli/cli-notify-command.ts) so both surfaces name the same fix
80
80
  * for a hook that cannot launch. Any other code gets no gloss. */
81
81
  export function notifyExitGloss(code) {
82
82
  return code === 127 ? " (command not found)" : code === 126 ? " (found but not executable)" : "";
@@ -87,7 +87,7 @@ function runStartEvents(events) {
87
87
  }
88
88
  /** The run_start pairing's start epoch ms: `${role}#${run}` → start epoch ms, the projection
89
89
  * of runStartEvents onto each event's `ts` — shared by `tumwater history`'s row spans
90
- * (history-data.ts), the failure digest's time-and-spend fold (time-spend.ts), and the run
90
+ * (history-data.ts), the failure digest's time-and-spend fold (failure-time-spend.ts), and the run
91
91
  * drill-down's duration (run-detail-data.ts), so those consumers cannot drift into different
92
92
  * notions of a run's span. A run_end whose start is not in `events` (log rotation cut it, or
93
93
  * the run was skipped before any start logged) has no entry; callers decide what an unpaired
@@ -321,7 +321,7 @@ export function readEventsSinceJoined(root, sinceMs, joinBackslackMs) {
321
321
  (window.events[firstNarrowIdx]?.ts ?? Number.POSITIVE_INFINITY) <= cutoff;
322
322
  return { cutoff, events, join: { cutoff: joinCutoff, events: joinEvents }, covered };
323
323
  }
324
- /** The duration-shaped windowed read behind `logs --since` (log-commands.ts): the cutoff is
324
+ /** The duration-shaped windowed read behind `logs --since` (cli-log-commands.ts): the cutoff is
325
325
  * now − sinceMs, the rotation-spanning read is keyed on the cutoff's local calendar day
326
326
  * (dayKey — the shared dayKey helper eventDayKey buckets events with, so the read's day keys
327
327
  * cannot disagree with the ts filter), the day-keyed read may include earlier hours of that
@@ -97,6 +97,14 @@ export function logEventBestEffort(root, event) {
97
97
  process.stderr.write(`tumwater: ${stripTerminalControls(detail)}\n`);
98
98
  }
99
99
  }
100
+ /** Log one `image_blind` event (plans/image-input.md "Detecting a blind read"): an agent read an
101
+ * image its model cannot see — the authoring run was killed (action "fail") or a gate run merely
102
+ * recorded the read (action "logged"). The runner and both review paths used to build this exact
103
+ * event object themselves (source + type + run + path + model + action); it lives here so the
104
+ * payload has one home and cannot drift between the three callers. */
105
+ export function logBlindRead(root, source, run, path, model, action) {
106
+ logEventBestEffort(root, { source, type: "image_blind", run, path, model, action });
107
+ }
100
108
  /** warnEvent's twin of logEventBestEffort: the "something is off but the loop continues"
101
109
  * warning must not be the thing that ends the role it warns about. The fallback keeps the
102
110
  * warning's own text on stderr — when the feed is the failure, this line is the only place the
@@ -1,6 +1,6 @@
1
1
  /** The cluster-key rules shared by everything that groups failure messages into causes: the
2
2
  * failure digest's collection (failure-data.ts) and the error-storm reducer
3
- * (src/team/error-storm.ts) both count by these keys, so the normalization, the two
3
+ * (src/team/team-error-storm.ts) both count by these keys, so the normalization, the two
4
4
  * run-timeout shapes and their pooling, and the grouping engine live here rather than
5
5
  * inside either consumer. Pure string
6
6
  * and grouping logic — no event reads, no clock. */
@@ -12,13 +12,13 @@ import { cutSplitsSurrogatePair } from "../text/text.js";
12
12
  export const EXAMPLE_MAX = 120;
13
13
  /** The example and cluster key every message-less failure takes: errorTextOrPlaceholder
14
14
  * returns it for the digest's error clusters (failure-data.ts) and its loss ranking
15
- * (time-spend.ts), so a broken `run_end` that skipped its error text reads the same on both
15
+ * (failure-time-spend.ts), so a broken `run_end` that skipped its error text reads the same on both
16
16
  * surfaces. */
17
17
  const NO_ERROR_TEXT = "(no error text recorded)";
18
18
  /** The error text a failure event carries, or NO_ERROR_TEXT when it carries none — the field
19
19
  * is a non-empty string or absent. The single home of the placeholder rule, shared by the
20
20
  * digest's error clusters (failure-data.ts) and the loss ranking's clustered and review_error
21
- * arms (time-spend.ts), so a message-less failure reads the same on every surface. */
21
+ * arms (failure-time-spend.ts), so a message-less failure reads the same on every surface. */
22
22
  export function errorTextOrPlaceholder(v) {
23
23
  return typeof v === "string" && v !== "" ? v : NO_ERROR_TEXT;
24
24
  }
@@ -49,7 +49,7 @@ export function truncateExample(message, max = EXAMPLE_MAX) {
49
49
  * array in ascending localeCompare order, so equal inputs always render identically no matter
50
50
  * the Set's insertion order. One home for the rule the failure digest's clusters
51
51
  * (clusterMessages) and loss causes (failure-data.ts) and the error-storm warning
52
- * (src/team/error-storm.ts) all share — an inline `.sort()` without the comparator would
52
+ * (src/team/team-error-storm.ts) all share — an inline `.sort()` without the comparator would
53
53
  * silently pick code-unit order instead and let the two rules drift. */
54
54
  export function sortedRoles(roles) {
55
55
  return [...roles].sort((a, b) => a.localeCompare(b));
@@ -76,7 +76,7 @@ export function normalizeClusterKey(message) {
76
76
  .replace(/(?<!exited\s)\b\d+\b/g, "<n>");
77
77
  return normalized.trim().slice(0, EXAMPLE_MAX);
78
78
  }
79
- /** normalizeClusterKey's rendering of the two run-timeout errors src/agent/run-agent.ts emits —
79
+ /** normalizeClusterKey's rendering of the two run-timeout errors src/agent/agent-run.ts emits —
80
80
  * the plain kill and the still-making-progress variant (session and worktree edits preserved
81
81
  * for resume, BUGS.md 2026-09-29). Both are one cause pointing at one knob (runTimeoutSeconds),
82
82
  * so every consumer that counts by cluster key pools them under the plain one. */
@@ -86,24 +86,15 @@ const RUN_TIMEOUT_PROGRESSING_KEY = "timed out after <dur> while still making pr
86
86
  // resumes, so its timeout names what actually happens — the commit is kept, the next
87
87
  // attempt re-reviews from scratch. Same cause, same knob (review.timeoutSeconds), one row.
88
88
  const RUN_TIMEOUT_PROGRESSING_REVIEW_KEY = "timed out after <dur> while still making progress — the commit is kept; the next attempt reviews it from scratch";
89
- // The review run's wording when a resumable session remains:
90
- // the next attempt continues this review instead of re-reviewing. Continuing the timed-out
91
- // session is a different cause from a from-scratch re-review, so this wording is its own
92
- // cluster rather than joining the pooled plain run-timeout row. Exported for its unit tests
93
- // (like RUN_TIMEOUT_KEY), so a display or digest test can pin the mapping by name.
94
- export const RUN_TIMEOUT_PROGRESSING_REVIEW_RESUME_KEY = "timed out after <dur> while still making progress — the commit is kept; the next attempt continues this review";
95
- /** The known progressing-timeout wordings and the key each pools under. The two review
96
- * wordings name one cause on one knob (review.timeoutSeconds): a still-making-progress kill
89
+ /** The known progressing-timeout wordings and the key each pools under. The review
90
+ * wording names one cause on one knob (review.timeoutSeconds): a still-making-progress kill
97
91
  * pools into the plain run-timeout row (a mixed team of plain and progressing kills is one
98
92
  * cause's agent-hours, not two half-size rows the top-N cut can drop — the same pooling the
99
- * error-storm reducer applies, src/team/error-storm.ts), and a resumed review's wording maps
100
- * to itself, because continuing the timed-out session is a different cause from a from-scratch
101
- * re-review. Enumerated so a future wording edit lands in
102
- * this table rather than silently changing which row its timeouts count under. */
93
+ * error-storm reducer applies, src/team/team-error-storm.ts). Enumerated so a future wording
94
+ * edit lands in this table rather than silently changing which row its timeouts count under. */
103
95
  const TIMEOUT_POOL = new Map([
104
96
  [RUN_TIMEOUT_PROGRESSING_KEY, RUN_TIMEOUT_KEY],
105
97
  [RUN_TIMEOUT_PROGRESSING_REVIEW_KEY, RUN_TIMEOUT_KEY],
106
- [RUN_TIMEOUT_PROGRESSING_REVIEW_RESUME_KEY, RUN_TIMEOUT_PROGRESSING_REVIEW_RESUME_KEY],
107
98
  ]);
108
99
  /** The cluster key a normalized error clusters under: the pooled wordings above map to their
109
100
  * row, and every other cause stands as normalizeClusterKey rendered it. */
@@ -2,11 +2,11 @@ import { assertReportSinceMs, eventWindowCovers, readWindowEvents, sinceWindowTi
2
2
  import { eventDayKey, eventRole } from "../events/event-read.js";
3
3
  import { baseRoleOf } from "../roles/runner-ids.js";
4
4
  import { scopeBases } from "../report/report-fold.js";
5
- import { timeAndSpend } from "./time-spend.js";
6
- import { dayAt, dayKey, dayWindow, formatDate } from "../text/datetime.js";
5
+ import { timeAndSpend } from "./failure-time-spend.js";
6
+ import { dayAt, dayKey, dayWindow, DAY_MS, formatDate } from "../text/datetime.js";
7
7
  import { describeStateChange, STATE_CHANGE_TOP, STATE_CHANGE_TYPES } from "./failure-state-change.js";
8
8
  import { clusterMessages, errorTextOrPlaceholder, truncateExample } from "./failure-cluster.js";
9
- import { addTo, getOrCreate, memoSetLru, rankByCount } from "../collections.js";
9
+ import { addTo, distinctOrdered, getOrCreate, memoSetLru, rankByCount } from "../collections.js";
10
10
  import { firstReason } from "../text/phrases.js";
11
11
  import { sameStatIdentity, statIdentityOrZero, statOrNull } from "../files/files.js";
12
12
  import { nonNegativeNumber } from "../files/json-object.js";
@@ -100,9 +100,13 @@ function mergeFailureSection(events) {
100
100
  }
101
101
  return section(clusterMessages(messages, MERGE_FAILURE_TOP), total);
102
102
  }
103
+ /** A role with no counted outcomes — the zero baseline roleStats starts a role on (a fresh
104
+ * copy, since roleStats mutates it) and deltaRows hands a role missing from either window.
105
+ * One home for the zero shape so a future RoleStats field is added once. */
106
+ const ZERO_ROLE_STATS = { runs: 0, errors: 0, quietKills: 0, rejections: 0 };
103
107
  function roleStats(events) {
104
108
  const byRole = new Map();
105
- const statsFor = (role) => getOrCreate(byRole, role, () => ({ runs: 0, errors: 0, quietKills: 0, rejections: 0 }));
109
+ const statsFor = (role) => getOrCreate(byRole, role, () => ({ ...ZERO_ROLE_STATS }));
106
110
  for (const ev of events) {
107
111
  // Runs are run_end events; a rejection is now recorded by the merge slot, AFTER the
108
112
  // authoring run has already ended `queued` (plans/merge-queue.md 3/5). review_rejected is
@@ -196,7 +200,7 @@ export function collectFailureReportSince(root, sinceMs, role, untilMs) {
196
200
  prior.push(ev);
197
201
  }
198
202
  return assembleFailureReport(current, prior, raw.events, eventWindowCovers(raw, priorFrom), raw.logState, {
199
- days: Math.ceil(length / 86_400_000),
203
+ days: Math.ceil(length / DAY_MS),
200
204
  from: dayKey(cutoff),
201
205
  to: dayKey(end),
202
206
  ...slice,
@@ -294,11 +298,11 @@ function outcomeRows(runEvents) {
294
298
  function deltaRows(current, prior) {
295
299
  const curStats = roleStats(current);
296
300
  const prevStats = roleStats(prior);
297
- const roles = [...new Set([...curStats.keys(), ...prevStats.keys()])];
301
+ const roles = distinctOrdered([...curStats.keys(), ...prevStats.keys()]);
298
302
  return roles
299
303
  .map((role) => {
300
- const c = curStats.get(role) ?? { runs: 0, errors: 0, quietKills: 0, rejections: 0 };
301
- const p = prevStats.get(role) ?? { runs: 0, errors: 0, quietKills: 0, rejections: 0 };
304
+ const c = curStats.get(role) ?? ZERO_ROLE_STATS;
305
+ const p = prevStats.get(role) ?? ZERO_ROLE_STATS;
302
306
  return {
303
307
  role,
304
308
  prevRuns: p.runs,
@@ -1,6 +1,6 @@
1
1
  import { agree, plural } from "../text/phrases.js";
2
2
  import { shortSha, usd } from "../text/format.js";
3
- import { boundedWindowLine, clockHm, dayKey, dayLabel, monthDay, reportWindow, sinceWindowLine } from "../text/datetime.js";
3
+ import { HOUR_MS, boundedWindowLine, clockHm, dayKey, dayLabel, monthDay, reportWindow, sinceWindowLine } from "../text/datetime.js";
4
4
  import { durationLabel } from "../cli/cli-args.js";
5
5
  import { eventsRotationLabel } from "../events/events.js";
6
6
  import { eventTypeLabel } from "../events/event-labels.js";
@@ -99,7 +99,7 @@ function spendCell(cell) {
99
99
  * from shortSpanPhrase's minutes-or-seconds (an event-feed phrase), because a summed 8-hour
100
100
  * timeout episode must not read as "480m". */
101
101
  function hoursPhrase(ms) {
102
- return `${(ms / 3_600_000).toFixed(1)} h`;
102
+ return `${(ms / HOUR_MS).toFixed(1)} h`;
103
103
  }
104
104
  /** Render the digest as bounded Markdown. Byte bound: for a given team the tables grow only with
105
105
  * the number of configured roles (fixed by config), the RESULT_ORDER vocabulary (fixed by
@@ -209,42 +209,52 @@ function pushTimeAndSpend(lines, data) {
209
209
  lines.push("## Time and spend by outcome");
210
210
  if (data.timeSpend.length === 0) {
211
211
  lines.push("_no finished runs in the window_");
212
+ return;
212
213
  }
213
- else {
214
- lines.push(...markdownTable(["role", "merged", "no change", "error or blocked"], data.timeSpend.map((row) => [
215
- roleCell(row.role),
216
- spendCell(row.classes.merged),
217
- spendCell(row.classes.no_change),
218
- spendCell(row.classes.error),
219
- ])));
220
- lines.push("");
221
- lines.push("**Top loss causes by time:**");
222
- if (data.lossCauses.length === 0) {
223
- lines.push("_no loss to rank_");
224
- }
225
- else {
226
- for (const c of data.lossCauses) {
227
- const cause = c.kind === "no_change"
228
- ? `no change on ${roleCell(c.roles[0] ?? "?")}`
229
- : c.kind === "review-rejected"
230
- ? `review-rejected authoring on ${roleCell(c.roles[0] ?? "?")}${c.example ? ` — ${c.example}` : ""}`
231
- : c.kind === "refused"
232
- ? `refused on ${roleCell(c.roles[0] ?? "?")}${c.example ? ` — ${c.example}` : ""}`
233
- : c.kind === "merge-conflict" || c.kind === "merge-blocked"
234
- ? `${c.kind} authoring on ${roleCell(c.roles[0] ?? "?")}${c.example ? ` — ${c.example}` : ""}`
235
- : c.kind === "cutoff"
236
- ? `context cut-off on ${roleCell(c.roles[0] ?? "?")}`
237
- : `${c.example} (${roleList(c.roles)})`;
238
- lines.push(`- ${hoursPhrase(c.ms)} · ${usd(c.costUsd)} — ${plural(c.runs, "run")}: ${cause}`);
239
- }
240
- // Mark the top-5 cut the way the cluster sections mark theirs: a capped ranking that
241
- // stays silent reads as a complete itemization of the window's loss (BUGS.md 2026-10-01).
242
- if (data.lossCausesHidden > 0) {
243
- const noun = agree(data.lossCausesHidden, "cause", "causes");
244
- lines.push(`_+${data.lossCausesHidden} more loss ${noun} by time not listed_`);
245
- }
246
- }
214
+ pushTimeSpendRows(lines, data);
215
+ pushLossCauses(lines, data);
216
+ }
217
+ /** The role × class table of hours and dollars. */
218
+ function pushTimeSpendRows(lines, data) {
219
+ lines.push(...markdownTable(["role", "merged", "no change", "error or blocked"], data.timeSpend.map((row) => [
220
+ roleCell(row.role),
221
+ spendCell(row.classes.merged),
222
+ spendCell(row.classes.no_change),
223
+ spendCell(row.classes.error),
224
+ ])));
225
+ }
226
+ /** The top loss causes by time, capped at the LOSS_TOP cut; a refused cause's text (and the
227
+ * empty ranking's placeholder) lives in `lossCauseText`/`pushLossCauses` here. */
228
+ function pushLossCauses(lines, data) {
229
+ lines.push("");
230
+ lines.push("**Top loss causes by time:**");
231
+ if (data.lossCauses.length === 0) {
232
+ lines.push("_no loss to rank_");
233
+ return;
234
+ }
235
+ for (const c of data.lossCauses) {
236
+ lines.push(`- ${hoursPhrase(c.ms)} · ${usd(c.costUsd)} — ${plural(c.runs, "run")}: ${lossCauseText(c)}`);
247
237
  }
238
+ // Mark the top-5 cut the way the cluster sections mark theirs: a capped ranking that
239
+ // stays silent reads as a complete itemization of the window's loss (BUGS.md 2026-10-01).
240
+ if (data.lossCausesHidden > 0) {
241
+ const noun = agree(data.lossCausesHidden, "cause", "causes");
242
+ lines.push(`_+${data.lossCausesHidden} more loss ${noun} by time not listed_`);
243
+ }
244
+ }
245
+ /** One loss cause's bullet text. */
246
+ function lossCauseText(c) {
247
+ return c.kind === "no_change"
248
+ ? `no change on ${roleCell(c.roles[0] ?? "?")}`
249
+ : c.kind === "review-rejected"
250
+ ? `review-rejected authoring on ${roleCell(c.roles[0] ?? "?")}${c.example ? ` — ${c.example}` : ""}`
251
+ : c.kind === "refused"
252
+ ? `refused on ${roleCell(c.roles[0] ?? "?")}${c.example ? ` — ${c.example}` : ""}`
253
+ : c.kind === "merge-conflict" || c.kind === "merge-blocked"
254
+ ? `${c.kind} authoring on ${roleCell(c.roles[0] ?? "?")}${c.example ? ` — ${c.example}` : ""}`
255
+ : c.kind === "cutoff"
256
+ ? `context cut-off on ${roleCell(c.roles[0] ?? "?")}`
257
+ : `${c.example} (${roleList(c.roles)})`;
248
258
  }
249
259
  /** Prompt-token telemetry: the per-role median prompt
250
260
  * send and how much of the prompt budget goes out before a run's first edit, plus each