tumwater 0.4.2 → 0.4.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (354) hide show
  1. package/README.md +6 -6
  2. package/dist/build-info.json +2 -2
  3. package/dist/src/agent/agent-backend.js +6 -6
  4. package/dist/src/agent/{run-agent.js → agent-run.js} +27 -13
  5. package/dist/src/agent/{run-settle.js → agent-settle.js} +48 -24
  6. package/dist/src/agent/{run-spawn.js → agent-spawn.js} +21 -12
  7. package/dist/src/agent/{run-stream.js → agent-stream.js} +4 -4
  8. package/dist/src/agent/backend-traits.js +2 -2
  9. package/dist/src/agent/claude/claude-account.js +6 -27
  10. package/dist/src/agent/claude/claude-backend.js +3 -3
  11. package/dist/src/agent/claude/claude-env.js +27 -31
  12. package/dist/src/agent/claude/claude-stream.js +111 -54
  13. package/dist/src/agent/codex/codex-account.js +8 -0
  14. package/dist/src/agent/codex/codex-args.js +5 -5
  15. package/dist/src/agent/codex/codex-backend.js +2 -2
  16. package/dist/src/agent/codex/codex-env.js +28 -15
  17. package/dist/src/agent/codex/codex-hook-trust.js +39 -20
  18. package/dist/src/agent/codex/codex-rollout.js +93 -56
  19. package/dist/src/agent/codex/codex-stream.js +1 -1
  20. package/dist/src/agent/env-scrub.js +18 -0
  21. package/dist/src/agent/image-input.js +68 -11
  22. package/dist/src/agent/notes-shim.js +2 -2
  23. package/dist/src/agent/opencode/opencode-backend.js +26 -24
  24. package/dist/src/agent/opencode/opencode-catalog.js +85 -36
  25. package/dist/src/agent/opencode/opencode-config.js +32 -14
  26. package/dist/src/agent/opencode/opencode-env.js +12 -13
  27. package/dist/src/agent/opencode/opencode-stream.js +47 -25
  28. package/dist/src/agent/session-ledger.js +23 -27
  29. package/dist/src/agent/subscription-account.js +2 -83
  30. package/dist/src/backlog/backlog-archive.js +3 -4
  31. package/dist/src/backlog/backlog-cli-view.js +81 -41
  32. package/dist/src/backlog/backlog-cli.js +57 -26
  33. package/dist/src/backlog/backlog-eligibility.js +56 -122
  34. package/dist/src/backlog/backlog-md.js +5 -5
  35. package/dist/src/backlog/backlog-parts.js +65 -27
  36. package/dist/src/backlog/{question-answer.js → backlog-question-answer.js} +1 -1
  37. package/dist/src/backlog/backlog-release.js +12 -12
  38. package/dist/src/backlog/backlog-structure.js +114 -61
  39. package/dist/src/backlog/backlog.js +3 -2
  40. package/dist/src/baseline/main-baseline.js +148 -84
  41. package/dist/src/baseline/main-guardrails.js +2 -2
  42. package/dist/src/baseline/main-watch.js +84 -50
  43. package/dist/src/brief/brief.js +26 -13
  44. package/dist/src/budget/budget-fallback-view.js +100 -243
  45. package/dist/src/budget/budget-wiring.js +29 -65
  46. package/dist/src/budget/budget.js +4 -5
  47. package/dist/src/budget/fallback-breaker.js +2 -2
  48. package/dist/src/build/build-check-detect.js +43 -14
  49. package/dist/src/build/build-check-events.js +2 -2
  50. package/dist/src/build/build-check-report.js +10 -0
  51. package/dist/src/build/build-check-scoped.js +40 -18
  52. package/dist/src/build/build-check.js +13 -5
  53. package/dist/src/build/build-stage.js +6 -4
  54. package/dist/src/change/change-data.js +1 -4
  55. package/dist/src/cli/cli-command-args.js +89 -52
  56. package/dist/src/cli/cli-completion-command.js +75 -0
  57. package/dist/src/cli/{config-commands.js → cli-config-commands.js} +82 -71
  58. package/dist/src/cli/cli-doctor-command.js +103 -0
  59. package/dist/src/cli/cli-flag-specs.js +42 -22
  60. package/dist/src/cli/cli-init.js +36 -14
  61. package/dist/src/cli/{log-commands.js → cli-log-commands.js} +43 -23
  62. package/dist/src/cli/{notify-command.js → cli-notify-command.js} +2 -2
  63. package/dist/src/cli/cli-query-commands.js +2 -2
  64. package/dist/src/cli/{question-commands.js → cli-question-commands.js} +86 -60
  65. package/dist/src/cli/cli-repo-gate.js +2 -2
  66. package/dist/src/cli/cli-role-command.js +2 -41
  67. package/dist/src/cli/cli-start-summary.js +4 -3
  68. package/dist/src/cli/cli-start-supervisor.js +190 -0
  69. package/dist/src/cli/cli-start.js +94 -244
  70. package/dist/src/cli/completion.js +85 -49
  71. package/dist/src/cli/follow-view.js +20 -2
  72. package/dist/src/cli/help.js +6 -13
  73. package/dist/src/cli.js +70 -176
  74. package/dist/src/collections.js +39 -27
  75. package/dist/src/concurrency/check-slots.js +70 -0
  76. package/dist/src/concurrency/lock.js +23 -1
  77. package/dist/src/concurrency/run-permit.js +49 -0
  78. package/dist/src/concurrency/semaphore.js +46 -250
  79. package/dist/src/config/config-editable-keys.js +4 -7
  80. package/dist/src/config/config-fallbacks.js +81 -101
  81. package/dist/src/config/config-field-checks.js +66 -37
  82. package/dist/src/config/config-keys.js +66 -40
  83. package/dist/src/config/config-live.js +4 -28
  84. package/dist/src/config/{model-selector.js → config-model-selector.js} +4 -4
  85. package/dist/src/config/config-request.js +163 -102
  86. package/dist/src/config/config-schema.js +10 -12
  87. package/dist/src/config/config-selector-checks.js +1 -1
  88. package/dist/src/config/config-validation-agents.js +2 -2
  89. package/dist/src/config/config-validation-roles.js +8 -72
  90. package/dist/src/config/config-validation.js +4 -39
  91. package/dist/src/config/config-views.js +26 -50
  92. package/dist/src/config/config-write.js +128 -102
  93. package/dist/src/config/config.js +5 -14
  94. package/dist/src/doctor/doctor-backlog-bugs.js +7 -6
  95. package/dist/src/doctor/doctor-backlog-plans.js +53 -120
  96. package/dist/src/doctor/doctor-backlog.js +16 -14
  97. package/dist/src/doctor/doctor-check-names.js +3 -6
  98. package/dist/src/doctor/doctor-checks.js +8 -8
  99. package/dist/src/doctor/doctor-claude-checks.js +70 -36
  100. package/dist/src/doctor/doctor-codex-checks.js +6 -6
  101. package/dist/src/doctor/doctor-contention-checks.js +57 -117
  102. package/dist/src/doctor/doctor-fallback-checks.js +48 -26
  103. package/dist/src/doctor/doctor-holds.js +8 -47
  104. package/dist/src/doctor/doctor-install.js +1 -1
  105. package/dist/src/doctor/doctor-launch-services.js +1 -1
  106. package/dist/src/doctor/doctor-model-checks.js +178 -94
  107. package/dist/src/doctor/doctor-notify.js +2 -2
  108. package/dist/src/doctor/doctor-opencode-checks.js +5 -4
  109. package/dist/src/doctor/doctor-orphans.js +122 -21
  110. package/dist/src/doctor/{outcome.js → doctor-outcome.js} +7 -0
  111. package/dist/src/doctor/doctor-pi-checks.js +2 -2
  112. package/dist/src/doctor/doctor-principles.js +3 -3
  113. package/dist/src/doctor/doctor-project-checks.js +24 -11
  114. package/dist/src/doctor/doctor-role-notes.js +1 -1
  115. package/dist/src/doctor/doctor.js +8 -11
  116. package/dist/src/events/event-format.js +44 -64
  117. package/dist/src/events/event-labels.js +1 -5
  118. package/dist/src/events/event-legacy.js +3 -5
  119. package/dist/src/events/{notify.js → event-notify.js} +6 -6
  120. package/dist/src/events/event-read.js +1 -1
  121. package/dist/src/events/event-window.js +1 -1
  122. package/dist/src/events/events.js +8 -0
  123. package/dist/src/failure/failure-cluster.js +9 -18
  124. package/dist/src/failure/failure-data.js +12 -8
  125. package/dist/src/failure/failure-render.js +46 -36
  126. package/dist/src/failure/failure-state-change.js +54 -120
  127. package/dist/src/failure/{time-spend.js → failure-time-spend.js} +117 -84
  128. package/dist/src/files/files.js +30 -0
  129. package/dist/src/files/json-object.js +8 -8
  130. package/dist/src/files/stat-cache.js +12 -8
  131. package/dist/src/gates/budget-gates.js +85 -84
  132. package/dist/src/gates/gate-polls.js +32 -74
  133. package/dist/src/gates/pause-gates.js +16 -2
  134. package/dist/src/gates/readiness.js +1 -1
  135. package/dist/src/gates/role-cap-gates.js +44 -35
  136. package/dist/src/gates/{startup-gate.js → startup-gates.js} +9 -8
  137. package/dist/src/gates/{streak-gate.js → streak-gates.js} +75 -52
  138. package/dist/src/git/git-diff.js +6 -2
  139. package/dist/src/git/git.js +75 -45
  140. package/dist/src/git/worktree-pool.js +3 -6
  141. package/dist/src/gui/gui-admin-endpoints.js +3 -3
  142. package/dist/src/gui/gui-args.js +12 -0
  143. package/dist/src/gui/gui-backlog-commands.js +4 -4
  144. package/dist/src/gui/gui-backlog-endpoints.js +61 -41
  145. package/dist/src/gui/gui-command.js +43 -24
  146. package/dist/src/gui/gui-config-commands.js +2 -64
  147. package/dist/src/gui/gui-endpoint-commands.js +1 -1
  148. package/dist/src/gui/gui-endpoint-common.js +19 -1
  149. package/dist/src/gui/gui-history-endpoints.js +100 -52
  150. package/dist/src/gui/gui-operator-commands.js +43 -48
  151. package/dist/src/gui/gui-prompt-commands.js +25 -25
  152. package/dist/src/gui/gui-report-endpoints.js +1 -1
  153. package/dist/src/gui/gui-server.js +1 -2
  154. package/dist/src/gui/gui-team-commands.js +2 -1
  155. package/dist/src/history/history-data.js +71 -44
  156. package/dist/src/history/history-queued.js +49 -27
  157. package/dist/src/history/history.js +54 -36
  158. package/dist/src/inbox/prompt-commands.js +59 -42
  159. package/dist/src/init/init.js +88 -40
  160. package/dist/src/merge/merge-backlog-conflicts.js +1 -1
  161. package/dist/src/merge/merge-check-failures.js +12 -9
  162. package/dist/src/merge/merge-core.js +7 -32
  163. package/dist/src/merge/merge-drain.js +11 -13
  164. package/dist/src/merge/merge-pipeline.js +6 -7
  165. package/dist/src/merge/merge-post-review-drain.js +11 -14
  166. package/dist/src/merge/merge-queue-render.js +3 -3
  167. package/dist/src/merge/merge-requeue.js +1 -1
  168. package/dist/src/merge/merge-slot.js +0 -8
  169. package/dist/src/merge/merge-stack.js +17 -18
  170. package/dist/src/merge/merge-to-main.js +13 -58
  171. package/dist/src/merge/merge-verify.js +12 -21
  172. package/dist/src/notes-cli.js +52 -42
  173. package/dist/src/operator/operator-commands.js +80 -47
  174. package/dist/src/operator/operator-intent.js +3 -3
  175. package/dist/src/orchestrator/orchestrator-launch.js +31 -63
  176. package/dist/src/orchestrator/orchestrator-poll.js +44 -69
  177. package/dist/src/orchestrator/orchestrator-scheduling.js +5 -9
  178. package/dist/src/orchestrator/orchestrator.js +8 -21
  179. package/dist/src/orchestrator/scheduling-admit.js +101 -68
  180. package/dist/src/orchestrator/scheduling-scope.js +18 -27
  181. package/dist/src/orchestrator/scheduling-skip.js +10 -14
  182. package/dist/src/orchestrator/startup-migrations.js +1 -1
  183. package/dist/src/paths.js +10 -9
  184. package/dist/src/pi/pi-args.js +5 -2
  185. package/dist/src/pi/pi-backend.js +2 -2
  186. package/dist/src/pi/pi-bin.js +3 -3
  187. package/dist/src/pi/pi-event-line.js +28 -1
  188. package/dist/src/pi/pi-listed-models.js +13 -0
  189. package/dist/src/pi/pi-models.js +43 -29
  190. package/dist/src/pi/pi-settings.js +43 -20
  191. package/dist/src/pi/pi-stream.js +41 -8
  192. package/dist/src/pi/pi-watchdogs.js +43 -15
  193. package/dist/src/pi/pi.js +2 -2
  194. package/dist/src/pi-extension/context-shake.js +32 -20
  195. package/dist/src/pi-extension/kill-guard.js +69 -0
  196. package/dist/src/process/process-group.js +51 -26
  197. package/dist/src/process/process.js +20 -12
  198. package/dist/src/process/run-marker.js +33 -15
  199. package/dist/src/process/supervisor.js +40 -28
  200. package/dist/src/prompt/dialect.js +1 -1
  201. package/dist/src/prompt/gate-notes.js +2 -2
  202. package/dist/src/prompt/gate-prompts.js +1 -44
  203. package/dist/src/prompt/notes-backlog.js +66 -37
  204. package/dist/src/prompt/principles.js +6 -4
  205. package/dist/src/prompt/prompt-followup.js +12 -4
  206. package/dist/src/prompt/prompt.js +88 -38
  207. package/dist/src/redeploy/redeployer.js +21 -1
  208. package/dist/src/report/report-data.js +53 -37
  209. package/dist/src/report/report-fold.js +16 -9
  210. package/dist/src/report/report-render.js +21 -194
  211. package/dist/src/report/report-since-data.js +4 -3
  212. package/dist/src/report/report-since-render.js +173 -0
  213. package/dist/src/report/report-usage-fold.js +8 -8
  214. package/dist/src/report/report.js +46 -22
  215. package/dist/src/review/diff-rules.js +35 -21
  216. package/dist/src/review/post-merge-review-render.js +9 -3
  217. package/dist/src/review/post-merge-review.js +48 -41
  218. package/dist/src/review/review-precheck.js +14 -4
  219. package/dist/src/review/review.js +48 -170
  220. package/dist/src/review/secret-leaks.js +26 -16
  221. package/dist/src/roles/legacy-role-migration.js +6 -3
  222. package/dist/src/roles/role-catalog.js +11 -3
  223. package/dist/src/roles/role-guidance.js +16 -12
  224. package/dist/src/roles/role-render.js +7 -46
  225. package/dist/src/roles/role-view.js +10 -33
  226. package/dist/src/run/acceptance-greps.js +88 -45
  227. package/dist/src/run/ci-parity.js +19 -8
  228. package/dist/src/run/run-apply.js +12 -15
  229. package/dist/src/run/run-detail-data.js +121 -68
  230. package/dist/src/run/run-detail-render.js +92 -35
  231. package/dist/src/run/run-detail.js +72 -41
  232. package/dist/src/run/run-prompt.js +63 -50
  233. package/dist/src/run/run-resume.js +76 -29
  234. package/dist/src/run/run-stage.js +141 -90
  235. package/dist/src/run/run-timing.js +8 -2
  236. package/dist/src/run/run-usage.js +31 -1
  237. package/dist/src/run/run-verdict.js +128 -50
  238. package/dist/src/run/stage-check.js +60 -50
  239. package/dist/src/run/stage-checks-shared.js +28 -0
  240. package/dist/src/run/stage-name-checks.js +111 -92
  241. package/dist/src/run/stage-path-checks.js +69 -42
  242. package/dist/src/run/stage-prose-checks.js +55 -40
  243. package/dist/src/runner/{leftover.js → runner-leftover.js} +9 -26
  244. package/dist/src/runner/runner-outcome.js +7 -2
  245. package/dist/src/runner/runner-pi.js +9 -22
  246. package/dist/src/runner/runner-recovery.js +5 -13
  247. package/dist/src/runner/{revision.js → runner-revision.js} +1 -2
  248. package/dist/src/runner/runner-run.js +103 -50
  249. package/dist/src/runner/runner-state-load.js +18 -28
  250. package/dist/src/runner/runner-state.js +0 -14
  251. package/dist/src/runner/runner.js +41 -25
  252. package/dist/src/scheduling/backoff.js +8 -3
  253. package/dist/src/scheduling/claim-entries.js +2 -1
  254. package/dist/src/scheduling/claims.js +0 -4
  255. package/dist/src/scheduling/scheduling.js +53 -66
  256. package/dist/src/status/status-budget.js +4 -43
  257. package/dist/src/status/status-data.js +35 -39
  258. package/dist/src/status/status-role-notes.js +2 -2
  259. package/dist/src/status/status-roles.js +9 -18
  260. package/dist/src/team/{error-storm.js → team-error-storm.js} +1 -1
  261. package/dist/src/team/{failure-spread.js → team-failure-spread.js} +5 -5
  262. package/dist/src/team/team-hold.js +105 -70
  263. package/dist/src/team/{orchestrator-info.js → team-orchestrator-info.js} +55 -36
  264. package/dist/src/team/team-polls.js +53 -201
  265. package/dist/src/team/{reclaim.js → team-reclaim.js} +61 -31
  266. package/dist/src/team/team-state.js +4 -13
  267. package/dist/src/text/datetime.js +16 -7
  268. package/dist/src/text/markdown.js +1 -1
  269. package/dist/src/text/phrases.js +29 -15
  270. package/dist/src/text/suggest.js +1 -1
  271. package/dist/src/text/text.js +6 -5
  272. package/dist/src/ui/alert-feeds.js +46 -111
  273. package/dist/src/ui/badges.js +17 -88
  274. package/dist/src/ui/gui/gui-client-boot.js +2 -8
  275. package/dist/src/ui/gui/gui-client-composer.js +1 -1
  276. package/dist/src/ui/gui/gui-client-drawer-config.js +8 -33
  277. package/dist/src/ui/gui/gui-client-drawer.js +1 -1
  278. package/dist/src/ui/gui/gui-client-feed.js +1 -1
  279. package/dist/src/ui/gui/gui-client-history.js +28 -14
  280. package/dist/src/ui/gui/gui-client-model.js +0 -12
  281. package/dist/src/ui/gui/gui-client-operator.js +7 -5
  282. package/dist/src/ui/gui/gui-client-pending.js +7 -6
  283. package/dist/src/ui/gui/gui-client-report-chart.js +5 -5
  284. package/dist/src/ui/gui/gui-client-report.js +1 -1
  285. package/dist/src/ui/gui/gui-client-roles.js +0 -1
  286. package/dist/src/ui/gui/gui-client-settings.js +43 -84
  287. package/dist/src/ui/gui/gui-client-team.js +12 -6
  288. package/dist/src/ui/gui/gui-client.js +7 -1
  289. package/dist/src/ui/progress-data.js +107 -44
  290. package/dist/src/ui/run-progress-model.js +20 -4
  291. package/dist/src/ui/status-model.js +2 -24
  292. package/dist/src/ui/status-payload.js +11 -30
  293. package/dist/src/ui/status-render.js +13 -24
  294. package/dist/src/ui/team-alert-backlog.js +14 -204
  295. package/dist/src/ui/team-alert-shared.js +6 -29
  296. package/dist/src/ui/team-alert-status.js +9 -170
  297. package/dist/src/ui/team-alert-tail.js +19 -215
  298. package/dist/src/ui/team-alerts.js +8 -15
  299. package/dist/src/ui/transcript-run.js +27 -16
  300. package/dist/src/ui/tui/tui-backlog.js +2 -2
  301. package/dist/src/ui/tui/tui-editor-answer.js +12 -10
  302. package/dist/src/ui/tui/tui-editor-attach.js +4 -3
  303. package/dist/src/ui/tui/tui-editor-backends.js +4 -3
  304. package/dist/src/ui/tui/tui-editor-brief.js +6 -9
  305. package/dist/src/ui/tui/tui-editor-budget.js +4 -3
  306. package/dist/src/ui/tui/tui-editor-config.js +80 -68
  307. package/dist/src/ui/tui/tui-editor-entry.js +12 -20
  308. package/dist/src/ui/tui/tui-editor-pause.js +51 -57
  309. package/dist/src/ui/tui/tui-editor-prompt.js +9 -13
  310. package/dist/src/ui/tui/tui-editor-queued.js +11 -14
  311. package/dist/src/ui/tui/tui-editor-quiet.js +4 -3
  312. package/dist/src/ui/tui/tui-editor-release.js +4 -3
  313. package/dist/src/ui/tui/tui-editor-retire.js +7 -7
  314. package/dist/src/ui/tui/tui-editor-role-enabled.js +4 -3
  315. package/dist/src/ui/tui/tui-editor-role-file.js +4 -4
  316. package/dist/src/ui/tui/tui-editor-role-key.js +4 -3
  317. package/dist/src/ui/tui/tui-editor-role-model.js +97 -15
  318. package/dist/src/ui/tui/tui-editor-search.js +4 -3
  319. package/dist/src/ui/tui/tui-editor-session.js +17 -4
  320. package/dist/src/ui/tui/tui-editors.js +5 -10
  321. package/dist/src/ui/tui/tui-frame.js +182 -134
  322. package/dist/src/ui/tui/tui-guard.js +20 -0
  323. package/dist/src/ui/tui/tui-input.js +167 -98
  324. package/dist/src/ui/tui/tui-keymap.js +51 -29
  325. package/dist/src/ui/tui/tui-keys.js +1 -1
  326. package/dist/src/ui/tui/tui-pane-activity.js +5 -4
  327. package/dist/src/ui/tui/tui-pane-backlog.js +74 -36
  328. package/dist/src/ui/tui/tui-pane-ctx.js +1 -1
  329. package/dist/src/ui/tui/tui-pane-health.js +252 -230
  330. package/dist/src/ui/tui/tui-pane-history.js +67 -74
  331. package/dist/src/ui/tui/tui-pane-pending.js +288 -247
  332. package/dist/src/ui/tui/tui-pane-queued.js +25 -27
  333. package/dist/src/ui/tui/tui-pane-report.js +2 -1
  334. package/dist/src/ui/tui/tui-pane-session.js +15 -10
  335. package/dist/src/ui/tui/tui-pane-transcript.js +19 -31
  336. package/dist/src/ui/tui/tui-pane.js +14 -11
  337. package/dist/src/ui/tui/tui-panes.js +38 -33
  338. package/dist/src/ui/tui/tui.js +4 -7
  339. package/dist/src/verdict/fix-claim.js +107 -56
  340. package/dist/src/verdict/refusal.js +1 -1
  341. package/dist/src/verdict/reply-contract.js +3 -4
  342. package/package.json +5 -5
  343. package/dist/src/concurrency/provider-lease.js +0 -105
  344. package/dist/src/review/review-followup.js +0 -87
  345. package/dist/src/review/review-run.js +0 -395
  346. package/dist/src/runner/model-fallback.js +0 -76
  347. package/dist/src/runner/runner-fallback.js +0 -66
  348. package/dist/src/ui/tui/tui-editor-claude-reserve.js +0 -24
  349. package/dist/src/ui/tui/tui-editor-provider-caps.js +0 -27
  350. /package/dist/src/cli/{brief-command.js → cli-brief-command.js} +0 -0
  351. /package/dist/src/doctor/{probe.js → doctor-probe.js} +0 -0
  352. /package/dist/src/gates/{disk-gate.js → disk-gates.js} +0 -0
  353. /package/dist/src/operator/{retire.js → operator-retire.js} +0 -0
  354. /package/dist/src/operator/{revert.js → operator-revert.js} +0 -0
@@ -0,0 +1,173 @@
1
+ import { compactTokens, usd } from "../text/format.js";
2
+ import { durationLabel } from "../cli/cli-args.js";
3
+ import { boundedWindowLine, sinceWindowLine } from "../text/datetime.js";
4
+ import { SPARSE_WINDOW_NOTE } from "../events/event-window.js";
5
+ import { foldBaseRoles } from "./report-fold.js";
6
+ import { rankedRoleMap, rankedRoleLine, promptTokensLine, pushRankedRoleLine, pushPresentRankedLine, pushProviderCostLines, pushReportSummary, costPerCommitPairsFrom, costPerCommitLine, } from "./report-render.js";
7
+ /** Render a trailing-window totals report as Markdown — the aggregate sibling of the
8
+ * `logs --since` raw stream ("how much" here, "what exactly" there). Pure function of
9
+ * SinceReport: no I/O, no clock reads (the window line's local time is rendered from the
10
+ * collector's captured cutoff, so the printed window matches the aggregated one). */
11
+ export function renderSinceReportMarkdown(data) {
12
+ const lines = [];
13
+ pushSinceWindowHeader(lines, data);
14
+ pushSinceSummary(lines, data);
15
+ pushSinceRoleLines(lines, data);
16
+ pushSinceProviderModelLines(lines, data);
17
+ pushSinceMainRedLines(lines, data);
18
+ pushSinceFrictionLines(lines, data);
19
+ pushSinceDeferralLines(lines, data);
20
+ pushSinceCommitLines(lines, data);
21
+ pushSinceCostLines(lines, data);
22
+ pushSinceCoverageLines(lines, data);
23
+ return lines.join("\n");
24
+ }
25
+ /** The window header: the title, the bounded-or-since window line and the role scope line. */
26
+ function pushSinceWindowHeader(lines, data) {
27
+ lines.push("# tumwater usage report");
28
+ lines.push("");
29
+ lines.push(data.untilMs !== undefined && data.toIso !== undefined
30
+ ? boundedWindowLine(durationLabel(data.sinceMs), durationLabel(data.untilMs), data.fromIso, data.toIso)
31
+ : sinceWindowLine(durationLabel(data.sinceMs), data.fromIso));
32
+ if (data.role !== undefined)
33
+ lines.push(`role: ${data.role}`);
34
+ lines.push("");
35
+ }
36
+ /** The shared totals voice (minus features/bugs/filing cells, stated at the tail), gated
37
+ * to the team report like the day sibling's. */
38
+ function pushSinceSummary(lines, data) {
39
+ const t = data.totals;
40
+ // Same totals voice as the day report, minus the features/bugs and filing cells: those tallies
41
+ // come from day-granular backlog-file dates that cannot subdivide a sub-day window, so the
42
+ // final line below states the omission instead of showing a day-rounded number.
43
+ pushReportSummary(lines, t, "", data.role !== undefined);
44
+ lines.push("");
45
+ }
46
+ /** Runs / tokens / prompt-tokens / cost breakdowns by role. */
47
+ function pushSinceRoleLines(lines, data) {
48
+ lines.push(rankedRoleLine("Runs by role", rankedRoleMap(data.runsByRole), String));
49
+ // Same breakdown for token spend: ranked by tokens desc then name asc, zero-token roles
50
+ // omitted — an all-zero window renders "-" like the other lines. Output tokens are the
51
+ // resource the budget charges, so this is the line an operator reads to find which role's
52
+ // prompt loop burned the window's tokens.
53
+ pushRankedRoleLine(lines, "Tokens by role", rankedRoleMap(data.tokensByRole), compactTokens);
54
+ // The prompt side of the same breakdown, straight after the output-token line: how many
55
+ // prompt tokens each role sent and the share of them its backend served from cache — the
56
+ // input side of the bill an operator cannot see from Tokens by role. Zero-prompt roles are
57
+ // omitted, and an all-zero window renders "-" like the sibling lines.
58
+ lines.push(promptTokensLine(rankedRoleMap(data.promptTokensByRole ?? {}), rankedRoleMap(data.cacheReadTokensByRole ?? {})));
59
+ // Same breakdown for spend: ranked by spend desc then name asc, zero-spend roles omitted —
60
+ // an all-zero window renders "-" like the runs line.
61
+ pushRankedRoleLine(lines, "Cost by role", rankedRoleMap(data.costByRole), usd);
62
+ }
63
+ /** The provider cost lines and the per-model line, gated to the team report. */
64
+ function pushSinceProviderModelLines(lines, data) {
65
+ // The same breakdown per PROVIDER (plans: per-provider spend), gated to the team report
66
+ // like its day sibling. SinceReport.costByProvider is always present (empty when none), so
67
+ // the zero-means-absent gate prints no line for a window with no provider spend.
68
+ if (data.role === undefined) {
69
+ pushProviderCostLines(lines, () => rankedRoleMap(data.costByProvider), () => rankedRoleMap(data.notionalByProvider));
70
+ // The same breakdown per MODEL (each run_start's agent-qualified selector — the model a
71
+ // team mixing providers, per-role overrides and fallbacks actually ran on), gated to the
72
+ // team report like the provider lines: a model is not a role, so a role-scoped window
73
+ // omits it. SinceReport.runsByModel is always present, so a window with no modelled
74
+ // start prints no line (keeping a model-less day's output unchanged), exactly as the
75
+ // provider lines stay absent when no provider spent.
76
+ pushPresentRankedLine(lines, "Runs by model", rankedRoleMap(data.runsByModel));
77
+ }
78
+ }
79
+ /** Main's red time and the test/merge culprits behind it, gated like the day sibling. */
80
+ function pushSinceMainRedLines(lines, data) {
81
+ // Main's red wall-clock over the window (main's own, not any role's — a role-scoped
82
+ // collector carries 0, so this never prints for a scoped window). Zero-absent: no line for
83
+ // a window main was never red.
84
+ if (data.mainRedMs > 0)
85
+ lines.push(`**Main red:** ${durationLabel(data.mainRedMs)}`);
86
+ // The same "what kept main red" detail named by failing test — which tests main's own red
87
+ // checks named over the window. Team-only like main's red time, so a role-scoped window
88
+ // omits it; zero-absent: no line when the window's red checks named no test.
89
+ if (data.role === undefined) {
90
+ pushPresentRankedLine(lines, "Main red by test", rankedRoleMap(data.mainRedByTest ?? {}));
91
+ // The same "what kept main red" detail named by merge — the landed merges the watch's
92
+ // own main_culprit events blamed, mirroring the failing-test line right above. Team-only
93
+ // like main's red time and zero-absent: no line when the window blamed no merge.
94
+ pushPresentRankedLine(lines, "Main red by merge", rankedRoleMap(data.mainRedByCulprit ?? {}));
95
+ }
96
+ }
97
+ /** The review-friction and failed-run breakdowns by role. */
98
+ function pushSinceFrictionLines(lines, data) {
99
+ // The same breakdown for review friction: rejections per role from review_rejected events;
100
+ // a window with none renders "-" like the other lines. The post-merge line is the
101
+ // afterMerge: true subset — rejections that reached an already-merged commit.
102
+ pushRankedRoleLine(lines, "Rejections by role", rankedRoleMap(data.rejectionsByRole));
103
+ pushRankedRoleLine(lines, "Post-merge rejections by role", rankedRoleMap(data.postMergeRejectionsByRole));
104
+ pushRankedRoleLine(lines, "Revisions by role", rankedRoleMap(data.revisionsByRole));
105
+ pushRankedRoleLine(lines, "Refusals by role", rankedRoleMap(data.refusalsByRole));
106
+ // The same breakdown for outright failures: error-class run_ends per role under the
107
+ // digest's OUTCOME_CLASS — the runs that burned agent time and merged nothing without
108
+ // going through review, which rejections/revisions would miss entirely. A window with
109
+ // none renders "-" like the other lines.
110
+ pushRankedRoleLine(lines, "Failed runs by role", rankedRoleMap(data.failedRunsByRole));
111
+ pushRankedRoleLine(lines, "Image reroutes by role", rankedRoleMap(data.imageReroutesByRole));
112
+ }
113
+ /** Deferred maintenance runs by reason code, gated to the team report. */
114
+ function pushSinceDeferralLines(lines, data) {
115
+ // Deferred maintenance runs per reason code, the --since twin of the day render's line:
116
+ // how many due maintenance runs stood down and why (backlog / no-work / plans-full).
117
+ // Gated to the team report — a reason is not a role, so a role-scoped window omits it.
118
+ // Zero-means-absent: a window with no deferral prints no line.
119
+ if (data.role === undefined) {
120
+ pushPresentRankedLine(lines, "Deferred runs by reason", rankedRoleMap(data.deferredByReason ?? {}));
121
+ }
122
+ }
123
+ /** Commits by role and the merged/merge-failure split, gated to the team report. */
124
+ function pushSinceCommitLines(lines, data) {
125
+ // The merged-commits split by base role, matching the day render's unfiltered line: the
126
+ // window's totals.commits carries the count; this line says which role's commits it was.
127
+ lines.push(rankedRoleLine("Commits by role", rankedRoleMap(data.commitsByRole), String));
128
+ // The merged-outcomes split by base role, mirroring the day render's line: which roles'
129
+ // changes reach main and which bounce off the gate (merge_failed). Gated to the team
130
+ // report — a role-scoped window omits merge (ROLE_SCOPE_NOTE), so the per-role
131
+ // attribution would contradict it. Zero-means-absent: the failures line prints only when
132
+ // some role has one; a merge-free window renders `-` like the other lines.
133
+ if (data.role === undefined) {
134
+ lines.push(rankedRoleLine("Merged by role", rankedRoleMap(data.mergedByRole), String));
135
+ pushPresentRankedLine(lines, "Merge failures by role", rankedRoleMap(data.mergeFailedByRole));
136
+ // WHY the window's merges failed, from each merge_failed's result code — the day
137
+ // report's reason twin of the role line above, so an operator can tell a review
138
+ // rejection (fix the change) from a conflict/block (rebase) from a shutdown abort
139
+ // (capacity). Ranked the same way (count desc then code asc), reason-free only.
140
+ pushPresentRankedLine(lines, "Merge failures by reason", rankedRoleMap(data.mergeFailedByReason ?? {}));
141
+ }
142
+ }
143
+ /** Cost/estimated per commit and the notional Estimated by role lines. */
144
+ function pushSinceCostLines(lines, data) {
145
+ // Spend per merged commit, the --since twin of the day render's `Cost per commit:` line:
146
+ // costByRole is keyed by role id (instances not folded, so `feature-2` joins `feature`),
147
+ // commitsByRole is already base-role keyed. Prints for a spend-free window as `-` and for a
148
+ // role-scoped window from its scoped maps, matching the day render.
149
+ lines.push(costPerCommitLine("Cost per commit", costPerCommitPairsFrom(foldBaseRoles(data.costByRole), data.commitsByRole)));
150
+ // Subscription spend is notional: its own line, only when the window has any — the same
151
+ // conditional/filtered `usd` form the day render's "Estimated by role" uses.
152
+ if (data.notionalByRole !== undefined) {
153
+ pushRankedRoleLine(lines, "Estimated by role", rankedRoleMap(data.notionalByRole), usd);
154
+ // The day render's `Estimated per commit:` twin, after `Estimated by role`, from the same
155
+ // notionalByRole source folded to base roles and the base-keyed commitsByRole.
156
+ lines.push(costPerCommitLine("Estimated per commit", costPerCommitPairsFrom(foldBaseRoles(data.notionalByRole), data.commitsByRole)));
157
+ }
158
+ }
159
+ /** The trailing blank and the exactly-one coverage/backlog-omission note. */
160
+ function pushSinceCoverageLines(lines, data) {
161
+ lines.push("");
162
+ // Exactly one final line, chosen by the collector's coverage proof: either a hedged note
163
+ // that stays true whenever it prints (the oldest retained event lies inside the window —
164
+ // whether from rotation or a log born inside the window), or the one-time statement of the
165
+ // backlog-tally omission. A flat "rotated out" claim would be false for a log that never
166
+ // had the older events, so the wording names only what is provably the case. A role-scoped
167
+ // report already states the omission in ROLE_SCOPE_NOTE, so it does not repeat the --days
168
+ // pointer, which would imply a scoped day report shows the tallies it too omits.
169
+ if (!data.coversFullWindow)
170
+ lines.push(SPARSE_WINDOW_NOTE);
171
+ else if (data.role === undefined)
172
+ lines.push("backlog tallies (features done / bugs fixed / plans added / bugs filed) need the day report (--days)");
173
+ }
@@ -10,7 +10,7 @@ import { addTo } from "../collections.js";
10
10
  import { isPassingFullCheck, isRedFullCheck } from "../build/build-check-events.js";
11
11
  import { eventRole, eventUsage } from "../events/event-read.js";
12
12
  import { finiteNumber, nonNegativeNumber } from "../files/json-object.js";
13
- import { OUTCOME_CLASS } from "../failure/time-spend.js";
13
+ import { OUTCOME_CLASS } from "../failure/failure-time-spend.js";
14
14
  import { commitTier } from "../roles/roles.js";
15
15
  import { baseRoleOf } from "../roles/runner-ids.js";
16
16
  /** Fold one event's `providerCost` map (`Record<provider, {costUsd, notionalCostUsd}>` — the
@@ -139,10 +139,10 @@ function foldRedCheck(target, _ev) {
139
139
  // The merge slot's red full-suite checks, for the day report's red-check count.
140
140
  target.checkFailures++;
141
141
  }
142
- /** Fold a model_fallback_started: one episode per start event, per role. */
143
- function foldFallbackStarted(target, ev) {
144
- // One episode per start event; durations are not summed (an episode can span the window edge).
145
- addTo(target.fallbacksByRole, eventRole(ev), 1);
142
+ /** Fold an image_reroute: one reroute per event, preflight or after a blind read. */
143
+ function foldImageReroute(target, ev) {
144
+ // One reroute per event, preflight or after a blind read.
145
+ addTo(target.imageReroutesByRole, eventRole(ev), 1);
146
146
  }
147
147
  /** Fold a run_deferred: one deferral per event, bucketed by the raw reason code (team-only). */
148
148
  function foldDeferred(target, ev) {
@@ -245,7 +245,7 @@ function foldMergeOutcome(target, ev) {
245
245
  }
246
246
  /** Fold one event into a usage accumulator by dispatching to a per-family builder —
247
247
  * foldRunEnd, foldReviewRejected, foldReviewVerdict, foldRevision, foldPassingCheck,
248
- * foldRedCheck, foldFallbackStarted, foldDeferred, foldMerged,
248
+ * foldRedCheck, foldImageReroute, foldDeferred, foldMerged,
249
249
  * foldRunStarted, foldProviderWait, foldSlotWait, foldCheckWait or foldMergeOutcome — each
250
250
  * named for the family it folds, so how a family folds is found by name. The dispatch holds
251
251
  * no accumulation of its own. All behavior lives in the builders: run_end events add a run
@@ -269,8 +269,8 @@ export function foldUsageEvent(target, ev) {
269
269
  foldPassingCheck(target, ev);
270
270
  else if (isRedFullCheck(ev))
271
271
  foldRedCheck(target, ev);
272
- else if (ev.type === "model_fallback_started")
273
- foldFallbackStarted(target, ev);
272
+ else if (ev.type === "image_reroute")
273
+ foldImageReroute(target, ev);
274
274
  else if (ev.type === "run_deferred")
275
275
  foldDeferred(target, ev);
276
276
  else if (ev.type === "merged")
@@ -6,7 +6,8 @@ import { collectReport } from "./report-data.js";
6
6
  import { collectReportSince } from "./report-since-data.js";
7
7
  import { collectFailureReport, collectFailureReportSince } from "../failure/failure-data.js";
8
8
  import { renderFailureMarkdown } from "../failure/failure-render.js";
9
- import { renderReportMarkdown, renderSinceReportMarkdown } from "./report-render.js";
9
+ import { renderReportMarkdown } from "./report-render.js";
10
+ import { renderSinceReportMarkdown } from "./report-since-render.js";
10
11
  import { fail, say, sayJson, sayJsonOrRender } from "../cli/cli-output.js";
11
12
  import { followView } from "../cli/follow-view.js";
12
13
  import { failRivalShapes, failUntilShorterThanSince, flagValue, parseCountFlag, parseRoleListScope, parseSinceFlag } from "../cli/cli-args.js";
@@ -26,31 +27,47 @@ export async function cmdReport(root, args, seams = {}) {
26
27
  // digest, whose day and --since collectors take the same optional list of base roles.
27
28
  const role = parseRoleListScope(root, args);
28
29
  const follow = hasFollowFlag(args);
30
+ failOnFollowJson(follow, args);
31
+ if (ms !== null) {
32
+ sinceReportShape(root, args, ms, role, untilMs, follow);
33
+ return;
34
+ }
35
+ await dayReportShape(root, args, reportDays(args), role, follow, seams);
36
+ }
37
+ /** `-f/--follow` with `--json` is refused before either shape dispatches: the live view
38
+ * repaints a document, `--json` prints one — drop one of them. */
39
+ function failOnFollowJson(follow, args) {
29
40
  if (follow && args.includes("--json"))
30
41
  fail("report --json prints one document; -f/--follow repaints a live view — drop one of them");
31
- if (ms !== null) {
32
- // --days is still a rival shape: it counts whole local days, while --since totals a trailing
33
- // window. --failures is not a rival anymore — the digest has a windowed-since collector that
34
- // reuses the day digest's own sections, so `report --failures --since <d>` dispatches it
35
- // through the existing --json / render choice; --until bounds its window's end.
36
- if (follow)
37
- failRivalShapes("report --since", "-f/--follow", 'follow means "from now on"; --since shows a bounded past window');
38
- if (args.includes("--days"))
39
- failRivalShapes("report --since", "--days", "--days counts whole local days; --since totals a trailing window");
40
- if (args.includes("--failures")) {
41
- const digest = collectFailureReportSince(root, ms, role ?? undefined, untilMs ?? undefined);
42
- if (args.includes("--json"))
43
- sayJson(digest);
44
- else
45
- say(renderFailureMarkdown(digest));
46
- return;
47
- }
48
- // --json swaps the renderer for the collector's own payload, exactly as status --json
49
- // does: bounds and cap checks above are shared, only the printing differs.
50
- const since = collectReportSince(root, ms, role ?? undefined, untilMs ?? undefined);
51
- sayJsonOrRender(args, since, renderSinceReportMarkdown);
42
+ }
43
+ /** The `--since`/`--until` trailing-window shape: its rival-shape guards against
44
+ * `-f/--follow` and `--days`, then the `--failures` digest or the plain totals, both through
45
+ * the `--json` / render choice. Fatal on any rival flag; returns only after printing. */
46
+ function sinceReportShape(root, args, ms, role, untilMs, follow) {
47
+ // --days is still a rival shape: it counts whole local days, while --since totals a trailing
48
+ // window. --failures is not a rival anymore — the digest has a windowed-since collector that
49
+ // reuses the day digest's own sections, so `report --failures --since <d>` dispatches it
50
+ // through the existing --json / render choice; --until bounds its window's end.
51
+ if (follow)
52
+ failRivalShapes("report --since", "-f/--follow", 'follow means "from now on"; --since shows a bounded past window');
53
+ if (args.includes("--days"))
54
+ failRivalShapes("report --since", "--days", "--days counts whole local days; --since totals a trailing window");
55
+ if (args.includes("--failures")) {
56
+ const digest = collectFailureReportSince(root, ms, role ?? undefined, untilMs ?? undefined);
57
+ if (args.includes("--json"))
58
+ sayJson(digest);
59
+ else
60
+ say(renderFailureMarkdown(digest));
52
61
  return;
53
62
  }
63
+ // --json swaps the renderer for the collector's own payload, exactly as status --json
64
+ // does: bounds and cap checks above are shared, only the printing differs.
65
+ const since = collectReportSince(root, ms, role ?? undefined, untilMs ?? undefined);
66
+ sayJsonOrRender(args, since, renderSinceReportMarkdown);
67
+ }
68
+ /** The `--days` parse: the explicit flag's value checked against REPORT_MAX_DAYS, or the
69
+ * default series length when absent. Fatal on a value past the shared bound. */
70
+ function reportDays(args) {
54
71
  const daysRaw = flagValue(args, "--days");
55
72
  let days = REPORT_DEFAULT_DAYS;
56
73
  if (daysRaw !== null) {
@@ -61,6 +78,13 @@ export async function cmdReport(root, args, seams = {}) {
61
78
  // rejected 0, non-decimals, and a missing value.
62
79
  days = parseCountFlag("--days", daysRaw, REPORT_MAX_DAYS);
63
80
  }
81
+ return days;
82
+ }
83
+ /** The whole-day report shape: the `-f/--follow` live repaint, the `--failures --json`
84
+ * digest, the plain `--json` payload, or the day Markdown/failure-digest render. Fatal on a
85
+ * `follow && --json` clash (already refused by cmdReport's failOnFollowJson); returns only
86
+ * after printing. */
87
+ async function dayReportShape(root, args, days, role, follow, seams) {
64
88
  // --failures --json prints the collector's own payload (the FailureReportData object), the
65
89
  // report --json and doctor --json precedent: the time-and-spend fold gave the digest a
66
90
  // stable data shape, so the machine-readable form is the data itself, not a re-parse of the
@@ -1,17 +1,23 @@
1
- /** The review gate's five deterministic diff rules, in the gate's own order and scope — the one
2
- * call site both the gate (review.ts) and the main watch (main-watch.ts) use, so a finding reads
3
- * the same on a changed tree and on main's tip. `baseRev` is a commit or branch name the async
1
+ /** The deterministic diff rules the review gate and the main watch share, each run in its own
2
+ * `scope` and order — the one call site both the gate (review.ts) and the main watch
3
+ * (main-watch.ts) use, so a finding reads the same on a changed tree and on main's tip.
4
+ * `baseRev` is a commit or branch name the async
4
5
  * rules resolve with changeBaseRev (merge-base of HEAD and it), so the watch passes a commit
5
- * while the gate passes the main branch. Runs conflictMarkerReason, then protectedEditReason,
6
- * then secretLeakReason, then — only for an exempt (md-only) diff — falseFixReason, then
7
- * backlogStructureReason for every diff, short-circuiting on the first finding exactly as the
8
- * original gate did: a later rule never runs once an earlier one already rejects. Returns a
9
- * one-reason array when a rule fires (the reason the gate rejects on and the watch reports) and
10
- * `[]` when nothing is wrong. An empty `role` (the watch cannot name the tip's merging role) is
11
- * no basis for calling a PRINCIPLES.md edit unauthorized, so the role-dependent half of the
12
- * protected-edit rule is skipped — but its role-independent half, the brief-block rule
13
- * (briefEditReason), still runs, so a brief edit on an unattributed head is still reported; the
14
- * gate always passes a known role, so its rejections are unchanged.
6
+ * while the gate passes the main branch. The final `scope` argument picks which set runs and in
7
+ * what order: `"watch"` runs all five — conflictMarkerReason, then protectedEditReason, then
8
+ * secretLeakReason, then — only for an exempt (md-only) diff — falseFixReason, then
9
+ * backlogStructureReason for every diff — while `"gate"` (the pre-merge gate) runs only the
10
+ * three a later commit cannot repair: briefEditReason, protectedEditReason, then
11
+ * secretLeakReason. A conflict marker, a false fix claim and a broken backlog structure can all
12
+ * be repaired by a later commit, so they stop refusing a diff pre-merge and the main watch
13
+ * reports them on main's tip instead. Both scopes short-circuit on their first finding exactly
14
+ * as the original gate did: a later rule never runs once an earlier one already rejects.
15
+ * Returns a one-reason array when a rule fires (the reason the gate rejects on and the watch
16
+ * reports) and `[]` when nothing is wrong. An empty `role` (the watch cannot name the tip's
17
+ * merging role) is no basis for calling a PRINCIPLES.md edit unauthorized, so the
18
+ * role-dependent half of the protected-edit rule is skipped — but its role-independent half,
19
+ * the brief-block rule (briefEditReason), still runs, so a brief edit on an unattributed head
20
+ * is still reported; the gate always passes a known role, so its rejections are unchanged.
15
21
  *
16
22
  * This module lives apart from review-precheck.ts so the main watch can share the rules without a
17
23
  * runtime import cycle: review-precheck.ts imports src/baseline/main-watch.ts
@@ -25,10 +31,16 @@ import { briefEditReason, protectedEditReason } from "./protected-edits.js";
25
31
  import { secretLeakReason } from "./secret-leaks.js";
26
32
  import { falseFixReason } from "../verdict/fix-claim.js";
27
33
  import { backlogStructureReason } from "../backlog/backlog-structure.js";
28
- export async function diffRuleReasons(wt, baseRev, files, role, exempt) {
29
- const marker = conflictMarkerReason(wt, files);
30
- if (marker !== undefined)
31
- return [marker];
34
+ export async function diffRuleReasons(wt, baseRev, files, role, exempt, scope) {
35
+ // The gate scope rejects only the three findings a later commit cannot repair (a brief-block
36
+ // edit, a protected edit, a leaked credential); the watch scope runs the full set, because a
37
+ // conflict marker, a false fix claim and a broken backlog structure are all repairable and so
38
+ // bare waiting for the main watch to report them on main's tip.
39
+ if (scope === "watch") {
40
+ const marker = conflictMarkerReason(wt, files);
41
+ if (marker !== undefined)
42
+ return [marker];
43
+ }
32
44
  // The brief-block rule is role-independent — its edit is forbidden for every role alike — so it
33
45
  // runs even when the watch cannot name the tip's merging role (role === ""). Only the
34
46
  // PRINCIPLES.md half of protectedEditReason needs a known role to condemn an edit.
@@ -41,13 +53,15 @@ export async function diffRuleReasons(wt, baseRev, files, role, exempt) {
41
53
  const secretReason = await secretLeakReason(wt, baseRev, files);
42
54
  if (secretReason !== undefined)
43
55
  return [secretReason];
44
- if (exempt) {
56
+ if (scope === "watch" && exempt) {
45
57
  const falseFix = await falseFixReason(wt, baseRev, files);
46
58
  if (falseFix)
47
59
  return [falseFix];
48
60
  }
49
- const structure = await backlogStructureReason(wt, baseRev, files);
50
- if (structure)
51
- return [structure];
61
+ if (scope === "watch") {
62
+ const structure = await backlogStructureReason(wt, baseRev, files);
63
+ if (structure)
64
+ return [structure];
65
+ }
52
66
  return [];
53
67
  }
@@ -33,7 +33,9 @@ export function postReviewState(entry) {
33
33
  export const POST_REVIEW_STATES = ["waiting", "failing", "rejected", "fixed"];
34
34
  /** The bracketed status suffix one post-merge review entry carries on every observer surface
35
35
  * (`tumwater reviews`, the dashboard's Post-merge reviews card, and the TUI Pending pane's
36
- * review block): the reviewer's recorded rejected reasons when `rejected` is set; else the
36
+ * review block): the reviewer's recorded rejected reasons when `rejected` is set — or just
37
+ * `[rejected]` when the recorded list is empty, so a rejection with no reasons never renders
38
+ * a dangling `rejected — `; else the
37
39
  * number of verdict-less review runs when `failures` is positive, naming the
38
40
  * last failure's reason beside the count when `lastFailure` is a non-empty string; else `""`.
39
41
  * Reasons are squashed to 60 as `tumwater reviews` has always printed them, and the failure
@@ -44,8 +46,12 @@ export function postReviewStatusLabel(entry) {
44
46
  switch (postReviewState(entry)) {
45
47
  case "fixed":
46
48
  return `[fixed — the repair ${shortSha(entry.fixed)} already merged on main]`;
47
- case "rejected":
48
- return `[rejected — ${entry.rejected.map((r) => squash(r, 60)).join("; ")}]`;
49
+ case "rejected": {
50
+ const reasons = entry.rejected; // postReviewState only says "rejected" when rejected is an array
51
+ return reasons.length === 0
52
+ ? "[rejected]"
53
+ : `[rejected — ${reasons.map((r) => squash(r, 60)).join("; ")}]`;
54
+ }
49
55
  case "failing": {
50
56
  const failures = entry.failures; // postReviewState only says "failing" when failures > 0
51
57
  if (typeof entry.lastFailure === "string" && entry.lastFailure.length > 0) {
@@ -1,7 +1,8 @@
1
1
  import { readJsonFile, writeJsonAtomic } from "../files/json-files.js";
2
+ import { cachedByStat } from "../files/stat-cache.js";
2
3
  import { compareCodeUnits } from "../collections.js";
3
4
  import { postReviewPath, postReviewWorktreePath, nativeLogPath, piLogPath, reviewRunSessionDir, reviewFixRef } from "../paths.js";
4
- import { reviewedAfterMerge, reviewRunConfig } from "../config/config-views.js";
5
+ import { reviewRunConfig } from "../config/config-views.js";
5
6
  import { logEventBestEffort, TUMWATER_SOURCE, tryOrWarn } from "../events/events.js";
6
7
  import { isJsonObject } from "../files/json-object.js";
7
8
  import { git, gitCouldNotStart, gitTry } from "../git/git-run.js";
@@ -11,9 +12,10 @@ import { useWorktree } from "../git/worktree-use.js";
11
12
  import { readPrinciples } from "../prompt/principles.js";
12
13
  import { buildReviewPrompt } from "../prompt/gate-prompts.js";
13
14
  import { shortSha } from "../text/format.js";
15
+ import { DAY_MS } from "../text/datetime.js";
14
16
  import { errorMessage } from "../text/text.js";
15
17
  import { revisionRoundOf } from "../git/commit-message.js";
16
- import { REVISION_BACKSTOP } from "../runner/revision.js";
18
+ import { REVISION_BACKSTOP } from "../runner/runner-revision.js";
17
19
  import { eventRole } from "../events/event-read.js";
18
20
  import { baseRoleOf } from "../roles/runner-ids.js";
19
21
  import { fixLine } from "../verdict/reply-contract.js";
@@ -23,19 +25,18 @@ export const POST_REVIEW_FAILURE_LIMIT = 3;
23
25
  /** The bar a day's post-merge rejections must reach before the doctor's `post-merge churn` row
24
26
  * fires: one post-merge rejection is the tiering's ordinary cost,
25
27
  * two in a day is a role whose already-landed changes keep being turned back — the
26
- * reviewer-fix signal for an operator to narrow `review.afterMerge` or raise the
27
- * authoring bar. */
28
+ * reviewer-fix signal for an operator to raise the authoring bar. */
28
29
  export const POST_MERGE_REJECT_WARN = 2;
29
30
  /** How far back the post-merge-churn rule looks — one day, the 24 h event-window convention
30
31
  * every event-reading doctor row uses. */
31
- export const POST_MERGE_REJECT_WINDOW_MS = 24 * 60 * 60_000;
32
+ export const POST_MERGE_REJECT_WINDOW_MS = DAY_MS;
32
33
  /** Group the `afterMerge: true` `review_rejected` events of `events` by base role — the same
33
34
  * `e.type === "review_rejected" && e.afterMerge === true` test the report fold
34
35
  * (src/report/report-fold.ts `postMergeRejectionsByRole`) applies, so the churn row and the
35
36
  * report cannot disagree on which rejections are post-merge. The key is the event's base role
36
37
  * (`baseRoleOf(eventRole(ev))`), not the raw source: an event's `source` is the runner id
37
- * (`bugfix-2`), and both the row's remedy ("narrow `review.afterMerge`") and the scoped
38
- * report are keyed by base role, so one base role's churn must read as one row. Pre-merge
38
+ * (`bugfix-2`), and both the row's remedy and the scoped report are keyed by base role, so
39
+ * one base role's churn must read as one row. Pre-merge
39
40
  * rejections and other event types contribute nothing. */
40
41
  export function postMergeRejectionCounts(events) {
41
42
  const byRole = new Map();
@@ -60,44 +61,50 @@ export function postMergeChurn(events) {
60
61
  const count = roles.reduce((n, r) => n + r.count, 0);
61
62
  return count >= POST_MERGE_REJECT_WARN ? { count, roles } : null;
62
63
  }
63
- /** Whether `role`'s changes are reviewed after merging instead of before. One home for the
64
- * membership test, config-views.ts's reviewedAfterMerge — the same predicate the review gate,
65
- * the runner's `already_queued` recovery arm and the role view read. */
66
- export function isAfterMergeRole(config, role) {
67
- return reviewedAfterMerge(config, role);
68
- }
64
+ // One stat-cached read of the review queue, keyed by the queue file (merge-queue.ts's readEntry
65
+ // caches its single JSON file the same way): the dashboard's /api/status poll asks this every
66
+ // second, and the file only changes when a merge settles its review — an unchanged file costs
67
+ // one stat per poll instead of a whole-file read plus a JSON.parse on every request. Any write
68
+ // (addPostReview/settlePostReview's tmp+rename) lands a fresh inode and mtime/size, so the same
69
+ // dev/ino/mtime/size check invalidates it exactly like the other polled files; each caller gets
70
+ // a deep-enough copy (the entry objects and their `rejected` arrays) so treating a read result
71
+ // as one's own cannot poison a later poll.
69
72
  /** The queued entries. A torn or hand-edited entry (not an object with a string commit, role and
70
73
  * summary, or a `rejected` that is not an array — a non-string `lastFailure` is dropped like the
71
74
  * other fields) is dropped, and the next write leaves it out. */
75
+ const postReviewCache = new Map();
72
76
  export function readPostReviews(root) {
73
- const saved = readJsonFile(postReviewPath(root));
74
- if (!Array.isArray(saved?.pending))
75
- return [];
76
- const entries = [];
77
- for (const raw of saved.pending) {
78
- if (!isJsonObject(raw))
79
- continue;
80
- const { commit, role, summary, body, failures, lastFailure, rejected, enqueuedAt, fixed } = raw;
81
- if (typeof commit !== "string" || typeof role !== "string" || typeof summary !== "string")
82
- continue;
83
- if (rejected !== undefined && !Array.isArray(rejected))
84
- continue;
85
- const entry = { commit, role, summary };
86
- if (typeof body === "string")
87
- entry.body = body;
88
- if (typeof failures === "number" && Number.isFinite(failures))
89
- entry.failures = failures;
90
- if (typeof lastFailure === "string")
91
- entry.lastFailure = lastFailure;
92
- if (Array.isArray(rejected))
93
- entry.rejected = rejected.filter((r) => typeof r === "string");
94
- if (typeof fixed === "string")
95
- entry.fixed = fixed;
96
- if (typeof enqueuedAt === "number" && Number.isFinite(enqueuedAt) && enqueuedAt >= 0)
97
- entry.enqueuedAt = enqueuedAt;
98
- entries.push(entry);
99
- }
100
- return entries;
77
+ const file = postReviewPath(root);
78
+ return (cachedByStat(postReviewCache, file, file, () => {
79
+ const saved = readJsonFile(file);
80
+ if (!Array.isArray(saved?.pending))
81
+ return [];
82
+ const entries = [];
83
+ for (const raw of saved.pending) {
84
+ if (!isJsonObject(raw))
85
+ continue;
86
+ const { commit, role, summary, body, failures, lastFailure, rejected, enqueuedAt, fixed } = raw;
87
+ if (typeof commit !== "string" || typeof role !== "string" || typeof summary !== "string")
88
+ continue;
89
+ if (rejected !== undefined && !Array.isArray(rejected))
90
+ continue;
91
+ const entry = { commit, role, summary };
92
+ if (typeof body === "string")
93
+ entry.body = body;
94
+ if (typeof failures === "number" && Number.isFinite(failures))
95
+ entry.failures = failures;
96
+ if (typeof lastFailure === "string")
97
+ entry.lastFailure = lastFailure;
98
+ if (Array.isArray(rejected))
99
+ entry.rejected = rejected.filter((r) => typeof r === "string");
100
+ if (typeof fixed === "string")
101
+ entry.fixed = fixed;
102
+ if (typeof enqueuedAt === "number" && Number.isFinite(enqueuedAt) && enqueuedAt >= 0)
103
+ entry.enqueuedAt = enqueuedAt;
104
+ entries.push(entry);
105
+ }
106
+ return entries;
107
+ }, (entries) => entries.map((e) => ({ ...e, ...(e.rejected !== undefined ? { rejected: [...e.rejected] } : {}) }))) ?? []);
101
108
  }
102
109
  function writePostReviews(root, pending) {
103
110
  tryOrWarn(root, TUMWATER_SOURCE, "write post-review.json", () => writeJsonAtomic(postReviewPath(root), { pending }));
@@ -7,7 +7,7 @@
7
7
  import { warnEventBestEffort } from "../events/events.js";
8
8
  import { BUILD_CHECK_TIMEOUT_MS } from "../build/build-check-detect.js";
9
9
  import { runScopedBuildCheck } from "../build/build-check-scoped.js";
10
- import { checkFailureReasons, describeCheck, failureHeadline, FAILING_TESTS_MAX } from "../build/build-check-report.js";
10
+ import { checkFailureReasons, FAILING_TESTS_MAX, redCheckHeadline } from "../build/build-check-report.js";
11
11
  import { sleptPhrase, UNVERIFIED_TREE, unverifiedTreeOutcome } from "../build/build-check-events.js";
12
12
  import { sampleSleepClock } from "../build/host-sleep.js";
13
13
  import { recordReview } from "../run/run-apply.js";
@@ -56,8 +56,18 @@ export async function gateBuildPrecheck(ctx, state, head, reject) {
56
56
  // The gate's one check invocation — the pre-check and its one flake re-run below are the
57
57
  // same run in the same scope, worktree, and config with the same resolved timeout, so the
58
58
  // scope and timeout resolution live here once and cannot drift between the two calls.
59
- const runGateCheck = () => runScopedBuildCheck(root, role, "gate", wt, config, ctx.buildCheckTimeoutMs ?? BUILD_CHECK_TIMEOUT_MS, ctx.sampleSleep ?? sampleSleepClock, undefined, checkWaitStage(root, [role]));
59
+ const runGateCheck = () => runScopedBuildCheck(root, role, "gate", wt, config, ctx.buildCheckTimeoutMs ?? BUILD_CHECK_TIMEOUT_MS, ctx.sampleSleep ?? sampleSleepClock, undefined, checkWaitStage(root, [role]), undefined, ctx.signal);
60
60
  const preCheck = await runGateCheck();
61
+ // A check the harness stopped on the verify's abort (a user abort or a shutdown) is not the
62
+ // tree's verdict and not a flake: fail closed as aborted before the re-run, the quarantine
63
+ // and the attribution — the signal fires MID-check (runScriptGroup returns an `aborted`
64
+ // skip), and `reviewPinnedChange` settles the verify `aborted` on this result so an abort
65
+ // never has to wait a parked check out. A signal already aborted before the check started is
66
+ // caught by the `aborted` checks in the attribution path below (the check ran and reached
67
+ // its own verdict), so the pre-existing shutdown case keeps its two attempts.
68
+ if (preCheck?.outcome.skipReason === "aborted") {
69
+ return { resolved: { decision: "failed", aborted: true } };
70
+ }
61
71
  if (preCheck) {
62
72
  const { check } = preCheck;
63
73
  let { outcome } = preCheck;
@@ -81,7 +91,7 @@ export async function gateBuildPrecheck(ctx, state, head, reject) {
81
91
  warnEventBestEffort(root, role, sleptPhrase("gate check", outcome.run.sleptMs, "then passed on retry"));
82
92
  }
83
93
  else {
84
- const flaky = failureHeadline(outcome.outputTail) ?? describeCheck(check);
94
+ const flaky = redCheckHeadline(check, outcome.outputTail);
85
95
  warnEventBestEffort(root, role, `${FLAKY_WARNING_PREFIX}${flaky}`);
86
96
  // Each test the failed run named failed and then passed on this same tree: quarantined.
87
97
  quarantineTests(root, outcome.failingTests, outcome.outputTail);
@@ -112,7 +122,7 @@ export async function gateBuildPrecheck(ctx, state, head, reject) {
112
122
  // instead of ending the merge attempt as review_error and paying a requeue. It carries no
113
123
  // verifiedHead — the tree was not verified — so the merge
114
124
  // path's own check still judges the tree before it reaches main.
115
- const flaky = failureHeadline(outcome.outputTail) ?? describeCheck(check);
125
+ const flaky = redCheckHeadline(check, outcome.outputTail);
116
126
  // A failure that names only tests main already fails is main's, not this change's: it goes
117
127
  // on to review with no fresh run on main. No verifiedHead — the tree did not pass, so the
118
128
  // merge path must not seed a green for it — and the merge check judges it by the same rule.