tumwater 0.4.2 → 0.4.3

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 (305) 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} +25 -12
  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 +1 -8
  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 +9 -0
  14. package/dist/src/agent/codex/codex-backend.js +2 -2
  15. package/dist/src/agent/codex/codex-env.js +28 -15
  16. package/dist/src/agent/codex/codex-hook-trust.js +39 -20
  17. package/dist/src/agent/codex/codex-rollout.js +93 -56
  18. package/dist/src/agent/codex/codex-stream.js +1 -1
  19. package/dist/src/agent/env-scrub.js +18 -0
  20. package/dist/src/agent/image-input.js +68 -11
  21. package/dist/src/agent/notes-shim.js +2 -2
  22. package/dist/src/agent/opencode/opencode-backend.js +2 -2
  23. package/dist/src/agent/opencode/opencode-catalog.js +50 -27
  24. package/dist/src/agent/opencode/opencode-config.js +32 -14
  25. package/dist/src/agent/opencode/opencode-env.js +12 -13
  26. package/dist/src/agent/opencode/opencode-stream.js +47 -25
  27. package/dist/src/agent/session-ledger.js +1 -1
  28. package/dist/src/agent/subscription-account.js +3 -3
  29. package/dist/src/backlog/backlog-cli-view.js +81 -41
  30. package/dist/src/backlog/backlog-cli.js +57 -26
  31. package/dist/src/backlog/backlog-eligibility.js +58 -102
  32. package/dist/src/backlog/backlog-md.js +5 -5
  33. package/dist/src/backlog/backlog-parts.js +65 -27
  34. package/dist/src/backlog/backlog-release.js +12 -12
  35. package/dist/src/backlog/backlog-structure.js +114 -61
  36. package/dist/src/backlog/backlog.js +1 -1
  37. package/dist/src/baseline/main-baseline.js +148 -84
  38. package/dist/src/budget/budget-fallback-view.js +100 -243
  39. package/dist/src/budget/budget-wiring.js +29 -65
  40. package/dist/src/budget/fallback-breaker.js +2 -2
  41. package/dist/src/build/build-check-detect.js +43 -14
  42. package/dist/src/build/build-check-events.js +2 -2
  43. package/dist/src/build/build-check-scoped.js +40 -16
  44. package/dist/src/build/build-check.js +13 -5
  45. package/dist/src/cli/cli-command-args.js +89 -52
  46. package/dist/src/cli/cli-flag-specs.js +42 -22
  47. package/dist/src/cli/cli-init.js +34 -12
  48. package/dist/src/cli/cli-query-commands.js +1 -1
  49. package/dist/src/cli/cli-repo-gate.js +2 -2
  50. package/dist/src/cli/cli-start.js +91 -58
  51. package/dist/src/cli/completion.js +82 -46
  52. package/dist/src/cli/config-commands.js +82 -71
  53. package/dist/src/cli/help.js +4 -6
  54. package/dist/src/cli/log-commands.js +43 -23
  55. package/dist/src/cli/notify-command.js +2 -2
  56. package/dist/src/cli/question-commands.js +86 -60
  57. package/dist/src/cli.js +181 -135
  58. package/dist/src/collections.js +31 -16
  59. package/dist/src/concurrency/check-slots.js +70 -0
  60. package/dist/src/concurrency/lock.js +22 -0
  61. package/dist/src/concurrency/run-permit.js +49 -0
  62. package/dist/src/concurrency/semaphore.js +46 -250
  63. package/dist/src/config/config-editable-keys.js +4 -7
  64. package/dist/src/config/config-fallbacks.js +81 -101
  65. package/dist/src/config/config-field-checks.js +66 -37
  66. package/dist/src/config/config-keys.js +5 -20
  67. package/dist/src/config/config-live.js +4 -28
  68. package/dist/src/config/{model-selector.js → config-model-selector.js} +4 -4
  69. package/dist/src/config/config-request.js +153 -102
  70. package/dist/src/config/config-schema.js +9 -11
  71. package/dist/src/config/config-selector-checks.js +1 -1
  72. package/dist/src/config/config-validation-agents.js +2 -2
  73. package/dist/src/config/config-validation-roles.js +4 -52
  74. package/dist/src/config/config-validation.js +4 -38
  75. package/dist/src/config/config-views.js +9 -34
  76. package/dist/src/config/config-write.js +6 -23
  77. package/dist/src/config/config.js +4 -5
  78. package/dist/src/doctor/doctor-backlog-bugs.js +7 -6
  79. package/dist/src/doctor/doctor-backlog-plans.js +24 -24
  80. package/dist/src/doctor/doctor-backlog.js +7 -5
  81. package/dist/src/doctor/doctor-check-names.js +3 -6
  82. package/dist/src/doctor/doctor-checks.js +5 -5
  83. package/dist/src/doctor/doctor-claude-checks.js +44 -28
  84. package/dist/src/doctor/doctor-codex-checks.js +6 -6
  85. package/dist/src/doctor/doctor-contention-checks.js +44 -112
  86. package/dist/src/doctor/doctor-fallback-checks.js +2 -2
  87. package/dist/src/doctor/doctor-holds.js +6 -45
  88. package/dist/src/doctor/doctor-install.js +1 -1
  89. package/dist/src/doctor/doctor-launch-services.js +1 -1
  90. package/dist/src/doctor/doctor-model-checks.js +178 -94
  91. package/dist/src/doctor/doctor-notify.js +2 -2
  92. package/dist/src/doctor/doctor-opencode-checks.js +5 -4
  93. package/dist/src/doctor/doctor-orphans.js +120 -20
  94. package/dist/src/doctor/{outcome.js → doctor-outcome.js} +7 -0
  95. package/dist/src/doctor/doctor-pi-checks.js +2 -2
  96. package/dist/src/doctor/doctor-principles.js +3 -3
  97. package/dist/src/doctor/doctor-project-checks.js +3 -3
  98. package/dist/src/doctor/doctor-role-notes.js +1 -1
  99. package/dist/src/doctor/doctor.js +8 -11
  100. package/dist/src/events/event-format.js +30 -54
  101. package/dist/src/events/event-labels.js +1 -5
  102. package/dist/src/events/event-legacy.js +1 -3
  103. package/dist/src/events/{notify.js → event-notify.js} +2 -2
  104. package/dist/src/events/event-read.js +1 -1
  105. package/dist/src/failure/failure-cluster.js +6 -6
  106. package/dist/src/failure/failure-data.js +8 -4
  107. package/dist/src/failure/failure-render.js +44 -34
  108. package/dist/src/failure/failure-state-change.js +4 -34
  109. package/dist/src/failure/{time-spend.js → failure-time-spend.js} +117 -84
  110. package/dist/src/files/files.js +30 -0
  111. package/dist/src/files/json-object.js +8 -8
  112. package/dist/src/files/stat-cache.js +12 -8
  113. package/dist/src/gates/budget-gates.js +85 -84
  114. package/dist/src/gates/gate-polls.js +27 -68
  115. package/dist/src/gates/pause-gates.js +16 -2
  116. package/dist/src/gates/readiness.js +1 -1
  117. package/dist/src/gates/role-cap-gates.js +44 -35
  118. package/dist/src/gates/{startup-gate.js → startup-gates.js} +7 -7
  119. package/dist/src/gates/{streak-gate.js → streak-gates.js} +75 -52
  120. package/dist/src/git/git.js +75 -45
  121. package/dist/src/git/worktree-pool.js +3 -6
  122. package/dist/src/gui/gui-admin-endpoints.js +3 -3
  123. package/dist/src/gui/gui-backlog-commands.js +4 -4
  124. package/dist/src/gui/gui-backlog-endpoints.js +61 -41
  125. package/dist/src/gui/gui-command.js +43 -24
  126. package/dist/src/gui/gui-history-endpoints.js +100 -52
  127. package/dist/src/gui/gui-operator-commands.js +22 -23
  128. package/dist/src/gui/gui-prompt-commands.js +23 -23
  129. package/dist/src/gui/gui-report-endpoints.js +1 -1
  130. package/dist/src/history/history-data.js +71 -44
  131. package/dist/src/history/history-queued.js +49 -27
  132. package/dist/src/history/history.js +51 -33
  133. package/dist/src/inbox/prompt-commands.js +59 -42
  134. package/dist/src/init/init.js +88 -40
  135. package/dist/src/merge/merge-backlog-conflicts.js +1 -1
  136. package/dist/src/merge/merge-core.js +1 -1
  137. package/dist/src/merge/merge-drain.js +8 -11
  138. package/dist/src/merge/merge-pipeline.js +6 -7
  139. package/dist/src/merge/merge-post-review-drain.js +10 -13
  140. package/dist/src/merge/merge-requeue.js +1 -1
  141. package/dist/src/merge/merge-slot.js +0 -8
  142. package/dist/src/merge/merge-verify.js +8 -18
  143. package/dist/src/notes-cli.js +52 -42
  144. package/dist/src/operator/operator-commands.js +76 -44
  145. package/dist/src/operator/operator-intent.js +3 -3
  146. package/dist/src/orchestrator/orchestrator-launch.js +31 -63
  147. package/dist/src/orchestrator/orchestrator-poll.js +27 -32
  148. package/dist/src/orchestrator/orchestrator.js +8 -21
  149. package/dist/src/orchestrator/scheduling-admit.js +101 -68
  150. package/dist/src/orchestrator/scheduling-scope.js +1 -1
  151. package/dist/src/orchestrator/scheduling-skip.js +1 -1
  152. package/dist/src/orchestrator/startup-migrations.js +1 -1
  153. package/dist/src/paths.js +8 -7
  154. package/dist/src/pi/pi-args.js +5 -2
  155. package/dist/src/pi/pi-backend.js +2 -2
  156. package/dist/src/pi/pi-bin.js +3 -3
  157. package/dist/src/pi/pi-event-line.js +1 -1
  158. package/dist/src/pi/pi-models.js +43 -29
  159. package/dist/src/pi/pi-settings.js +43 -20
  160. package/dist/src/pi/pi-stream.js +32 -6
  161. package/dist/src/pi/pi-watchdogs.js +12 -5
  162. package/dist/src/pi/pi.js +2 -2
  163. package/dist/src/pi-extension/context-shake.js +32 -20
  164. package/dist/src/pi-extension/kill-guard.js +69 -0
  165. package/dist/src/process/process-group.js +49 -5
  166. package/dist/src/process/process.js +8 -8
  167. package/dist/src/process/run-marker.js +31 -13
  168. package/dist/src/process/supervisor.js +40 -28
  169. package/dist/src/prompt/dialect.js +1 -1
  170. package/dist/src/prompt/gate-notes.js +2 -2
  171. package/dist/src/prompt/notes-backlog.js +66 -37
  172. package/dist/src/prompt/principles.js +6 -4
  173. package/dist/src/prompt/prompt-followup.js +12 -4
  174. package/dist/src/prompt/prompt.js +88 -38
  175. package/dist/src/report/report-data.js +53 -37
  176. package/dist/src/report/report-fold.js +16 -9
  177. package/dist/src/report/report-render.js +21 -194
  178. package/dist/src/report/report-since-data.js +4 -3
  179. package/dist/src/report/report-since-render.js +173 -0
  180. package/dist/src/report/report-usage-fold.js +8 -8
  181. package/dist/src/report/report.js +46 -22
  182. package/dist/src/review/post-merge-review-render.js +9 -3
  183. package/dist/src/review/post-merge-review.js +1 -1
  184. package/dist/src/review/review-precheck.js +11 -1
  185. package/dist/src/review/review.js +40 -153
  186. package/dist/src/review/secret-leaks.js +26 -16
  187. package/dist/src/roles/role-catalog.js +11 -3
  188. package/dist/src/roles/role-guidance.js +16 -12
  189. package/dist/src/roles/role-render.js +1 -14
  190. package/dist/src/roles/role-view.js +9 -21
  191. package/dist/src/run/acceptance-greps.js +88 -45
  192. package/dist/src/run/run-apply.js +12 -6
  193. package/dist/src/run/run-detail-data.js +121 -68
  194. package/dist/src/run/run-detail-render.js +92 -35
  195. package/dist/src/run/run-detail.js +72 -41
  196. package/dist/src/run/run-prompt.js +63 -50
  197. package/dist/src/run/run-resume.js +76 -29
  198. package/dist/src/run/run-stage.js +135 -84
  199. package/dist/src/run/run-timing.js +8 -2
  200. package/dist/src/run/run-usage.js +31 -1
  201. package/dist/src/run/run-verdict.js +119 -50
  202. package/dist/src/run/stage-check.js +49 -32
  203. package/dist/src/run/stage-checks-shared.js +28 -0
  204. package/dist/src/run/stage-name-checks.js +111 -92
  205. package/dist/src/run/stage-path-checks.js +69 -42
  206. package/dist/src/run/stage-prose-checks.js +55 -40
  207. package/dist/src/runner/runner-outcome.js +6 -1
  208. package/dist/src/runner/runner-pi.js +3 -10
  209. package/dist/src/runner/runner-recovery.js +1 -1
  210. package/dist/src/runner/{revision.js → runner-revision.js} +1 -1
  211. package/dist/src/runner/runner-run.js +97 -35
  212. package/dist/src/runner/runner-state-load.js +12 -9
  213. package/dist/src/runner/runner.js +41 -25
  214. package/dist/src/scheduling/claims.js +0 -4
  215. package/dist/src/scheduling/scheduling.js +53 -66
  216. package/dist/src/status/status-data.js +2 -2
  217. package/dist/src/status/status-role-notes.js +2 -2
  218. package/dist/src/status/status-roles.js +9 -17
  219. package/dist/src/team/{error-storm.js → team-error-storm.js} +1 -1
  220. package/dist/src/team/{failure-spread.js → team-failure-spread.js} +5 -5
  221. package/dist/src/team/team-hold.js +98 -65
  222. package/dist/src/team/{orchestrator-info.js → team-orchestrator-info.js} +2 -15
  223. package/dist/src/team/team-polls.js +28 -126
  224. package/dist/src/team/{reclaim.js → team-reclaim.js} +61 -31
  225. package/dist/src/team/team-state.js +4 -13
  226. package/dist/src/text/markdown.js +1 -1
  227. package/dist/src/text/phrases.js +20 -15
  228. package/dist/src/text/text.js +3 -3
  229. package/dist/src/ui/alert-feeds.js +49 -73
  230. package/dist/src/ui/badges.js +3 -13
  231. package/dist/src/ui/gui/gui-client-boot.js +2 -2
  232. package/dist/src/ui/gui/gui-client-composer.js +1 -1
  233. package/dist/src/ui/gui/gui-client-drawer-config.js +7 -17
  234. package/dist/src/ui/gui/gui-client-drawer.js +1 -1
  235. package/dist/src/ui/gui/gui-client-history.js +28 -14
  236. package/dist/src/ui/gui/gui-client-model.js +0 -12
  237. package/dist/src/ui/gui/gui-client-pending.js +7 -6
  238. package/dist/src/ui/gui/gui-client-report-chart.js +5 -5
  239. package/dist/src/ui/gui/gui-client-report.js +1 -1
  240. package/dist/src/ui/gui/gui-client-roles.js +0 -1
  241. package/dist/src/ui/gui/gui-client-settings.js +43 -84
  242. package/dist/src/ui/gui/gui-client-team.js +4 -4
  243. package/dist/src/ui/gui/gui-client.js +7 -1
  244. package/dist/src/ui/progress-data.js +53 -40
  245. package/dist/src/ui/run-progress-model.js +9 -2
  246. package/dist/src/ui/status-model.js +2 -24
  247. package/dist/src/ui/status-payload.js +3 -7
  248. package/dist/src/ui/status-render.js +5 -9
  249. package/dist/src/ui/team-alert-backlog.js +6 -6
  250. package/dist/src/ui/team-alert-status.js +9 -170
  251. package/dist/src/ui/team-alert-tail.js +19 -215
  252. package/dist/src/ui/team-alerts.js +7 -8
  253. package/dist/src/ui/transcript-run.js +27 -16
  254. package/dist/src/ui/tui/tui-editor-answer.js +12 -10
  255. package/dist/src/ui/tui/tui-editor-attach.js +4 -3
  256. package/dist/src/ui/tui/tui-editor-backends.js +4 -3
  257. package/dist/src/ui/tui/tui-editor-brief.js +6 -9
  258. package/dist/src/ui/tui/tui-editor-budget.js +4 -3
  259. package/dist/src/ui/tui/tui-editor-config.js +80 -68
  260. package/dist/src/ui/tui/tui-editor-entry.js +12 -20
  261. package/dist/src/ui/tui/tui-editor-pause.js +51 -57
  262. package/dist/src/ui/tui/tui-editor-prompt.js +9 -13
  263. package/dist/src/ui/tui/tui-editor-queued.js +11 -14
  264. package/dist/src/ui/tui/tui-editor-quiet.js +4 -3
  265. package/dist/src/ui/tui/tui-editor-release.js +4 -3
  266. package/dist/src/ui/tui/tui-editor-retire.js +7 -7
  267. package/dist/src/ui/tui/tui-editor-role-enabled.js +4 -3
  268. package/dist/src/ui/tui/tui-editor-role-file.js +4 -4
  269. package/dist/src/ui/tui/tui-editor-role-key.js +4 -3
  270. package/dist/src/ui/tui/tui-editor-role-model.js +97 -15
  271. package/dist/src/ui/tui/tui-editor-search.js +4 -3
  272. package/dist/src/ui/tui/tui-editor-session.js +17 -4
  273. package/dist/src/ui/tui/tui-editors.js +1 -7
  274. package/dist/src/ui/tui/tui-frame.js +182 -134
  275. package/dist/src/ui/tui/tui-guard.js +20 -0
  276. package/dist/src/ui/tui/tui-input.js +159 -92
  277. package/dist/src/ui/tui/tui-keymap.js +51 -29
  278. package/dist/src/ui/tui/tui-keys.js +1 -1
  279. package/dist/src/ui/tui/tui-pane-activity.js +3 -3
  280. package/dist/src/ui/tui/tui-pane-backlog.js +74 -36
  281. package/dist/src/ui/tui/tui-pane-ctx.js +1 -1
  282. package/dist/src/ui/tui/tui-pane-health.js +252 -228
  283. package/dist/src/ui/tui/tui-pane-history.js +61 -70
  284. package/dist/src/ui/tui/tui-pane-pending.js +284 -247
  285. package/dist/src/ui/tui/tui-pane-queued.js +25 -27
  286. package/dist/src/ui/tui/tui-pane-report.js +2 -1
  287. package/dist/src/ui/tui/tui-pane-session.js +11 -7
  288. package/dist/src/ui/tui/tui-pane-transcript.js +19 -31
  289. package/dist/src/ui/tui/tui-pane.js +14 -11
  290. package/dist/src/ui/tui/tui-panes.js +29 -32
  291. package/dist/src/ui/tui/tui.js +3 -4
  292. package/dist/src/verdict/fix-claim.js +76 -54
  293. package/dist/src/verdict/refusal.js +1 -1
  294. package/package.json +5 -5
  295. package/dist/src/concurrency/provider-lease.js +0 -105
  296. package/dist/src/runner/model-fallback.js +0 -76
  297. package/dist/src/runner/runner-fallback.js +0 -66
  298. package/dist/src/ui/tui/tui-editor-claude-reserve.js +0 -24
  299. package/dist/src/ui/tui/tui-editor-provider-caps.js +0 -27
  300. /package/dist/src/backlog/{question-answer.js → backlog-question-answer.js} +0 -0
  301. /package/dist/src/doctor/{probe.js → doctor-probe.js} +0 -0
  302. /package/dist/src/gates/{disk-gate.js → disk-gates.js} +0 -0
  303. /package/dist/src/operator/{retire.js → operator-retire.js} +0 -0
  304. /package/dist/src/operator/{revert.js → operator-revert.js} +0 -0
  305. /package/dist/src/runner/{leftover.js → runner-leftover.js} +0 -0
@@ -17,7 +17,7 @@ import { buildStaleness, isSelfHosted, readBuildInfo, STALE_INPUTS_LABEL } from
17
17
  import { dirEntryNames, statOrNull } from "../files/files.js";
18
18
  import { refSha } from "../git/git.js";
19
19
  import { STATE_DIR, eventsArchivePath, eventsLogPath } from "../paths.js";
20
- import { BYTES_PER_GB, diskVolumePath, sampleFreeBytes } from "../gates/disk-gate.js";
20
+ import { BYTES_PER_GB, diskVolumePath, sampleFreeBytes } from "../gates/disk-gates.js";
21
21
  import { DAY_MS, dayKey } from "../text/datetime.js";
22
22
  import { errorMessage } from "../text/text.js";
23
23
  import { lowerMedian } from "../collections.js";
@@ -33,7 +33,7 @@ import { roleLabel } from "../roles/role-labels.js";
33
33
  import { quarantineWarning } from "../baseline/main-guardrails.js";
34
34
  import { readCiParity, currentCiParityState, parityIsCurrent, ciParityStalled, CI_PARITY_STALE_MS } from "../run/ci-parity.js";
35
35
  import { readQaCoverage, qaCoverageStalled, QA_COVERAGE_STALE_MS } from "../run/qa-coverage.js";
36
- import { failOutcome, okOutcome, warnOutcome } from "./outcome.js";
36
+ import { failOutcome, okOutcome, warnOutcome } from "./doctor-outcome.js";
37
37
  /** .tumwater writable — absent is fine (created on first run); present, prove it by writing
38
38
  * and deleting a temp file inside. The probe leaves the dir's listing unchanged after the run.
39
39
  * The write is the property under test: only a write that fails is "not writable", so the
@@ -251,7 +251,7 @@ export function checkBuildCheck(root, config) {
251
251
  * once, so a routine one-off flake stays quiet. ok/warn only — never a fail — so a flaky suite
252
252
  * cannot fail a scripted doctor pre-flight (the checkStalledEntries precedent; the exit-code
253
253
  * rule is
254
- * outcome.ts's). */
254
+ * doctor-outcome.ts's). */
255
255
  export function checkFlakyChecks(root, now = Date.now()) {
256
256
  const { events } = readEventsSince(root, KNOWN_FLAKE_WINDOW_MS);
257
257
  const flapped = repeatedFlakeCounts(events);
@@ -1,7 +1,7 @@
1
1
  import { readRoleNotesOverCap } from "../status/status-role-notes.js";
2
2
  import { ROLE_NOTES_MAX_BYTES } from "../agent/role-notes-write.js";
3
3
  import { agree, plural } from "../text/phrases.js";
4
- import { okOutcome, warnOutcome } from "./outcome.js";
4
+ import { okOutcome, warnOutcome } from "./doctor-outcome.js";
5
5
  /** How many oversized notes the `role notes` row names before appending "and N more", the
6
6
  * cap the other multi-role observer rows (e.g. workspaces) use so a long tail does not drown
7
7
  * the report. */
@@ -1,23 +1,23 @@
1
1
  import { loadConfigSafe } from "../config/config.js";
2
2
  import { envPath } from "../files/files.js";
3
- import { orchestratorAlive, readOrchestratorInfo } from "../team/orchestrator-info.js";
3
+ import { orchestratorAlive, readOrchestratorInfo } from "../team/team-orchestrator-info.js";
4
4
  import { systemProcessProbe } from "../process/process-table.js";
5
5
  import { checkOrphans } from "./doctor-orphans.js";
6
- import { checkBudget, checkErrorStreaks, checkMainRed, checkMaintenanceQuota, checkPause, checkProviderHolds, checkQuietHours, checkSubscriptionWindow } from "./doctor-holds.js";
6
+ import { checkBudget, checkErrorStreaks, checkMainRed, checkMaintenanceQuota, checkPause, checkProviderHolds, checkQuietHours } from "./doctor-holds.js";
7
7
  import { checkRootInstall } from "./doctor-install.js";
8
8
  import { checkNotify } from "./doctor-notify.js";
9
9
  import { checkRoleNotes } from "./doctor-role-notes.js";
10
10
  import { checkLaunchServicesPorts } from "./doctor-launch-services.js";
11
11
  import { plural } from "../text/phrases.js";
12
12
  import { shortSha } from "../text/format.js";
13
- import { failOutcome } from "./outcome.js";
13
+ import { failOutcome } from "./doctor-outcome.js";
14
14
  import { errorMessage } from "../text/text.js";
15
15
  import { checkBackendBinary, checkBrief, checkBootstrap, checkClaudeTranscripts, checkExecPath, checkFunes, checkGitBinary, checkConfigKeyNames, checkInit, checkOpencodeVersion, checkNodeVersion, checkPiExtensions, checkRepo, } from "./doctor-checks.js";
16
16
  import { checkPrinciples } from "./doctor-principles.js";
17
17
  import { checkBuild, checkBuildCheck, checkCiParity, checkDiskSpace, checkDroppedPostMergeReviews, checkEventLog, checkFlakyChecks, checkMainDiffRules, checkPostMergeChurn, checkQaCoverage, checkStateDir, } from "./doctor-project-checks.js";
18
- import { sampleFreeBytes } from "../gates/disk-gate.js";
19
- import { checkCheckContention, checkLeftoverRoleBranches, checkMergeLock, checkProviderConcurrency, checkProviderContention, checkTumwaterPins, checkWorkInstances, checkWorktreePool, } from "./doctor-contention-checks.js";
20
- import { checkFeedbackFiling, checkTierModels } from "./doctor-model-checks.js";
18
+ import { sampleFreeBytes } from "../gates/disk-gates.js";
19
+ import { checkCheckContention, checkLeftoverRoleBranches, checkMergeLock, checkProviderContention, checkTumwaterPins, checkWorkInstances, checkWorktreePool, } from "./doctor-contention-checks.js";
20
+ import { checkFeedbackFiling, checkImageInput, checkTierModels } from "./doctor-model-checks.js";
21
21
  import { checkFallbackChain, checkFallbackModel } from "./doctor-fallback-checks.js";
22
22
  import { checkAnthroApiKey, checkClaudeAuth, checkClaudeVersion } from "./doctor-claude-checks.js";
23
23
  import { checkOpencode } from "./doctor-opencode-checks.js";
@@ -95,11 +95,8 @@ export async function runDoctor(root, pathEnv = envPath(), probe = systemProcess
95
95
  { name: "pause", run: () => checkPause(root) },
96
96
  { name: "quiet hours", run: () => checkQuietHours(config) },
97
97
  { name: "budget", run: () => checkBudget(root, config, (orchestratorAlive(root, info) && info?.budget) || null) },
98
- { name: "claude window", run: () => checkSubscriptionWindow("claude", root, undefined, (orchestratorAlive(root, info) && info?.chain) || undefined, config) },
99
- { name: "codex window", run: () => checkSubscriptionWindow("codex", root, undefined, (orchestratorAlive(root, info) && info?.chain) || undefined, config) },
100
98
  { name: "provider waits", run: () => checkProviderHolds(root) },
101
99
  { name: "maintenance allowance", run: () => checkMaintenanceQuota(root, config, orchestratorAlive(root, info)) },
102
- { name: "provider concurrency", run: () => checkProviderConcurrency(config, root) },
103
100
  { name: "provider contention", run: () => checkProviderContention(root, config) },
104
101
  { name: "check contention", run: () => checkCheckContention(root, config) },
105
102
  { name: "repeated failures", run: () => checkErrorStreaks(root, config) },
@@ -107,6 +104,7 @@ export async function runDoctor(root, pathEnv = envPath(), probe = systemProcess
107
104
  { name: "over-cap model", run: () => checkFallbackModel(root, undefined, (orchestratorAlive(root, info) && info?.fallbackDemoted) || null) },
108
105
  { name: "fallback chain", run: () => checkFallbackChain(root) },
109
106
  { name: "models for each tier", run: () => checkTierModels(root) },
107
+ { name: "image input", run: () => checkImageInput(root) },
110
108
  { name: "claude auth", run: () => checkClaudeAuth(root, pathEnv) },
111
109
  { name: "claude api key", run: () => checkAnthroApiKey(root, undefined, pathEnv) },
112
110
  { name: "claude version", run: () => checkClaudeVersion(root, pathEnv) },
@@ -127,8 +125,7 @@ export async function runDoctor(root, pathEnv = envPath(), probe = systemProcess
127
125
  // The receive-side guards name what a running team skips, so an operator tool meant for the
128
126
  // team (a pi package, an extension file, an mcp.json server, or funes) does not vanish
129
127
  // silently. Each check returns null to omit its row: the pi row for a team with no pi seam,
130
- // the funes row when funes is not on PATH, and the claude/codex window rows when that backend
131
- // has no subscription account.
128
+ // the funes row when funes is not on PATH.
132
129
  { name: "pi extensions", run: () => checkPiExtensions(config, undefined, pathEnv) },
133
130
  { name: "funes", run: () => checkFunes(config, pathEnv) },
134
131
  { name: "state dir", run: () => checkStateDir(root) },
@@ -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";
@@ -111,9 +110,16 @@ function providerScope(e) {
111
110
  export function backendLabel(e) {
112
111
  return typeof e.backend === "string" && e.backend !== "" ? e.backend : "claude";
113
112
  }
113
+ /** A subscription-window event's window, as "<backend> subscription window <type>" — the head
114
+ * every `subscription_window_*` line names (windowType is a torn/older event's only nullable
115
+ * half, so `reset` and the shaped lines differ only in what follows this prefix). One home so
116
+ * the four lines cannot drift on the backend's label or the window-type word. */
117
+ function subscriptionWindowPhrase(e) {
118
+ return `${backendLabel(e)} subscription window ${textOr(e.windowType)}`;
119
+ }
114
120
  /** The provider/model pair a fallback-ish line names, as "provider/model", defaulting each
115
121
  * 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. */
122
+ * line so the pair's shape and defaults cannot drift. */
117
123
  function providerModelPair(e, providerDefault, modelDefault) {
118
124
  return `${textOr(e.provider, providerDefault)}/${textOr(e.model, modelDefault)}`;
119
125
  }
@@ -213,9 +219,11 @@ function runEventMessage(e) {
213
219
  ? "resuming the run cut off at the context ceiling (compacted pi session, same workspace)"
214
220
  : e.cause === "handoff"
215
221
  ? "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);
222
+ : e.cause === "image-reroute"
223
+ ? "handing the blind-read run off to a fresh session on an image-capable model (same workspace, uncommitted edits kept)"
224
+ : e.cause === "salvaged"
225
+ ? "handing the work an earlier run left unsaved to a fresh session"
226
+ : "resuming the run a shutdown interrupted (same agent session and workspace)") + restored);
219
227
  }
220
228
  case "wake":
221
229
  return `woke (${textOr(e.reason)})`;
@@ -430,13 +438,6 @@ function budgetEventMessage(e) {
430
438
  });
431
439
  return `fallback: ${parts.join("; ")}`;
432
440
  }
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
441
  case "opencode_free_model_billed":
441
442
  // The promotion ended (plans/opencode-backend.md §7, part 6c/7): a run billed what the
442
443
  // catalog called free, so the team now prices that model as paid from here on. No warning
@@ -531,19 +532,6 @@ function resourceEventMessage(e) {
531
532
  return `backend recovered (${backendKindPhrase(e.kind)})${providerAt(e)} — roles on ${providerScope(e)} run again`;
532
533
  return `provider limit lifted${providerAt(e)} — roles on ${providerScope(e)} run again`;
533
534
  }
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
535
  case "subscription_window_limited":
548
536
  // Routine-with-explanation, like rate_limit_hold — no warning prefix: waiting is the
549
537
  // harness handling the account's own throttle. Names the window, the reset, and which
@@ -554,9 +542,9 @@ function resourceEventMessage(e) {
554
542
  {
555
543
  const moved = fallbackTierEntries(isJsonObject(e.tiers) ? e.tiers : undefined);
556
544
  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)`;
545
+ return `${subscriptionWindowPhrase(e)} rejected — ${backendLabel(e)} model jobs move to ${moved.join(", ")}${windowResetPhrase(e)} (director keeps waiting)`;
558
546
  }
559
- return `waiting: ${backendLabel(e)} subscription window ${textOr(e.windowType)} rejected — ${backendLabel(e)} model jobs wait${windowResetPhrase(e)} (director keeps running)`;
547
+ return `waiting: ${subscriptionWindowPhrase(e)} rejected — ${backendLabel(e)} model jobs wait${windowResetPhrase(e)} (director keeps running)`;
560
548
  case "subscription_window_reset":
561
549
  return `${backendLabel(e)} subscription window reset — ${backendLabel(e)} model jobs run again`;
562
550
  default: return textOr(e.type);
@@ -568,10 +556,6 @@ function configEventMessage(e) {
568
556
  // Routine state change, like counters_reset — no warning prefix.
569
557
  return `maxConcurrent changed: ${textOr(e.from)} → ${textOr(e.to)}`;
570
558
  }
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
559
  case "retention_changed": {
576
560
  // Routine state change, like its maxConcurrent sibling — no warning prefix.
577
561
  return `sessionRetentionDays changed: ${textOr(e.from)} → ${textOr(e.to)}`;
@@ -604,9 +588,9 @@ function configEventMessage(e) {
604
588
  function imageRestartEventMessage(e) {
605
589
  switch (e.type) {
606
590
  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.
591
+ // Routine-with-explanation — no warning prefix: the run was stopped (or a gate run
592
+ // noted) because the model cannot see the image. Names the file, the model, and
593
+ // whether the run failed on it or a gate run only logged it.
610
594
  const action = e.action === "logged"
611
595
  ? " (gate run logged)"
612
596
  : e.action === "fail"
@@ -614,6 +598,13 @@ function imageRestartEventMessage(e) {
614
598
  : "";
615
599
  return `blind image read: ${textOr(e.path)} on ${textOr(e.model, "the model")}${action}`;
616
600
  }
601
+ case "image_reroute":
602
+ // The run's work moves to a model that can read the image (plans/image-input.md part
603
+ // 4/5). Routine-with-explanation — no warning prefix: names the hop the way the
604
+ // handoff reads, and whether the traits-of-attachment preflight avoided a failed run.
605
+ return e.preflight === true
606
+ ? `image attachment routed to ${textOr(e.to, "the target")} (${textOr(e.tier, "a peer tier")}) to read its image — no failed run`
607
+ : `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
608
  case "build_stale":
618
609
  // Self-hosting teams only (src/redeploy/redeploy.ts): the code main describes is not the
619
610
  // code running. Not a warning prefix — a stale build is a state, and auto-restart resolves
@@ -669,22 +660,11 @@ function checkWaitEventMessage(e) {
669
660
  return `waited ${shortSpanPhrase(finiteNumber(e.waitedMs, 0))} to start (${textOr(e.role)} ${purpose}; ${textOr(e.slots)} workspaces, ${textOr(e.pinned)} pinned)`;
670
661
  }
671
662
  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.
663
+ // Routine-with-explanation, like slot_wait: waiting for the team slot is the cap
664
+ // working as designed, so no warning prefix. Names the wait, so the operator can size
665
+ // maxConcurrent with the number in hand.
678
666
  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)})`;
667
+ return `waited ${span} for a run slot (${textOr(e.role)})`;
688
668
  }
689
669
  case "check_wait":
690
670
  // Routine-with-explanation, like slot_wait: waiting for a check permit is the cap working
@@ -754,8 +734,6 @@ export function eventMessage(e) {
754
734
  case "budget_resumed":
755
735
  case "budget_handback":
756
736
  case "fallback_switched":
757
- case "model_fallback_started":
758
- case "model_fallback_ended":
759
737
  case "opencode_free_model_billed": return budgetEventMessage(e);
760
738
  case "team_paused":
761
739
  case "team_resumed":
@@ -775,12 +753,9 @@ export function eventMessage(e) {
775
753
  case "quiet_hours_ended":
776
754
  case "rate_limit_hold":
777
755
  case "rate_limit_resumed":
778
- case "subscription_window_warning":
779
- case "subscription_window_reserved":
780
756
  case "subscription_window_limited":
781
757
  case "subscription_window_reset": return resourceEventMessage(e);
782
758
  case "max_concurrent_changed":
783
- case "provider_concurrency_changed":
784
759
  case "retention_changed":
785
760
  case "config_changed":
786
761
  case "model_changed":
@@ -789,6 +764,7 @@ export function eventMessage(e) {
789
764
  case "orchestrator_start":
790
765
  case "orchestrator_stop": return configEventMessage(e);
791
766
  case "image_blind":
767
+ case "image_reroute":
792
768
  case "build_stale":
793
769
  case "restart_pending":
794
770
  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;
@@ -10,8 +10,8 @@ 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
17
  * orchestrator is gone, so src/cli/cli-start.ts pages it directly with notifyTeamDown; events
@@ -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
@@ -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. */
@@ -96,7 +96,7 @@ export const RUN_TIMEOUT_PROGRESSING_REVIEW_RESUME_KEY = "timed out after <dur>
96
96
  * wordings name one cause on one knob (review.timeoutSeconds): a still-making-progress kill
97
97
  * pools into the plain run-timeout row (a mixed team of plain and progressing kills is one
98
98
  * 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
99
+ * error-storm reducer applies, src/team/team-error-storm.ts), and a resumed review's wording maps
100
100
  * to itself, because continuing the timed-out session is a different cause from a from-scratch
101
101
  * re-review. Enumerated so a future wording edit lands in
102
102
  * this table rather than silently changing which row its timeouts count under. */
@@ -2,7 +2,7 @@ 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";
5
+ import { timeAndSpend } from "./failure-time-spend.js";
6
6
  import { dayAt, dayKey, dayWindow, 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";
@@ -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
@@ -297,8 +301,8 @@ function deltaRows(current, prior) {
297
301
  const roles = [...new Set([...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,
@@ -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