tumwater 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (446) hide show
  1. package/README.md +27 -13
  2. package/dist/build-info.json +2 -2
  3. package/dist/src/agent/agent-args.js +8 -9
  4. package/dist/src/agent/agent-backend.js +8 -6
  5. package/dist/src/agent/backend-traits.js +42 -25
  6. package/dist/src/agent/claude/claude-account.js +71 -204
  7. package/dist/src/agent/claude/claude-args.js +9 -6
  8. package/dist/src/agent/claude/claude-backend.js +10 -10
  9. package/dist/src/agent/claude/claude-env.js +1 -1
  10. package/dist/src/agent/claude/claude-stream.js +4 -3
  11. package/dist/src/agent/claude/claude-write-guard.js +49 -0
  12. package/dist/src/agent/claude/write-guard-hook.js +13 -0
  13. package/dist/src/agent/codex/codex-account.js +119 -0
  14. package/dist/src/agent/codex/codex-args.js +181 -0
  15. package/dist/src/agent/codex/codex-backend.js +279 -0
  16. package/dist/src/agent/codex/codex-catalog.js +97 -0
  17. package/dist/src/agent/codex/codex-env.js +18 -0
  18. package/dist/src/agent/codex/codex-hook-trust.js +246 -0
  19. package/dist/src/agent/codex/codex-patch-paths.js +36 -0
  20. package/dist/src/agent/codex/codex-rollout.js +179 -0
  21. package/dist/src/agent/codex/codex-stream.js +402 -0
  22. package/dist/src/agent/codex/codex-write-guard.js +86 -0
  23. package/dist/src/agent/codex/write-guard-hook.js +12 -0
  24. package/dist/src/agent/image-input.js +29 -24
  25. package/dist/src/agent/notes-shim.js +11 -12
  26. package/dist/src/agent/opencode/opencode-args.js +0 -1
  27. package/dist/src/agent/opencode/opencode-backend.js +12 -9
  28. package/dist/src/agent/opencode/opencode-catalog.js +55 -29
  29. package/dist/src/agent/opencode/opencode-config.js +19 -14
  30. package/dist/src/agent/opencode/opencode-env.js +0 -1
  31. package/dist/src/agent/opencode/opencode-session-id.js +1 -2
  32. package/dist/src/agent/opencode/opencode-stream.js +1 -1
  33. package/dist/src/agent/opencode/write-guard-plugin/index.js +48 -0
  34. package/dist/src/agent/role-notes-write.js +5 -6
  35. package/dist/src/agent/run-agent.js +465 -448
  36. package/dist/src/agent/run-settle.js +214 -0
  37. package/dist/src/agent/session-ledger.js +86 -16
  38. package/dist/src/agent/subscription-account.js +220 -0
  39. package/dist/src/agent/write-guard.js +148 -0
  40. package/dist/src/backlog/backlog-archive.js +246 -55
  41. package/dist/src/backlog/backlog-cli-view.js +280 -0
  42. package/dist/src/backlog/backlog-cli.js +85 -274
  43. package/dist/src/backlog/backlog-commit.js +4 -5
  44. package/dist/src/backlog/backlog-eligibility.js +175 -242
  45. package/dist/src/backlog/backlog-index.js +149 -0
  46. package/dist/src/backlog/backlog-md.js +54 -6
  47. package/dist/src/backlog/backlog-parts.js +254 -0
  48. package/dist/src/backlog/backlog-release.js +117 -0
  49. package/dist/src/backlog/backlog-render.js +27 -0
  50. package/dist/src/backlog/backlog-structure.js +99 -156
  51. package/dist/src/backlog/backlog-write.js +40 -127
  52. package/dist/src/backlog/backlog.js +21 -6
  53. package/dist/src/backlog/question-answer.js +21 -15
  54. package/dist/src/baseline/main-baseline.js +89 -20
  55. package/dist/src/baseline/main-culprit.js +94 -0
  56. package/dist/src/baseline/main-guardrails.js +209 -0
  57. package/dist/src/baseline/main-red.js +139 -32
  58. package/dist/src/baseline/main-revert.js +133 -0
  59. package/dist/src/baseline/main-watch.js +184 -0
  60. package/dist/src/brief/brief-edit.js +9 -11
  61. package/dist/src/brief/brief.js +31 -51
  62. package/dist/src/budget/budget.js +30 -20
  63. package/dist/src/budget/fallback-breaker.js +26 -27
  64. package/dist/src/build/build-check-detect.js +23 -15
  65. package/dist/src/build/build-check-events.js +25 -6
  66. package/dist/src/build/build-check-report.js +49 -9
  67. package/dist/src/build/build-check-scoped.js +18 -11
  68. package/dist/src/build/build-check.js +2 -1
  69. package/dist/src/build/build-info.js +2 -3
  70. package/dist/src/build/build-stage.js +9 -14
  71. package/dist/src/build/dep-install.js +1 -2
  72. package/dist/src/change/change-data.js +123 -31
  73. package/dist/src/change/change-render.js +46 -5
  74. package/dist/src/cli/brief-command.js +3 -4
  75. package/dist/src/cli/cli-args.js +118 -33
  76. package/dist/src/cli/cli-command-args.js +46 -18
  77. package/dist/src/cli/cli-editor.js +2 -1
  78. package/dist/src/cli/cli-flag-specs.js +150 -45
  79. package/dist/src/cli/cli-init.js +51 -0
  80. package/dist/src/cli/cli-marker-commands.js +4 -4
  81. package/dist/src/cli/cli-query-commands.js +308 -590
  82. package/dist/src/cli/cli-repo-gate.js +11 -1
  83. package/dist/src/cli/cli-role-command.js +420 -0
  84. package/dist/src/cli/cli-start-summary.js +160 -0
  85. package/dist/src/cli/{cli-run.js → cli-start.js} +165 -237
  86. package/dist/src/cli/completion.js +149 -22
  87. package/dist/src/cli/config-commands.js +41 -18
  88. package/dist/src/cli/follow-view.js +75 -0
  89. package/dist/src/cli/help.js +189 -73
  90. package/dist/src/cli/log-commands.js +148 -82
  91. package/dist/src/cli/question-commands.js +205 -10
  92. package/dist/src/cli.js +106 -43
  93. package/dist/src/collections.js +26 -10
  94. package/dist/src/concurrency/check-permit.js +20 -3
  95. package/dist/src/concurrency/lock.js +17 -13
  96. package/dist/src/concurrency/provider-lease.js +44 -8
  97. package/dist/src/concurrency/semaphore.js +102 -26
  98. package/dist/src/config/config-aliases.js +0 -1
  99. package/dist/src/config/config-editable-keys.js +13 -1
  100. package/dist/src/config/config-fallbacks.js +93 -56
  101. package/dist/src/config/config-field-checks.js +40 -10
  102. package/dist/src/config/config-keys.js +30 -8
  103. package/dist/src/config/config-live.js +63 -24
  104. package/dist/src/config/config-request.js +137 -0
  105. package/dist/src/config/config-schema.js +31 -24
  106. package/dist/src/config/config-selector-checks.js +123 -0
  107. package/dist/src/config/config-validation-agents.js +80 -0
  108. package/dist/src/config/config-validation-model.js +116 -0
  109. package/dist/src/config/config-validation.js +141 -199
  110. package/dist/src/config/config-views.js +127 -53
  111. package/dist/src/config/config-write-core.js +135 -0
  112. package/dist/src/config/config-write.js +131 -264
  113. package/dist/src/config/config.js +48 -38
  114. package/dist/src/config/model-selector.js +34 -34
  115. package/dist/src/doctor/doctor-backlog-bugs.js +287 -0
  116. package/dist/src/doctor/doctor-backlog-plans.js +263 -0
  117. package/dist/src/doctor/doctor-backlog.js +100 -496
  118. package/dist/src/doctor/doctor-check-names.js +81 -0
  119. package/dist/src/doctor/doctor-checks.js +52 -579
  120. package/dist/src/doctor/doctor-claude-checks.js +145 -0
  121. package/dist/src/doctor/doctor-codex-checks.js +279 -0
  122. package/dist/src/doctor/doctor-contention-checks.js +380 -0
  123. package/dist/src/doctor/doctor-holds.js +82 -66
  124. package/dist/src/doctor/doctor-install.js +2 -1
  125. package/dist/src/doctor/doctor-model-checks.js +236 -402
  126. package/dist/src/doctor/doctor-notify.js +3 -4
  127. package/dist/src/doctor/doctor-opencode-checks.js +162 -0
  128. package/dist/src/doctor/doctor-orphans.js +182 -63
  129. package/dist/src/doctor/doctor-project-checks.js +303 -0
  130. package/dist/src/doctor/doctor.js +121 -77
  131. package/dist/src/doctor/image-probe.js +21 -8
  132. package/dist/src/doctor/outcome.js +3 -2
  133. package/dist/src/doctor/probe.js +7 -11
  134. package/dist/src/events/event-format.js +181 -86
  135. package/dist/src/events/event-labels.js +12 -5
  136. package/dist/src/events/event-legacy.js +39 -15
  137. package/dist/src/events/event-read.js +39 -39
  138. package/dist/src/events/event-tally.js +16 -0
  139. package/dist/src/events/event-window.js +60 -24
  140. package/dist/src/events/events.js +30 -13
  141. package/dist/src/events/notify.js +7 -7
  142. package/dist/src/failure/failure-cluster.js +12 -12
  143. package/dist/src/failure/failure-data.js +56 -56
  144. package/dist/src/failure/failure-render.js +31 -23
  145. package/dist/src/failure/failure-state-change.js +17 -12
  146. package/dist/src/failure/time-spend.js +99 -35
  147. package/dist/src/files/files.js +111 -35
  148. package/dist/src/files/json-files.js +3 -3
  149. package/dist/src/files/json-object.js +28 -7
  150. package/dist/src/files/stat-cache.js +2 -2
  151. package/dist/src/files/tail.js +1 -1
  152. package/dist/src/gates/bootstrap-gates.js +4 -5
  153. package/dist/src/gates/budget-gates.js +177 -84
  154. package/dist/src/gates/disk-gate.js +1 -1
  155. package/dist/src/gates/gate-polls.js +108 -58
  156. package/dist/src/gates/maintenance-quota.js +10 -13
  157. package/dist/src/gates/pause-gates.js +28 -10
  158. package/dist/src/gates/readiness.js +7 -3
  159. package/dist/src/gates/role-cap-gates.js +8 -8
  160. package/dist/src/gates/startup-gate.js +94 -32
  161. package/dist/src/gates/streak-gate.js +54 -30
  162. package/dist/src/git/commit-message.js +77 -46
  163. package/dist/src/git/git-diff.js +16 -10
  164. package/dist/src/git/git-run.js +79 -38
  165. package/dist/src/git/git.js +36 -34
  166. package/dist/src/git/slot-salvage.js +253 -0
  167. package/dist/src/git/slots-state.js +6 -6
  168. package/dist/src/git/worktree-pool.js +132 -38
  169. package/dist/src/git/worktree-use.js +4 -11
  170. package/dist/src/git/worktree.js +94 -37
  171. package/dist/src/gui/gui-admin-endpoints.js +96 -0
  172. package/dist/src/gui/gui-args.js +79 -8
  173. package/dist/src/gui/gui-backlog-commands.js +42 -18
  174. package/dist/src/gui/gui-backlog-endpoints.js +155 -0
  175. package/dist/src/gui/gui-command.js +8 -13
  176. package/dist/src/gui/gui-config-commands.js +94 -72
  177. package/dist/src/gui/gui-endpoint-commands.js +4 -3
  178. package/dist/src/gui/gui-endpoint-common.js +4 -4
  179. package/dist/src/gui/gui-endpoints.js +11 -380
  180. package/dist/src/gui/gui-history-endpoints.js +301 -0
  181. package/dist/src/gui/gui-operator-commands.js +108 -13
  182. package/dist/src/gui/gui-prompt-commands.js +11 -14
  183. package/dist/src/gui/gui-report-endpoints.js +90 -0
  184. package/dist/src/gui/gui-server.js +11 -4
  185. package/dist/src/gui/gui-team-commands.js +7 -7
  186. package/dist/src/gui/http-body.js +1 -1
  187. package/dist/src/history/history-data.js +122 -234
  188. package/dist/src/history/history-queued.js +147 -0
  189. package/dist/src/history/history-render.js +42 -22
  190. package/dist/src/history/history.js +193 -49
  191. package/dist/src/inbox/inbox-attachments.js +29 -30
  192. package/dist/src/inbox/inbox-cancel.js +10 -11
  193. package/dist/src/inbox/inbox-edit.js +9 -9
  194. package/dist/src/inbox/inbox-submit.js +18 -18
  195. package/dist/src/inbox/inbox-top.js +25 -9
  196. package/dist/src/inbox/inbox.js +42 -43
  197. package/dist/src/inbox/pending-prompt.js +26 -26
  198. package/dist/src/inbox/prompt-commands.js +62 -49
  199. package/dist/src/inbox/prompt-not-before.js +2 -2
  200. package/dist/src/init/init-templates.js +1 -2
  201. package/dist/src/init/init.js +28 -23
  202. package/dist/src/merge/backlog-conflicts.js +215 -35
  203. package/dist/src/merge/merge-batch.js +14 -8
  204. package/dist/src/merge/merge-cancel.js +53 -0
  205. package/dist/src/merge/merge-check-failures.js +44 -22
  206. package/dist/src/merge/merge-core.js +81 -32
  207. package/dist/src/merge/merge-diff.js +5 -4
  208. package/dist/src/merge/merge-drain.js +150 -11
  209. package/dist/src/merge/merge-git.js +177 -38
  210. package/dist/src/merge/merge-pipeline.js +15 -3
  211. package/dist/src/merge/merge-post-review.js +90 -0
  212. package/dist/src/merge/merge-queue-render.js +49 -11
  213. package/dist/src/merge/merge-queue.js +31 -15
  214. package/dist/src/merge/merge-requeue.js +102 -0
  215. package/dist/src/merge/merge-slot.js +61 -16
  216. package/dist/src/merge/merge-stack.js +33 -11
  217. package/dist/src/merge/merge-to-main.js +86 -27
  218. package/dist/src/merge/merge-top.js +69 -0
  219. package/dist/src/merge/merge-verify.js +12 -10
  220. package/dist/src/notes-cli.js +30 -15
  221. package/dist/src/operator/operator-commands.js +192 -81
  222. package/dist/src/operator/operator-intent.js +69 -42
  223. package/dist/src/operator/operator-requests.js +35 -40
  224. package/dist/src/operator/retire.js +5 -6
  225. package/dist/src/operator/revert.js +142 -34
  226. package/dist/src/orchestrator/orchestrator-launch.js +60 -52
  227. package/dist/src/orchestrator/orchestrator-poll.js +320 -0
  228. package/dist/src/orchestrator/orchestrator-scheduling.js +68 -369
  229. package/dist/src/orchestrator/orchestrator.js +128 -369
  230. package/dist/src/orchestrator/retention.js +42 -8
  231. package/dist/src/orchestrator/scheduling-admit.js +116 -0
  232. package/dist/src/orchestrator/scheduling-scope.js +201 -0
  233. package/dist/src/orchestrator/scheduling-skip.js +185 -0
  234. package/dist/src/orchestrator/startup-migration-steps.js +8 -7
  235. package/dist/src/orchestrator/startup-migrations.js +11 -7
  236. package/dist/src/paths.js +116 -49
  237. package/dist/src/pi/command-shape.js +12 -1
  238. package/dist/src/pi/pi-args.js +6 -4
  239. package/dist/src/pi/pi-backend.js +6 -6
  240. package/dist/src/pi/pi-bin.js +54 -63
  241. package/dist/src/pi/pi-listed-models.js +53 -0
  242. package/dist/src/pi/pi-models.js +63 -30
  243. package/dist/src/pi/pi-run-result.js +1 -1
  244. package/dist/src/pi/pi-settings.js +1 -1
  245. package/dist/src/pi/pi-stream.js +6 -6
  246. package/dist/src/pi/pi-watchdogs.js +18 -19
  247. package/dist/src/pi-extension/bounded-output.js +5 -3
  248. package/dist/src/pi-extension/context-budget.js +2 -2
  249. package/dist/src/pi-extension/context-shake.js +1 -1
  250. package/dist/src/pi-extension/context-window.js +0 -1
  251. package/dist/src/pi-extension/full-output.js +3 -10
  252. package/dist/src/pi-extension/strip-thinking.js +0 -1
  253. package/dist/src/pi-extension/trim-system-prompt.js +3 -4
  254. package/dist/src/pi-extension/write-guard.js +37 -0
  255. package/dist/src/process/launch-services.js +1 -2
  256. package/dist/src/process/node-bin.js +16 -0
  257. package/dist/src/process/process-group.js +8 -18
  258. package/dist/src/process/process.js +41 -2
  259. package/dist/src/process/run-marker.js +18 -6
  260. package/dist/src/process/supervisor.js +22 -21
  261. package/dist/src/prompt/dialect.js +17 -2
  262. package/dist/src/prompt/gate-notes.js +203 -0
  263. package/dist/src/prompt/gate-prompts.js +121 -211
  264. package/dist/src/prompt/notes-backlog.js +181 -0
  265. package/dist/src/prompt/principles.js +1 -2
  266. package/dist/src/prompt/prompt-followup.js +19 -14
  267. package/dist/src/prompt/prompt.js +129 -204
  268. package/dist/src/redeploy/redeploy-policy.js +24 -19
  269. package/dist/src/redeploy/redeploy-probes.js +1 -1
  270. package/dist/src/redeploy/redeploy.js +2 -3
  271. package/dist/src/redeploy/redeployer.js +20 -20
  272. package/dist/src/redeploy/self-reload.js +2 -1
  273. package/dist/src/report/report-data.js +161 -77
  274. package/dist/src/report/report-fold.js +129 -52
  275. package/dist/src/report/report-main-red.js +55 -0
  276. package/dist/src/report/report-render.js +133 -64
  277. package/dist/src/report/report-types.js +1 -0
  278. package/dist/src/report/report.js +26 -9
  279. package/dist/src/request-timeouts.js +2 -2
  280. package/dist/src/review/known-flakes.js +100 -1
  281. package/dist/src/review/post-merge-review.js +106 -0
  282. package/dist/src/review/protected-edits.js +4 -5
  283. package/dist/src/review/review-followup.js +8 -8
  284. package/dist/src/review/review-precheck.js +66 -23
  285. package/dist/src/review/review-run.js +395 -0
  286. package/dist/src/review/review-verdict.js +2 -2
  287. package/dist/src/review/review.js +155 -360
  288. package/dist/src/review/secret-leaks.js +36 -16
  289. package/dist/src/review/suite-rerun.js +0 -1
  290. package/dist/src/roles/legacy-role-migration.js +80 -7
  291. package/dist/src/roles/role-catalog.js +69 -12
  292. package/dist/src/roles/role-guidance.js +55 -20
  293. package/dist/src/roles/role-labels.js +1 -2
  294. package/dist/src/roles/role-render.js +67 -50
  295. package/dist/src/roles/role-view.js +59 -47
  296. package/dist/src/roles/roles.js +31 -31
  297. package/dist/src/roles/runner-ids.js +97 -0
  298. package/dist/src/run/acceptance-greps.js +119 -0
  299. package/dist/src/run/feedback-filings.js +51 -0
  300. package/dist/src/{tick → run}/qa-coverage.js +9 -27
  301. package/dist/src/{tick/tick-apply.js → run/run-apply.js} +95 -94
  302. package/dist/src/run/run-detail-data.js +179 -0
  303. package/dist/src/run/run-detail-render.js +98 -0
  304. package/dist/src/run/run-detail.js +168 -0
  305. package/dist/src/{tick/tick-finalize.js → run/run-finalize.js} +72 -60
  306. package/dist/src/{tick/tick-outcome.js → run/run-outcome.js} +6 -7
  307. package/dist/src/{tick/tick-prompt.js → run/run-prompt.js} +108 -73
  308. package/dist/src/{tick/tick-resume.js → run/run-resume.js} +55 -26
  309. package/dist/src/{tick/tick-stage.js → run/run-stage.js} +108 -50
  310. package/dist/src/{tick/tick-timing.js → run/run-timing.js} +43 -41
  311. package/dist/src/{tick/tick-usage.js → run/run-usage.js} +45 -37
  312. package/dist/src/{tick/tick-verdict.js → run/run-verdict.js} +91 -56
  313. package/dist/src/run/selfcheck-digest.js +40 -0
  314. package/dist/src/run/stage-backlog-checks.js +249 -0
  315. package/dist/src/run/stage-check.js +172 -0
  316. package/dist/src/run/stage-path-checks.js +730 -0
  317. package/dist/src/{loop → runner}/leftover.js +80 -25
  318. package/dist/src/{loop → runner}/model-fallback.js +12 -12
  319. package/dist/src/runner/revision.js +202 -0
  320. package/dist/src/{loop/loop-fallback.js → runner/runner-fallback.js} +8 -8
  321. package/dist/src/runner/runner-outcome.js +107 -0
  322. package/dist/src/{loop/loop-pi.js → runner/runner-pi.js} +140 -131
  323. package/dist/src/{loop/loop-recovery.js → runner/runner-recovery.js} +21 -15
  324. package/dist/src/{loop/loop-tick.js → runner/runner-run.js} +218 -240
  325. package/dist/src/{loop/loop-state.js → runner/runner-state-load.js} +61 -114
  326. package/dist/src/runner/runner-state.js +102 -0
  327. package/dist/src/{loop/loop.js → runner/runner.js} +123 -120
  328. package/dist/src/scheduling/backoff.js +38 -38
  329. package/dist/src/scheduling/claims.js +35 -22
  330. package/dist/src/scheduling/once-round.js +22 -22
  331. package/dist/src/scheduling/quiet-hours.js +5 -6
  332. package/dist/src/scheduling/scheduling.js +74 -64
  333. package/dist/src/scheduling/work-merged-cache.js +2 -2
  334. package/dist/src/status/status-budget.js +55 -47
  335. package/dist/src/status/status-data.js +43 -16
  336. package/dist/src/status/status-inbox.js +2 -2
  337. package/dist/src/status/status-install.js +0 -1
  338. package/dist/src/status/status-polls.js +28 -28
  339. package/dist/src/status/status-roles.js +39 -13
  340. package/dist/src/team/error-storm.js +8 -8
  341. package/dist/src/team/failure-spread.js +2 -2
  342. package/dist/src/team/orchestrator-info.js +15 -9
  343. package/dist/src/team/reclaim.js +5 -6
  344. package/dist/src/team/team-hold.js +7 -7
  345. package/dist/src/team/team-polls.js +91 -50
  346. package/dist/src/team/team-state.js +8 -2
  347. package/dist/src/text/datetime.js +63 -17
  348. package/dist/src/text/format.js +48 -11
  349. package/dist/src/text/markdown.js +8 -5
  350. package/dist/src/text/phrases.js +57 -23
  351. package/dist/src/text/suggest.js +1 -1
  352. package/dist/src/text/text.js +67 -13
  353. package/dist/src/ui/badges.js +65 -37
  354. package/dist/src/ui/gui/gui-client-backlog.js +292 -0
  355. package/dist/src/ui/gui/gui-client-boot.js +32 -26
  356. package/dist/src/ui/gui/gui-client-composer.js +8 -9
  357. package/dist/src/ui/gui/gui-client-drawer-config.js +212 -0
  358. package/dist/src/ui/gui/gui-client-drawer.js +126 -285
  359. package/dist/src/ui/gui/gui-client-feed.js +271 -0
  360. package/dist/src/ui/gui/gui-client-history.js +434 -67
  361. package/dist/src/ui/gui/gui-client-labels.js +1 -2
  362. package/dist/src/ui/gui/gui-client-markdown.js +1 -2
  363. package/dist/src/ui/gui/gui-client-model.js +27 -36
  364. package/dist/src/ui/gui/gui-client-operator.js +55 -51
  365. package/dist/src/ui/gui/gui-client-pending.js +227 -23
  366. package/dist/src/ui/gui/gui-client-report-chart.js +464 -0
  367. package/dist/src/ui/gui/gui-client-report.js +87 -427
  368. package/dist/src/ui/gui/{gui-client-loops.js → gui-client-roles.js} +43 -44
  369. package/dist/src/ui/gui/gui-client-settings.js +56 -4
  370. package/dist/src/ui/gui/gui-client-team.js +25 -465
  371. package/dist/src/ui/gui/gui-client.js +27 -25
  372. package/dist/src/ui/gui/gui-icons.js +0 -2
  373. package/dist/src/ui/gui/gui-page.js +9 -7
  374. package/dist/src/ui/gui/gui-styles.js +34 -34
  375. package/dist/src/ui/progress-data.js +9 -11
  376. package/dist/src/ui/{tick-progress-model.js → run-progress-model.js} +31 -31
  377. package/dist/src/ui/status-model.js +133 -125
  378. package/dist/src/ui/status-payload.js +136 -49
  379. package/dist/src/ui/status-render.js +101 -82
  380. package/dist/src/ui/team-alerts.js +248 -57
  381. package/dist/src/ui/tone.js +5 -6
  382. package/dist/src/ui/transcript-run.js +58 -0
  383. package/dist/src/ui/transcript-tail.js +28 -20
  384. package/dist/src/ui/transcript.js +63 -23
  385. package/dist/src/ui/tui/tui-app.js +0 -1
  386. package/dist/src/ui/tui/tui-backlog.js +51 -14
  387. package/dist/src/ui/tui/tui-editor-answer.js +27 -31
  388. package/dist/src/ui/tui/tui-editor-attach.js +3 -5
  389. package/dist/src/ui/tui/tui-editor-backends.js +132 -0
  390. package/dist/src/ui/tui/tui-editor-brief.js +10 -12
  391. package/dist/src/ui/tui/tui-editor-budget.js +35 -31
  392. package/dist/src/ui/tui/tui-editor-claude-reserve.js +24 -0
  393. package/dist/src/ui/tui/tui-editor-concurrency.js +24 -0
  394. package/dist/src/ui/tui/tui-editor-config.js +86 -0
  395. package/dist/src/ui/tui/tui-editor-entry.js +36 -47
  396. package/dist/src/ui/tui/tui-editor-fallback.js +31 -0
  397. package/dist/src/ui/tui/tui-editor-instances.js +37 -0
  398. package/dist/src/ui/tui/tui-editor-instructions.js +136 -0
  399. package/dist/src/ui/tui/tui-editor-interval.js +11 -9
  400. package/dist/src/ui/tui/tui-editor-model.js +29 -0
  401. package/dist/src/ui/tui/tui-editor-notes.js +3 -5
  402. package/dist/src/ui/tui/tui-editor-notify.js +25 -0
  403. package/dist/src/ui/tui/tui-editor-pause.js +48 -59
  404. package/dist/src/ui/tui/tui-editor-prompt.js +11 -16
  405. package/dist/src/ui/tui/tui-editor-provider-caps.js +27 -0
  406. package/dist/src/ui/tui/tui-editor-queued.js +5 -9
  407. package/dist/src/ui/tui/tui-editor-quiet.js +99 -35
  408. package/dist/src/ui/tui/tui-editor-retire.js +4 -6
  409. package/dist/src/ui/tui/tui-editor-role-key.js +76 -0
  410. package/dist/src/ui/tui/tui-editor-role-model.js +28 -0
  411. package/dist/src/ui/tui/tui-editor-search.js +45 -33
  412. package/dist/src/ui/tui/tui-editor-session.js +60 -34
  413. package/dist/src/ui/tui/tui-editor-slots.js +23 -0
  414. package/dist/src/ui/tui/tui-editors.js +35 -2
  415. package/dist/src/ui/tui/tui-frame.js +56 -11
  416. package/dist/src/ui/tui/tui-history.js +8 -9
  417. package/dist/src/ui/tui/tui-input.js +13 -11
  418. package/dist/src/ui/tui/tui-keymap.js +0 -1
  419. package/dist/src/ui/tui/tui-keys.js +24 -9
  420. package/dist/src/ui/tui/tui-pane-activity.js +14 -11
  421. package/dist/src/ui/tui/tui-pane-backlog.js +171 -126
  422. package/dist/src/ui/tui/tui-pane-brief.js +0 -1
  423. package/dist/src/ui/tui/tui-pane-ctx.js +2 -2
  424. package/dist/src/ui/tui/tui-pane-health.js +58 -8
  425. package/dist/src/ui/tui/tui-pane-history.js +402 -82
  426. package/dist/src/ui/tui/tui-pane-pending.js +308 -13
  427. package/dist/src/ui/tui/tui-pane-queued.js +71 -55
  428. package/dist/src/ui/tui/tui-pane-report.js +46 -14
  429. package/dist/src/ui/tui/tui-pane-session.js +25 -6
  430. package/dist/src/ui/tui/tui-pane-transcript.js +111 -37
  431. package/dist/src/ui/tui/tui-pane.js +80 -23
  432. package/dist/src/ui/tui/tui-panes.js +100 -30
  433. package/dist/src/ui/tui/tui.js +14 -14
  434. package/dist/src/verdict/fix-claim.js +82 -32
  435. package/dist/src/verdict/no-change.js +4 -4
  436. package/dist/src/verdict/refusal.js +40 -16
  437. package/dist/src/verdict/reply-contract.js +42 -31
  438. package/dist/src/version.js +0 -1
  439. package/package.json +2 -1
  440. package/dist/src/loop/revision.js +0 -70
  441. package/dist/src/roles/loop-ids.js +0 -77
  442. package/dist/src/tick/stage-check.js +0 -419
  443. package/dist/src/tick/telemetry-digest.js +0 -25
  444. package/dist/src/tick/tick-detail-data.js +0 -108
  445. package/dist/src/tick/tick-detail.js +0 -163
  446. package/dist/src/ui/gui/gui-client-sound.js +0 -53
@@ -1,12 +1,13 @@
1
1
  import { addTo, rankCountEntries, sortByValueDesc } from "../collections.js";
2
2
  import { compactTokens, usd } from "../text/format.js";
3
3
  import { durationLabel } from "../cli/cli-args.js";
4
- import { monthDay, reportWindow, sinceWindowLine } from "../text/datetime.js";
4
+ import { boundedWindowLine, monthDay, reportWindow, sinceWindowLine } from "../text/datetime.js";
5
5
  import { eventsRotationLabel } from "../events/events.js";
6
6
  import { SPARSE_WINDOW_NOTE } from "../events/event-window.js";
7
7
  import { shortSpanPhrase } from "../text/phrases.js";
8
8
  import { markdownTable } from "../text/markdown.js";
9
- import { baseRoleOf } from "../roles/loop-ids.js";
9
+ import { baseRoleOf } from "../roles/runner-ids.js";
10
+ import { backendDisplayName } from "../agent/backend-traits.js";
10
11
  /** Bar width for one day: up to 20 blocks scaled to the window's max tokensOut —
11
12
  * round(20·v/max), min 1 when v > 0. */
12
13
  function barWidth(v, max) {
@@ -37,18 +38,37 @@ function rankedRoleTotals(series, pick) {
37
38
  function rankedRoleLine(label, pairs, fmt) {
38
39
  return `**${label}:** ${pairs.length === 0 ? "-" : pairs.map(([r, n]) => `${r} — ${fmt(n)}`).join(" · ")}`;
39
40
  }
40
- /** The per-day-median line of the team day report — "MMM D <span>" cells for each day that
41
+ /** Push one "**Label:** …" ranked-role line, dropping roles with a non-positive value (zero
42
+ * count/spend) before rendering, so an all-zero window still renders "-" exactly like the
43
+ * unfiltered lines. fmt formats each role's number (default String for count lines; pass
44
+ * usd/compactTokens for spend/token lines). All the "X by role" lines share this filter-rule
45
+ * home so the zero-means-render-dash guard cannot drift between the --since and day renders. */
46
+ function pushRankedRoleLine(lines, label, pairs, fmt = String) {
47
+ lines.push(rankedRoleLine(label, pairs.filter(([, n]) => n > 0), fmt));
48
+ }
49
+ /** The per-day cameo line of the team day report — "MMM D <cell>" cells for each day that
41
50
  * carries a value, joined under a bold title, or undefined when no day has one (zero means
42
- * absent). The one home of that cameo rendering, shared by the merge check-time and merge-wait
43
- * medians so their date+span cells and title line cannot drift; the planned provider-slot wait
44
- * median is a third caller of the same rule. */
45
- function medianLine(series, title, pick) {
51
+ * absent). The one home of that cameo rendering, shared by the per-day medians, the red-check
52
+ * count and the main-red wall-clock lines so their date+cell shape and title line cannot drift.
53
+ * Each caller supplies its own cell formatter (shortSpanPhrase for the medians, `×n` for the
54
+ * red-check count, durationLabel for the main-red span). */
55
+ function dayCameoLine(series, title, pick, fmt = shortSpanPhrase) {
46
56
  const cells = series.flatMap((d) => {
47
57
  const v = pick(d);
48
- return v === undefined ? [] : [`${monthDay(d.date)} ${shortSpanPhrase(v)}`];
58
+ return v === undefined ? [] : [`${monthDay(d.date)} ${fmt(v)}`];
49
59
  });
50
60
  return cells.length > 0 ? `**${title}:** ${cells.join(" · ")}` : undefined;
51
61
  }
62
+ /** Push a day-cameo line onto `lines` only when some day carries a value — the “compute a
63
+ * cameo line, push it when present” boilerplate every per-day cell repeats. Renders via
64
+ * dayCameoLine (the one cameo shape), so the date+cell shape and title line cannot drift.
65
+ * Zero-means-absent: a day whose field is undefined renders no cell, and a series with no such
66
+ * day pushes no line. */
67
+ function pushDayCameoLine(lines, series, title, pick, fmt = shortSpanPhrase) {
68
+ const line = dayCameoLine(series, title, pick, fmt);
69
+ if (line !== undefined)
70
+ lines.push(line);
71
+ }
52
72
  /** Push the `Cost by provider` and `Estimated by provider` lines of the per-provider spend
53
73
  * breakdown (plans: per-provider spend), from the caller's already-ranked real- and notional-
54
74
  * spend pairs. Role scoping is the caller's `role === undefined` gate — this helper only
@@ -80,13 +100,38 @@ function mergeShareLine(t) {
80
100
  const failed = (t.mergeFailed ?? 0) > 0 ? ` (${t.mergeFailed} failed)` : "";
81
101
  return `of which merge runs: ${t.mergeRuns} runs${failed} · ${compactTokens(t.mergeTokens)} tokens · ${usd(t.mergeCostUsd)} (reviewer + conflict resolution)`;
82
102
  }
83
- /** The notional-spend line (plans/agent-backends.md part 6a): the API-equivalent cost of
84
- * claude.ai subscription runs folded into the window. Never part of the totals' costUsd,
85
- * because a subscription run is exempt from the daily dollar cap; the caller prints it only
86
- * when a subscription run actually folded in (zero means absent, like the merge line), so
87
- * an API-only team's report is unchanged. */
88
- function notionalLine(t) {
89
- return `claude subscription, API-equivalent: ${usd(t.notionalCostUsd ?? 0)} estimated`;
103
+ /** A float-robust "the per-backend notional sums account for the window's total" check: the two
104
+ * numbers fold in different orders (per-event vs per-backend-then-summed), so their float traces
105
+ * need not coincide exactly even when every subscription event is attributed — the gap lands
106
+ * around machine epsilon. A small relative slack absorbs that reordering noise, while a genuinely
107
+ * partial map (an unattributed subscription merge, or a legacy event with no agent) is off by a
108
+ * real, much larger amount and must fall back to the generic line. */
109
+ function notionalSumsCoverTotal(summed, total) {
110
+ return Math.abs(summed - total) <= 1e-9 * Math.max(1, Math.abs(total));
111
+ }
112
+ /** The notional-spend line(s) (plans/agent-backends.md part 6a, codex-backend.md part 3d/9):
113
+ * the API-equivalent cost of subscription runs folded into the window. Never part of the
114
+ * totals' costUsd, because a subscription run is exempt from the daily dollar cap; the caller
115
+ * prints it only when a subscription run actually folded in (zero means absent, like the merge
116
+ * line), so an API-only team's report is unchanged. When the window's notional spend fully
117
+ * decomposes into backends, one labeled line names each backend — including a single non-claude
118
+ * backend (a codex-only window), so a codex team's line never reads "claude". A claude-only
119
+ * account, an old-log window, or unattributed merge spend keeps one generic lowercase "claude
120
+ * subscription" line — byte-identical to the pre-codex render. */
121
+ function notionalLines(t) {
122
+ const total = t.notionalCostUsd ?? 0;
123
+ const byBackend = t.notionalByBackend ?? {};
124
+ const backends = Object.entries(byBackend).filter(([, c]) => c > 0);
125
+ const summed = backends.reduce((n, [, c]) => n + c, 0);
126
+ // A single fully-attributed claude account collapses to the generic line (its true or
127
+ // fallback backend is claude, and the capitalization keeps old/byte-identical output);
128
+ // every other fully-attributed window — multi-backend, or a single codex/other backend —
129
+ // names each backend on its own line.
130
+ const singleClaude = backends.length === 1 && backends[0]?.[0] === "claude";
131
+ if (notionalSumsCoverTotal(summed, total) && !singleClaude) {
132
+ return backends.map(([backend, c]) => `${backendDisplayName(backend)} subscription, API-equivalent: ${usd(c)} estimated`);
133
+ }
134
+ return [`claude subscription, API-equivalent: ${usd(total)} estimated`];
90
135
  }
91
136
  /** The scope note a `--role` report prints: merge runs belong to the merge slot and the
92
137
  * day-granular backlog tallies (features done / bugs fixed / plans added / bugs filed) come
@@ -104,7 +149,7 @@ function pushReportSummary(lines, t, tail = "", scoped = false) {
104
149
  if (t.mergeRuns > 0)
105
150
  lines.push(mergeShareLine(t));
106
151
  if ((t.notionalCostUsd ?? 0) > 0)
107
- lines.push(notionalLine(t));
152
+ lines.push(...notionalLines(t));
108
153
  if (scoped)
109
154
  lines.push(ROLE_SCOPE_NOTE);
110
155
  }
@@ -116,7 +161,9 @@ export function renderSinceReportMarkdown(data) {
116
161
  const lines = [];
117
162
  lines.push("# tumwater usage report");
118
163
  lines.push("");
119
- lines.push(sinceWindowLine(durationLabel(data.sinceMs), data.fromIso));
164
+ lines.push(data.untilMs !== undefined && data.toIso !== undefined
165
+ ? boundedWindowLine(durationLabel(data.sinceMs), durationLabel(data.untilMs), data.fromIso, data.toIso)
166
+ : sinceWindowLine(durationLabel(data.sinceMs), data.fromIso));
120
167
  if (data.role !== undefined)
121
168
  lines.push(`role: ${data.role}`);
122
169
  lines.push("");
@@ -131,10 +178,10 @@ export function renderSinceReportMarkdown(data) {
131
178
  // omitted — an all-zero window renders "-" like the other lines. Output tokens are the
132
179
  // resource the budget charges, so this is the line an operator reads to find which role's
133
180
  // prompt loop burned the window's tokens.
134
- lines.push(rankedRoleLine("Tokens by role", rankedRoleMap(data.tokensByRole).filter(([, n]) => n > 0), compactTokens));
181
+ pushRankedRoleLine(lines, "Tokens by role", rankedRoleMap(data.tokensByRole), compactTokens);
135
182
  // Same breakdown for spend: ranked by spend desc then name asc, zero-spend roles omitted —
136
- // an all-zero window renders "-" like the ticks line.
137
- lines.push(rankedRoleLine("Cost by role", rankedRoleMap(data.costByRole).filter(([, c]) => c > 0), usd));
183
+ // an all-zero window renders "-" like the runs line.
184
+ pushRankedRoleLine(lines, "Cost by role", rankedRoleMap(data.costByRole), usd);
138
185
  // The same breakdown per PROVIDER (plans: per-provider spend), gated to the team report
139
186
  // like its day sibling. SinceReport.costByProvider is always present (empty when none), so
140
187
  // the zero-means-absent gate prints no line for a window with no provider spend.
@@ -150,19 +197,25 @@ export function renderSinceReportMarkdown(data) {
150
197
  if (modelPairs.length > 0)
151
198
  lines.push(rankedRoleLine("Runs by model", modelPairs, String));
152
199
  }
200
+ // Main's red wall-clock over the window (main's own, not any role's — a role-scoped
201
+ // collector carries 0, so this never prints for a scoped window). Zero-absent: no line for
202
+ // a window main was never red.
203
+ if (data.mainRedMs > 0)
204
+ lines.push(`**Main red:** ${durationLabel(data.mainRedMs)}`);
153
205
  // The same breakdown for review friction: rejections per role from review_rejected events;
154
206
  // a window with none renders "-" like the other lines.
155
- lines.push(rankedRoleLine("Rejections by role", rankedRoleMap(data.rejectionsByRole).filter(([, r]) => r > 0), String));
156
- lines.push(rankedRoleLine("Revisions by role", rankedRoleMap(data.revisionsByRole).filter(([, r]) => r > 0), String));
157
- lines.push(rankedRoleLine("Refusals by role", rankedRoleMap(data.refusalsByRole).filter(([, r]) => r > 0), String));
207
+ pushRankedRoleLine(lines, "Rejections by role", rankedRoleMap(data.rejectionsByRole));
208
+ pushRankedRoleLine(lines, "Revisions by role", rankedRoleMap(data.revisionsByRole));
209
+ pushRankedRoleLine(lines, "Refusals by role", rankedRoleMap(data.refusalsByRole));
210
+ pushRankedRoleLine(lines, "Reverts by role", rankedRoleMap(data.revertsByRole));
158
211
  // The same breakdown for outright failures: error-class run_ends per role under the
159
- // digest's OUTCOME_CLASS — the ticks that burned agent time and merged nothing without
212
+ // digest's OUTCOME_CLASS — the runs that burned agent time and merged nothing without
160
213
  // going through review, which rejections/revisions would miss entirely. A window with
161
214
  // none renders "-" like the other lines.
162
- lines.push(rankedRoleLine("Failed runs by role", rankedRoleMap(data.failedRunsByRole).filter(([, n]) => n > 0), String));
163
- lines.push(rankedRoleLine("Fallbacks by role", rankedRoleMap(data.fallbacksByRole).filter(([, n]) => n > 0), String));
164
- lines.push(rankedRoleLine("Image reroutes by role", rankedRoleMap(data.imageReroutesByRole).filter(([, n]) => n > 0), String));
165
- // Deferred maintenance ticks per reason code, the --since twin of the day render's line:
215
+ pushRankedRoleLine(lines, "Failed runs by role", rankedRoleMap(data.failedRunsByRole));
216
+ pushRankedRoleLine(lines, "Fallbacks by role", rankedRoleMap(data.fallbacksByRole));
217
+ pushRankedRoleLine(lines, "Image reroutes by role", rankedRoleMap(data.imageReroutesByRole));
218
+ // Deferred maintenance runs per reason code, the --since twin of the day render's line:
166
219
  // how many due maintenance runs stood down and why (backlog / no-work / plans-full).
167
220
  // Gated to the team report — a reason is not a role, so a role-scoped window omits it.
168
221
  // Zero-means-absent: a window with no deferral prints no line.
@@ -195,7 +248,7 @@ export function renderSinceReportMarkdown(data) {
195
248
  // Subscription spend is notional: its own line, only when the window has any — the same
196
249
  // conditional/filtered `usd` form the day render's "Estimated by role" uses.
197
250
  if (data.notionalByRole !== undefined) {
198
- lines.push(rankedRoleLine("Estimated by role", rankedRoleMap(data.notionalByRole).filter(([, c]) => c > 0), usd));
251
+ pushRankedRoleLine(lines, "Estimated by role", rankedRoleMap(data.notionalByRole), usd);
199
252
  }
200
253
  lines.push("");
201
254
  // Exactly one final line, chosen by the collector's coverage proof: either a hedged note
@@ -211,12 +264,12 @@ export function renderSinceReportMarkdown(data) {
211
264
  lines.push("backlog tallies (features done / bugs fixed / plans added / bugs filed) need the day report (--days)");
212
265
  return lines.join("\n");
213
266
  }
214
- /** Cost per merged commit by base role. Spend comes from costByRole (keyed by loop id, folded to
267
+ /** Cost per merged commit by base role. Spend comes from costByRole (keyed by role id, folded to
215
268
  * its base role so `feature-2` joins `feature`), commits from commitsByRole (already base-role
216
269
  * keyed). `merged` is [role, cost per commit, commits] for roles with both, ranked by cost per
217
270
  * commit desc then name asc; `idle` is roles that spent and merged nothing, ranked by spend.
218
271
  * Merge spend (the reviewer's) and notional subscription spend stay out by default, matching
219
- * `**Cost by role:**` (`pick` selects another per-loop-id spend map, e.g. notionalByRole); a
272
+ * `**Cost by role:**` (`pick` selects another per-role-id spend map, e.g. notionalByRole); a
220
273
  * role with commits but no spend in the picked map appears in neither list. */
221
274
  function costPerCommitPairs(series, pick = (d) => d.costByRole) {
222
275
  const cost = {};
@@ -273,22 +326,22 @@ export function renderReportMarkdown(data) {
273
326
  lines.push("");
274
327
  const maxTokens = data.series.reduce((m, d) => Math.max(m, d.tokensOut), 0);
275
328
  lines.push(...markdownTable(["day", "tokens out", "runs", "commits", "cost"], data.series.map((d) => {
276
- const ticks = Object.values(d.runsByRole).reduce((a, b) => a + b, 0);
329
+ const runs = Object.values(d.runsByRole).reduce((a, b) => a + b, 0);
277
330
  const w = maxTokens > 0 ? barWidth(d.tokensOut, maxTokens) : 0;
278
331
  const bar = w > 0 ? ` ${"█".repeat(w)}` : ""; // Zero days carry no bar (and no stray space).
279
- return [monthDay(d.date), `${compactTokens(d.tokensOut)}${bar}`, String(ticks), String(d.commits), usd(d.costUsd)];
332
+ return [monthDay(d.date), `${compactTokens(d.tokensOut)}${bar}`, String(runs), String(d.commits), usd(d.costUsd)];
280
333
  }), ["left", "right", "right", "right", "right"]));
281
334
  lines.push("");
282
335
  lines.push(rankedRoleLine("Runs by role", rankedRoleTotals(data.series, (d) => d.runsByRole), String));
283
336
  // The same breakdown for output tokens (the resource the budget charges): window totals per
284
- // role from tokensByRole, ranked like the ticks line. Zero-token roles are omitted; an
285
- // all-zero window renders "-" like the ticks line.
286
- lines.push(rankedRoleLine("Tokens by role", rankedRoleTotals(data.series, (d) => d.tokensByRole ?? {}).filter(([, n]) => n > 0), compactTokens));
337
+ // role from tokensByRole, ranked like the runs line. Zero-token roles are omitted; an
338
+ // all-zero window renders "-" like the runs line.
339
+ pushRankedRoleLine(lines, "Tokens by role", rankedRoleTotals(data.series, (d) => d.tokensByRole ?? {}), compactTokens);
287
340
  // The same breakdown for spend: window totals per role from costByRole, ranked by spend
288
341
  // desc then name asc — spend is what the operator acts on (an operator tuning per-role
289
- // intervals wants the burning loop first, not the busiest one). Zero-spend roles are
290
- // omitted; an all-zero window renders "-" like the ticks line.
291
- lines.push(rankedRoleLine("Cost by role", rankedRoleTotals(data.series, (d) => d.costByRole).filter(([, c]) => c > 0), usd));
342
+ // intervals wants the burning role first, not the busiest one). Zero-spend roles are
343
+ // omitted; an all-zero window renders "-" like the runs line.
344
+ pushRankedRoleLine(lines, "Cost by role", rankedRoleTotals(data.series, (d) => d.costByRole), usd);
292
345
  // The same breakdown per PROVIDER (plans: per-provider spend): which seam of a mixed team
293
346
  // spent what, straight from the events' providerCost maps (run_end's own runs and the
294
347
  // merge's reviewer seam). Gated to the team report — a provider is not a role, so a
@@ -308,18 +361,19 @@ export function renderReportMarkdown(data) {
308
361
  lines.push(rankedRoleLine("Runs by model", modelPairs, String));
309
362
  }
310
363
  // The same breakdown for review friction: window rejections per role from review_rejected
311
- // events, ranked like the ticks line; a window with none renders "-".
312
- lines.push(rankedRoleLine("Rejections by role", rankedRoleTotals(data.series, (d) => d.rejectionsByRole ?? {}).filter(([, r]) => r > 0), String));
313
- lines.push(rankedRoleLine("Revisions by role", rankedRoleTotals(data.series, (d) => d.revisionsByRole ?? {}).filter(([, r]) => r > 0), String));
314
- lines.push(rankedRoleLine("Refusals by role", rankedRoleTotals(data.series, (d) => d.refusalsByRole ?? {}).filter(([, r]) => r > 0), String));
364
+ // events, ranked like the runs line; a window with none renders "-".
365
+ pushRankedRoleLine(lines, "Rejections by role", rankedRoleTotals(data.series, (d) => d.rejectionsByRole ?? {}));
366
+ pushRankedRoleLine(lines, "Revisions by role", rankedRoleTotals(data.series, (d) => d.revisionsByRole ?? {}));
367
+ pushRankedRoleLine(lines, "Refusals by role", rankedRoleTotals(data.series, (d) => d.refusalsByRole ?? {}));
368
+ pushRankedRoleLine(lines, "Reverts by role", rankedRoleTotals(data.series, (d) => d.revertsByRole ?? {}));
315
369
  // The same breakdown for outright failures: error-class run_ends per role under the
316
- // digest's OUTCOME_CLASS — ticks that burned agent time and merged nothing without going
370
+ // digest's OUTCOME_CLASS — runs that burned agent time and merged nothing without going
317
371
  // through review, which rejections/revisions would miss entirely. Zero-means-absent, so a
318
372
  // failure-free window renders no line and an all-zero window renders "-" like the others.
319
- lines.push(rankedRoleLine("Failed runs by role", rankedRoleTotals(data.series, (d) => d.failedRunsByRole ?? {}).filter(([, n]) => n > 0), String));
320
- lines.push(rankedRoleLine("Fallbacks by role", rankedRoleTotals(data.series, (d) => d.fallbacksByRole ?? {}).filter(([, n]) => n > 0), String));
321
- lines.push(rankedRoleLine("Image reroutes by role", rankedRoleTotals(data.series, (d) => d.imageReroutesByRole ?? {}).filter(([, n]) => n > 0), String));
322
- // Deferred maintenance ticks per reason code — how many due maintenance runs stood down
373
+ pushRankedRoleLine(lines, "Failed runs by role", rankedRoleTotals(data.series, (d) => d.failedRunsByRole ?? {}));
374
+ pushRankedRoleLine(lines, "Fallbacks by role", rankedRoleTotals(data.series, (d) => d.fallbacksByRole ?? {}));
375
+ pushRankedRoleLine(lines, "Image reroutes by role", rankedRoleTotals(data.series, (d) => d.imageReroutesByRole ?? {}));
376
+ // Deferred maintenance runs per reason code — how many due maintenance runs stood down
323
377
  // and why, so an idle day reads as backlog-open / no-work / plans-full rather than blank,
324
378
  // the per-role breakdown of the deferred maintenances. Gated to the team report — a
325
379
  // reason is not a role (ROLE_SCOPE_NOTE covers the omission). Zero-means-absent: a window
@@ -352,29 +406,32 @@ export function renderReportMarkdown(data) {
352
406
  lines.push(rankedRoleLine("Merge failures by reason", mergeFailureReasons, String));
353
407
  // Per-day median passing check time, so a suite slowing over days shows; days without a
354
408
  // check are left out, and no line prints when no day has one (zero means absent).
355
- const medians = medianLine(data.series, "Merge check median", (d) => d.checkMedianMs);
356
- if (medians !== undefined)
357
- lines.push(medians);
409
+ pushDayCameoLine(lines, data.series, "Merge check median", (d) => d.checkMedianMs);
358
410
  // Per-day count of red full-suite checks, beside the passing medians above; a day whose
359
411
  // suite went red before passing shows here. Zero-absent: no line when no day fails.
360
- const reds = data.series.flatMap((d) => d.checkFailures === undefined ? [] : [`${monthDay(d.date)} ×${d.checkFailures}`]);
361
- if (reds.length > 0)
362
- lines.push(`**Red merge checks:** ${reds.join(" · ")}`);
363
- const waits = medianLine(data.series, "Merge wait median", (d) => d.mergeWaitMedianMs);
364
- if (waits !== undefined)
365
- lines.push(waits);
366
- const providerWaits = medianLine(data.series, "Provider wait median", (d) => d.providerWaitMedianMs);
367
- if (providerWaits !== undefined)
368
- lines.push(providerWaits);
369
- const slotWaits = medianLine(data.series, "Workspace wait median", (d) => d.slotWaitMedianMs);
370
- if (slotWaits !== undefined)
371
- lines.push(slotWaits);
412
+ pushDayCameoLine(lines, data.series, "Red merge checks", (d) => d.checkFailures, (n) => `×${n}`);
413
+ // Per-day main red wall-clock, beside the red-check counts above: how long each day main
414
+ // had no passing full check (the span every code role was held). Team-only (main's own
415
+ // red time, not any role's) and zero-absent: no line when no day was red.
416
+ pushDayCameoLine(lines, data.series, "Main red", (d) => d.mainRedMs, durationLabel);
417
+ pushDayCameoLine(lines, data.series, "Merge wait median", (d) => d.mergeWaitMedianMs);
418
+ // How long the merge slot itself held each change it landed, beside the merge-wait median:
419
+ // the queue backlog (merge wait minus slot time) apart from the slot's own review/check/
420
+ // rebase/merge work. Team-only (the slot is the team's), omitted when no day carries one.
421
+ pushDayCameoLine(lines, data.series, "Merge slot median", (d) => d.mergeSlotMedianMs);
422
+ pushDayCameoLine(lines, data.series, "Run duration median", (d) => d.runMedianMs);
423
+ // How long the review gate ran per day — how long the reviewer's verdict run(s) took —
424
+ // beside the run/merge-wait medians; pushDayCameoLine omits the line when no day carries one.
425
+ pushDayCameoLine(lines, data.series, "Review duration median", (d) => d.reviewMedianMs);
426
+ pushDayCameoLine(lines, data.series, "Provider wait median", (d) => d.providerWaitMedianMs);
427
+ pushDayCameoLine(lines, data.series, "Workspace wait median", (d) => d.slotWaitMedianMs);
428
+ pushDayCameoLine(lines, data.series, "Check wait median", (d) => d.checkWaitMedianMs);
372
429
  }
373
430
  lines.push(costPerCommitLine("Cost per commit", costPerCommitPairs(data.series)));
374
431
  // Subscription spend is notional: its own two lines, only when the window has any.
375
432
  if ((t.notionalCostUsd ?? 0) > 0) {
376
433
  const notionalOf = (d) => d.notionalByRole ?? {};
377
- lines.push(rankedRoleLine("Estimated by role", rankedRoleTotals(data.series, notionalOf).filter(([, c]) => c > 0), usd));
434
+ pushRankedRoleLine(lines, "Estimated by role", rankedRoleTotals(data.series, notionalOf), usd);
378
435
  lines.push(costPerCommitLine("Estimated per commit", costPerCommitPairs(data.series, notionalOf)));
379
436
  }
380
437
  // Commits per backlog item is day-granular backlog data (features done + bugs fixed), so it
@@ -382,6 +439,18 @@ export function renderReportMarkdown(data) {
382
439
  const items = t.featuresDone + t.bugsFixed;
383
440
  const perItem = items > 0 ? ` · ${(t.commits / items).toFixed(1)} commits per backlog item` : "";
384
441
  lines.push(`**Commits:** ${t.workCommits ?? 0} work / ${t.maintenanceCommits ?? 0} maintenance${perItem}`);
442
+ // How long each finished plan or bug waited from filing to done, per completion day — the
443
+ // median backlog age at which the day's items closed. Team-only (a lag has no role) and
444
+ // zero-means-absent: no line when no day carries one. Values are whole days from
445
+ // daysBetween, so a plain `${n}d` is exact where medianLine's shortSpanPhrase would print
446
+ // minutes.
447
+ if (data.role === undefined) {
448
+ const lags = data.series.flatMap((d) => d.backlogLagMedianDays === undefined
449
+ ? []
450
+ : [`${monthDay(d.date)} ${d.backlogLagMedianDays}d`]);
451
+ if (lags.length > 0)
452
+ lines.push(`**Backlog lag median:** ${lags.join(" · ")}`);
453
+ }
385
454
  // Where the bugfix work was hardest to confirm: the window's fixed bugs tallied by their
386
455
  // `Validation gap` tag, so the team can see where its own test infrastructure was weakest
387
456
  // (recorded first, consumed now). Team-only (a tag has no role) and zero-means-absent: no
@@ -0,0 +1 @@
1
+ export {};
@@ -6,28 +6,41 @@ import { collectReport, collectReportSince } from "./report-data.js";
6
6
  import { collectFailureReport, collectFailureReportSince } from "../failure/failure-data.js";
7
7
  import { renderFailureMarkdown } from "../failure/failure-render.js";
8
8
  import { renderReportMarkdown, renderSinceReportMarkdown } from "./report-render.js";
9
- import { say, sayJson, sayJsonOrRender } from "../cli/cli-output.js";
10
- import { failRivalShapes, flagValue, parseCountFlag, parseRoleScope, parseSinceFlag } from "../cli/cli-args.js";
9
+ import { fail, say, sayJson, sayJsonOrRender } from "../cli/cli-output.js";
10
+ import { followView } from "../cli/follow-view.js";
11
+ import { durationLabel, failRivalShapes, flagValue, parseCountFlag, parseRoleListScope, parseSinceFlag } from "../cli/cli-args.js";
12
+ import { hasFollowFlag } from "../cli/cli-flag-specs.js";
11
13
  import { REPORT_DEFAULT_DAYS, REPORT_MAX_DAYS, REPORT_SINCE_MAX_MS } from "../events/event-window.js";
12
- /** `tumwater report [--days <n>] [--failures] [--since <duration>]`: parse the flags and
14
+ /** `tumwater report [--days <n>] [--failures] [--since <duration>]
15
+ * [--until <duration>]`: parse the flags and
13
16
  * print. Unknown-args rejection and the no-ready-repo-gate decision stay in cli.ts's case,
14
17
  * like every other command's. */
15
- export async function cmdReport(root, args) {
18
+ export async function cmdReport(root, args, seams = {}) {
16
19
  // --since is handled before the day-shape reads: it is a rival shape (totals over a
17
20
  // trailing window vs a series over whole days), not a modifier of either.
18
21
  const ms = parseSinceFlag(args, "report --since", REPORT_SINCE_MAX_MS);
22
+ const untilMs = parseSinceFlag(args, "report --until", REPORT_SINCE_MAX_MS, "--until");
23
+ if (untilMs !== null && ms === null)
24
+ fail("report --until requires --since");
25
+ if (untilMs !== null && ms !== null && untilMs >= ms)
26
+ fail(`report --until (${durationLabel(untilMs)}) must be shorter than --since (${durationLabel(ms)})`);
19
27
  // --role scopes both report shapes: the usage report (day and --since) and the failure
20
- // digest, whose day and --since collectors take the same optional base role.
21
- const role = parseRoleScope(root, args);
28
+ // digest, whose day and --since collectors take the same optional list of base roles.
29
+ const role = parseRoleListScope(root, args);
30
+ const follow = hasFollowFlag(args);
31
+ if (follow && args.includes("--json"))
32
+ fail("report --json prints one document; -f/--follow repaints a live view — drop one of them");
22
33
  if (ms !== null) {
23
34
  // --days is still a rival shape: it counts whole local days, while --since totals a trailing
24
35
  // window. --failures is not a rival anymore — the digest has a windowed-since collector that
25
36
  // reuses the day digest's own sections, so `report --failures --since <d>` dispatches it
26
- // through the existing --json / render choice.
37
+ // through the existing --json / render choice; --until bounds its window's end.
38
+ if (follow)
39
+ failRivalShapes("report --since", "-f/--follow", 'follow means "from now on"; --since shows a bounded past window');
27
40
  if (args.includes("--days"))
28
41
  failRivalShapes("report --since", "--days", "--days counts whole local days; --since totals a trailing window");
29
42
  if (args.includes("--failures")) {
30
- const digest = collectFailureReportSince(root, ms, role ?? undefined);
43
+ const digest = collectFailureReportSince(root, ms, role ?? undefined, untilMs ?? undefined);
31
44
  if (args.includes("--json"))
32
45
  sayJson(digest);
33
46
  else
@@ -36,7 +49,7 @@ export async function cmdReport(root, args) {
36
49
  }
37
50
  // --json swaps the renderer for the collector's own payload, exactly as status --json
38
51
  // does: bounds and cap checks above are shared, only the printing differs.
39
- const since = collectReportSince(root, ms, role ?? undefined);
52
+ const since = collectReportSince(root, ms, role ?? undefined, untilMs ?? undefined);
40
53
  sayJsonOrRender(args, since, renderSinceReportMarkdown);
41
54
  return;
42
55
  }
@@ -54,6 +67,10 @@ export async function cmdReport(root, args) {
54
67
  // report --json and doctor --json precedent: the time-and-spend fold gave the digest a
55
68
  // stable data shape, so the machine-readable form is the data itself, not a re-parse of the
56
69
  // Markdown (the old "clustered narrative with no agreed shape" refusal is retired).
70
+ if (follow) {
71
+ await followView(() => args.includes("--failures") ? renderFailureMarkdown(collectFailureReport(root, days, role ?? undefined)) : renderReportMarkdown(collectReport(root, days, role ?? undefined)), seams);
72
+ return;
73
+ }
57
74
  if (args.includes("--failures") && args.includes("--json")) {
58
75
  sayJson(collectFailureReport(root, days, role ?? undefined));
59
76
  return;
@@ -1,6 +1,6 @@
1
- export function cappedRequestTimeouts(cfg, tickCapS, quietCapS) {
1
+ export function cappedRequestTimeouts(cfg, runCapS, quietCapS) {
2
2
  return {
3
- runTimeoutSeconds: Math.min(cfg.runTimeoutSeconds, tickCapS),
3
+ runTimeoutSeconds: Math.min(cfg.runTimeoutSeconds, runCapS),
4
4
  quietTimeoutSeconds: cfg.quietTimeoutSeconds > 0 ? Math.min(cfg.quietTimeoutSeconds, quietCapS) : quietCapS,
5
5
  };
6
6
  }
@@ -9,15 +9,24 @@
9
9
  * repeat. Only the gate's positive flake record is trusted: a check-failure rejection is not
10
10
  * evidence of flakiness — it is exactly the attribution this memory exists to avoid making
11
11
  * twice. */
12
+ import { DAY_MS } from "../text/datetime.js";
13
+ import { TUMWATER_SOURCE, tryOrWarn } from "../events/events.js";
12
14
  import { readEventsSince } from "../events/event-window.js";
13
15
  import { increment } from "../collections.js";
14
16
  import { normalizeClusterKey } from "../failure/failure-cluster.js";
17
+ import { FAILING_TESTS_MAX } from "../build/build-check-report.js";
18
+ import { readJsonFile, writeJsonAtomic } from "../files/json-files.js";
19
+ import { isJsonObject } from "../files/json-object.js";
20
+ import { parseEntryDetails } from "../backlog/backlog-md.js";
21
+ import { readTextOrNull } from "../files/files.js";
22
+ import { quarantinePath } from "../paths.js";
23
+ import path from "node:path";
15
24
  /** How far back the gate looks for its own flake warnings. One local day: the example in
16
25
  * BUGS.md 2026-10-06 recorded a flake at 10:50 and rejected an unrelated change on the same
17
26
  * headline at 15:57, and a day is short enough that a retired flake stops excusing failures
18
27
  * while still covering a team's working span. Exported so the doctor's `flaky checks` check
19
28
  * and the gate share the one window and cannot drift. */
20
- export const KNOWN_FLAKE_WINDOW_MS = 24 * 60 * 60 * 1000;
29
+ export const KNOWN_FLAKE_WINDOW_MS = DAY_MS;
21
30
  /** The exact prefix review-precheck.ts writes before a flake's failure headline. Exported so
22
31
  * the writer and reader cannot drift over the separator. */
23
32
  export const FLAKY_WARNING_PREFIX = "gate check failed then passed on retry — flaky: ";
@@ -63,3 +72,93 @@ export function isKnownFlake(root, headline) {
63
72
  const { events } = readEventsSince(root, KNOWN_FLAKE_WINDOW_MS);
64
73
  return flakeKeys(events, Date.now()).has(normalizeClusterKey(headline));
65
74
  }
75
+ /** How many quarantined tests are kept; the oldest by lastSeen fall off. */
76
+ export const QUARANTINE_MAX = 100;
77
+ /** The quarantine list, a malformed file or entry reading as no data. */
78
+ export function loadQuarantine(root) {
79
+ const stored = readJsonFile(quarantinePath(root));
80
+ const out = [];
81
+ for (const e of Array.isArray(stored?.tests) ? stored.tests : []) {
82
+ if (!isJsonObject(e) || typeof e.test !== "string")
83
+ continue;
84
+ const num = (v) => (typeof v === "number" ? v : 0);
85
+ out.push({
86
+ test: e.test,
87
+ firstSeen: num(e.firstSeen),
88
+ lastSeen: num(e.lastSeen),
89
+ count: num(e.count) || 1,
90
+ bug: typeof e.bug === "string" ? e.bug : "",
91
+ ...(typeof e.tail === "string" ? { tail: e.tail } : {}),
92
+ });
93
+ }
94
+ return out;
95
+ }
96
+ function saveQuarantine(root, entries) {
97
+ const kept = [...entries].sort((a, b) => b.lastSeen - a.lastSeen).slice(0, QUARANTINE_MAX);
98
+ writeJsonAtomic(quarantinePath(root), { tests: kept });
99
+ }
100
+ /** The quarantined test names. */
101
+ export function quarantinedTests(root) {
102
+ return loadQuarantine(root).map((e) => e.test);
103
+ }
104
+ /** The failing tests when every one is quarantined; null when none is named, any is outside the
105
+ * list, or the list may have been cut short (FAILING_TESTS_MAX). */
106
+ export function onlyQuarantined(root, failingTests) {
107
+ if (!failingTests || failingTests.length === 0 || failingTests.length >= FAILING_TESTS_MAX)
108
+ return null;
109
+ const known = new Set(quarantinedTests(root));
110
+ return failingTests.every((t) => known.has(t)) ? [...failingTests] : null;
111
+ }
112
+ /** Quarantine each test of a check that failed and then passed on the same tree. A test already
113
+ * listed counts another flake; `tail` is kept for the BUGS.md entry of a new one. Returns the
114
+ * newly quarantined tests. Nothing is recorded for an empty or possibly truncated list. A failed
115
+ * write warns and returns no tests: the callers run this after a re-run passed, so a lost record
116
+ * must not fail that check. */
117
+ export function quarantineTests(root, tests, tail) {
118
+ if (!tests || tests.length === 0 || tests.length >= FAILING_TESTS_MAX)
119
+ return [];
120
+ const entries = loadQuarantine(root);
121
+ const now = Date.now();
122
+ const added = [];
123
+ for (const test of new Set(tests)) {
124
+ const have = entries.find((e) => e.test === test);
125
+ if (have) {
126
+ have.lastSeen = now;
127
+ have.count += 1;
128
+ }
129
+ else {
130
+ const text = typeof tail === "string" ? tail : (tail ?? []).join("\n");
131
+ entries.push({ test, firstSeen: now, lastSeen: now, count: 1, bug: "", ...(text ? { tail: text } : {}) });
132
+ added.push(test);
133
+ }
134
+ }
135
+ if (!tryOrWarn(root, TUMWATER_SOURCE, "write quarantine.json", () => saveQuarantine(root, entries)))
136
+ return [];
137
+ return added;
138
+ }
139
+ /** Record the BUGS.md heading filed for each of `tests`, dropping their kept output. */
140
+ export function markQuarantineFiled(root, bugs) {
141
+ const entries = loadQuarantine(root);
142
+ for (const e of entries) {
143
+ const bug = bugs.get(e.test);
144
+ if (bug === undefined)
145
+ continue;
146
+ e.bug = bug;
147
+ delete e.tail;
148
+ }
149
+ saveQuarantine(root, entries);
150
+ }
151
+ /** Release the quarantined tests whose filed BUGS.md entry is no longer under `## Open`; a later
152
+ * flake of one puts it back. Returns the released tests. A missing BUGS.md releases nothing. */
153
+ export function releaseQuarantine(root) {
154
+ const entries = loadQuarantine(root);
155
+ const md = readTextOrNull(path.join(root, "BUGS.md"));
156
+ if (md === null || entries.length === 0)
157
+ return [];
158
+ const open = new Set(parseEntryDetails(md, "Open").map((e) => e.title));
159
+ const released = entries.filter((e) => e.bug !== "" && !open.has(e.bug));
160
+ if (released.length === 0)
161
+ return [];
162
+ saveQuarantine(root, entries.filter((e) => !released.includes(e)));
163
+ return released.map((e) => e.test);
164
+ }