tumwater 0.4.0 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (291) hide show
  1. package/README.md +4 -6
  2. package/dist/build-info.json +2 -2
  3. package/dist/src/agent/agent-backend.js +5 -2
  4. package/dist/src/agent/backend-traits.js +21 -0
  5. package/dist/src/agent/claude/claude-backend.js +1 -1
  6. package/dist/src/agent/claude/claude-stream.js +181 -182
  7. package/dist/src/agent/claude/{write-guard-hook.js → claude-write-guard-hook.js} +1 -1
  8. package/dist/src/agent/claude/claude-write-guard.js +5 -4
  9. package/dist/src/agent/codex/codex-backend.js +1 -1
  10. package/dist/src/agent/codex/codex-rollout.js +48 -45
  11. package/dist/src/agent/codex/codex-stream.js +269 -215
  12. package/dist/src/agent/codex/{write-guard-hook.js → codex-write-guard-hook.js} +2 -1
  13. package/dist/src/agent/codex/codex-write-guard.js +3 -3
  14. package/dist/src/agent/image-input.js +18 -87
  15. package/dist/src/agent/notes-shim.js +2 -2
  16. package/dist/src/agent/opencode/opencode-backend.js +2 -2
  17. package/dist/src/agent/opencode/opencode-catalog.js +2 -2
  18. package/dist/src/agent/opencode/opencode-config.js +2 -2
  19. package/dist/src/agent/opencode/opencode-stream.js +101 -92
  20. package/dist/src/agent/run-agent.js +76 -382
  21. package/dist/src/agent/run-settle.js +25 -15
  22. package/dist/src/agent/run-spawn.js +226 -0
  23. package/dist/src/agent/run-stream.js +185 -0
  24. package/dist/src/agent/session-ledger.js +16 -0
  25. package/dist/src/agent/write-guard.js +5 -3
  26. package/dist/src/backlog/backlog-archive.js +86 -170
  27. package/dist/src/backlog/backlog-cli-view.js +89 -70
  28. package/dist/src/backlog/backlog-eligibility.js +51 -59
  29. package/dist/src/backlog/backlog-file-bugs.js +133 -0
  30. package/dist/src/backlog/backlog-index-data.js +159 -0
  31. package/dist/src/backlog/backlog-index.js +7 -5
  32. package/dist/src/backlog/backlog-md.js +39 -28
  33. package/dist/src/backlog/backlog-release.js +21 -36
  34. package/dist/src/backlog/backlog-render.js +1 -1
  35. package/dist/src/backlog/backlog-structure.js +14 -145
  36. package/dist/src/backlog/backlog-write.js +31 -18
  37. package/dist/src/backlog/backlog.js +14 -4
  38. package/dist/src/backlog/question-answer.js +33 -15
  39. package/dist/src/baseline/main-baseline.js +71 -26
  40. package/dist/src/baseline/main-guardrails.js +76 -15
  41. package/dist/src/baseline/main-red.js +45 -94
  42. package/dist/src/baseline/main-watch.js +9 -3
  43. package/dist/src/brief/brief.js +40 -0
  44. package/dist/src/budget/budget-fallback-view.js +50 -8
  45. package/dist/src/budget/budget-wiring.js +15 -11
  46. package/dist/src/budget/budget.js +3 -2
  47. package/dist/src/build/build-check-events.js +4 -2
  48. package/dist/src/build/build-check-report.js +2 -1
  49. package/dist/src/build/build-check-scoped.js +70 -41
  50. package/dist/src/change/change-data.js +4 -2
  51. package/dist/src/cli/cli-args.js +9 -0
  52. package/dist/src/cli/cli-command-args.js +199 -98
  53. package/dist/src/cli/cli-flag-specs.js +6 -4
  54. package/dist/src/cli/cli-marker-commands.js +2 -2
  55. package/dist/src/cli/cli-query-commands.js +41 -6
  56. package/dist/src/cli/cli-review-commands.js +2 -1
  57. package/dist/src/cli/cli-role-command.js +46 -92
  58. package/dist/src/cli/cli-start.js +123 -47
  59. package/dist/src/cli/follow-view.js +3 -1
  60. package/dist/src/cli/help.js +19 -12
  61. package/dist/src/cli/log-commands.js +128 -107
  62. package/dist/src/cli/question-commands.js +82 -63
  63. package/dist/src/cli.js +396 -343
  64. package/dist/src/collections.js +46 -9
  65. package/dist/src/config/config-fallbacks.js +0 -4
  66. package/dist/src/config/config-live.js +179 -147
  67. package/dist/src/config/config-request.js +176 -51
  68. package/dist/src/config/config-validation-roles.js +271 -0
  69. package/dist/src/config/config-validation.js +10 -205
  70. package/dist/src/config/config-views.js +2 -7
  71. package/dist/src/config/config-write.js +205 -175
  72. package/dist/src/config/config.js +2 -1
  73. package/dist/src/doctor/doctor-backlog-bugs.js +24 -122
  74. package/dist/src/doctor/doctor-backlog-plans.js +24 -25
  75. package/dist/src/doctor/doctor-backlog.js +34 -42
  76. package/dist/src/doctor/doctor-check-names.js +4 -2
  77. package/dist/src/doctor/doctor-checks.js +16 -4
  78. package/dist/src/doctor/doctor-claude-checks.js +2 -2
  79. package/dist/src/doctor/doctor-codex-checks.js +9 -20
  80. package/dist/src/doctor/doctor-contention-checks.js +52 -19
  81. package/dist/src/doctor/doctor-holds.js +22 -20
  82. package/dist/src/doctor/doctor-model-checks.js +8 -184
  83. package/dist/src/doctor/doctor-notify.js +2 -5
  84. package/dist/src/doctor/doctor-opencode-checks.js +90 -41
  85. package/dist/src/doctor/doctor-principles.js +26 -0
  86. package/dist/src/doctor/doctor-project-checks.js +69 -19
  87. package/dist/src/doctor/doctor-role-notes.js +33 -0
  88. package/dist/src/doctor/doctor.js +13 -6
  89. package/dist/src/doctor/probe.js +1 -1
  90. package/dist/src/events/event-format.js +307 -162
  91. package/dist/src/events/event-labels.js +0 -1
  92. package/dist/src/events/event-legacy.js +1 -1
  93. package/dist/src/events/event-window.js +1 -1
  94. package/dist/src/events/events.js +15 -2
  95. package/dist/src/events/notify.js +1 -1
  96. package/dist/src/failure/failure-cluster.js +4 -3
  97. package/dist/src/failure/failure-data.js +126 -94
  98. package/dist/src/failure/failure-render.js +58 -17
  99. package/dist/src/failure/failure-state-change.js +308 -27
  100. package/dist/src/failure/time-spend.js +139 -106
  101. package/dist/src/files/files.js +7 -0
  102. package/dist/src/files/json-object.js +15 -2
  103. package/dist/src/gates/budget-gates.js +90 -68
  104. package/dist/src/gates/gate-polls.js +92 -61
  105. package/dist/src/gates/role-cap-gates.js +1 -1
  106. package/dist/src/gates/startup-gate.js +28 -28
  107. package/dist/src/gates/streak-gate.js +1 -1
  108. package/dist/src/git/git.js +1 -1
  109. package/dist/src/git/slot-salvage.js +9 -9
  110. package/dist/src/git/slots-state.js +12 -3
  111. package/dist/src/git/worktree-pool-startup.js +127 -0
  112. package/dist/src/git/worktree-pool.js +1 -120
  113. package/dist/src/gui/gui-admin-endpoints.js +3 -4
  114. package/dist/src/gui/gui-args.js +1 -1
  115. package/dist/src/gui/gui-backlog-commands.js +20 -11
  116. package/dist/src/gui/gui-backlog-endpoints.js +2 -8
  117. package/dist/src/gui/gui-config-commands.js +51 -76
  118. package/dist/src/gui/gui-endpoint-commands.js +4 -2
  119. package/dist/src/gui/gui-endpoint-common.js +4 -3
  120. package/dist/src/gui/gui-history-endpoints.js +36 -27
  121. package/dist/src/gui/gui-operator-commands.js +43 -17
  122. package/dist/src/gui/gui-prompt-commands.js +27 -21
  123. package/dist/src/gui/gui-quarantine-commands.js +30 -0
  124. package/dist/src/gui/gui-report-endpoints.js +13 -9
  125. package/dist/src/gui/gui-server.js +5 -3
  126. package/dist/src/gui/gui-team-commands.js +1 -1
  127. package/dist/src/history/history-data.js +7 -1
  128. package/dist/src/history/history-render.js +15 -10
  129. package/dist/src/history/history.js +103 -68
  130. package/dist/src/inbox/inbox-submit.js +1 -5
  131. package/dist/src/inbox/pending-prompt.js +3 -2
  132. package/dist/src/inbox/prompt-commands.js +194 -161
  133. package/dist/src/init/init.js +104 -25
  134. package/dist/src/merge/merge-core.js +1 -1
  135. package/dist/src/merge/merge-drain.js +1 -1
  136. package/dist/src/merge/merge-git.js +5 -5
  137. package/dist/src/merge/{merge-post-review.js → merge-post-review-drain.js} +1 -1
  138. package/dist/src/merge/merge-queue.js +2 -1
  139. package/dist/src/merge/merge-to-main.js +1 -1
  140. package/dist/src/merge/merge-verify.js +3 -2
  141. package/dist/src/operator/operator-commands.js +84 -52
  142. package/dist/src/operator/operator-intent.js +26 -3
  143. package/dist/src/operator/retire.js +2 -1
  144. package/dist/src/operator/revert.js +18 -12
  145. package/dist/src/orchestrator/orchestrator-poll.js +2 -2
  146. package/dist/src/orchestrator/orchestrator.js +1 -1
  147. package/dist/src/orchestrator/retention.js +6 -5
  148. package/dist/src/paths.js +10 -10
  149. package/dist/src/pi/pi-bin.js +2 -2
  150. package/dist/src/pi/pi-models.js +9 -0
  151. package/dist/src/pi/pi-stream.js +63 -41
  152. package/dist/src/pi/pi-watchdogs.js +84 -49
  153. package/dist/src/pi-extension/context-shake.js +55 -31
  154. package/dist/src/pi-extension/full-output.js +2 -3
  155. package/dist/src/process/process.js +24 -1
  156. package/dist/src/process/run-marker.js +2 -2
  157. package/dist/src/process/supervisor.js +24 -5
  158. package/dist/src/prompt/gate-notes.js +41 -16
  159. package/dist/src/prompt/notes-backlog.js +22 -29
  160. package/dist/src/prompt/principles.js +11 -0
  161. package/dist/src/prompt/prompt-followup.js +4 -12
  162. package/dist/src/prompt/prompt.js +15 -12
  163. package/dist/src/redeploy/redeploy-probes.js +11 -1
  164. package/dist/src/redeploy/redeployer.js +54 -7
  165. package/dist/src/redeploy/self-reload.js +7 -2
  166. package/dist/src/report/report-data.js +224 -320
  167. package/dist/src/report/report-fold.js +23 -231
  168. package/dist/src/report/report-main-red.js +70 -3
  169. package/dist/src/report/report-render.js +181 -48
  170. package/dist/src/report/report-since-data.js +218 -0
  171. package/dist/src/report/report-usage-fold.js +288 -0
  172. package/dist/src/report/report.js +2 -1
  173. package/dist/src/review/known-flakes.js +22 -3
  174. package/dist/src/review/post-merge-review-accept.js +3 -2
  175. package/dist/src/review/post-merge-review.js +16 -14
  176. package/dist/src/review/quarantine-render.js +29 -11
  177. package/dist/src/review/review-precheck.js +2 -1
  178. package/dist/src/roles/role-catalog.js +30 -29
  179. package/dist/src/roles/role-guidance.js +26 -43
  180. package/dist/src/roles/role-render.js +19 -0
  181. package/dist/src/roles/role-view.js +19 -5
  182. package/dist/src/roles/roles.js +2 -22
  183. package/dist/src/run/ci-parity.js +5 -6
  184. package/dist/src/run/feedback-filings.js +10 -1
  185. package/dist/src/run/qa-coverage.js +4 -4
  186. package/dist/src/run/run-apply.js +14 -2
  187. package/dist/src/run/run-detail-data.js +50 -10
  188. package/dist/src/run/run-detail-render.js +32 -1
  189. package/dist/src/run/run-detail.js +44 -22
  190. package/dist/src/run/run-dismiss.js +43 -0
  191. package/dist/src/run/run-finalize.js +75 -46
  192. package/dist/src/run/run-prompt.js +271 -187
  193. package/dist/src/run/run-resume.js +32 -31
  194. package/dist/src/run/run-stage.js +116 -64
  195. package/dist/src/run/run-timing.js +18 -4
  196. package/dist/src/run/run-verdict.js +119 -83
  197. package/dist/src/run/stage-backlog-checks.js +4 -31
  198. package/dist/src/run/stage-check.js +4 -12
  199. package/dist/src/run/stage-checks-shared.js +15 -0
  200. package/dist/src/run/stage-name-checks.js +3 -13
  201. package/dist/src/run/stage-path-checks.js +26 -29
  202. package/dist/src/runner/leftover.js +13 -4
  203. package/dist/src/runner/revision.js +2 -2
  204. package/dist/src/runner/runner-fallback.js +1 -1
  205. package/dist/src/runner/runner-outcome.js +0 -5
  206. package/dist/src/runner/runner-pi.js +72 -49
  207. package/dist/src/runner/runner-run.js +19 -102
  208. package/dist/src/runner/runner-state-load.js +0 -10
  209. package/dist/src/runner/runner.js +1 -32
  210. package/dist/src/scheduling/claims.js +4 -11
  211. package/dist/src/status/status-data.js +140 -91
  212. package/dist/src/status/status-polls.js +41 -5
  213. package/dist/src/status/status-role-notes.js +53 -0
  214. package/dist/src/status/status-roles.js +10 -37
  215. package/dist/src/text/datetime.js +8 -1
  216. package/dist/src/text/format.js +13 -1
  217. package/dist/src/text/phrases.js +16 -2
  218. package/dist/src/text/text.js +12 -0
  219. package/dist/src/ui/alert-feeds.js +115 -0
  220. package/dist/src/ui/badges.js +3 -4
  221. package/dist/src/ui/gui/gui-client-boot.js +78 -57
  222. package/dist/src/ui/gui/gui-client-drawer-config.js +8 -3
  223. package/dist/src/ui/gui/gui-client-drawer.js +18 -2
  224. package/dist/src/ui/gui/gui-client-feed.js +3 -8
  225. package/dist/src/ui/gui/gui-client-history.js +16 -16
  226. package/dist/src/ui/gui/gui-client-model.js +9 -9
  227. package/dist/src/ui/gui/gui-client-operator.js +6 -10
  228. package/dist/src/ui/gui/gui-client-pending.js +4 -4
  229. package/dist/src/ui/gui/gui-client-report-chart.js +111 -10
  230. package/dist/src/ui/gui/gui-client-report.js +70 -11
  231. package/dist/src/ui/gui/gui-client-roles.js +52 -12
  232. package/dist/src/ui/gui/gui-client-settings.js +31 -26
  233. package/dist/src/ui/gui/gui-client-team.js +28 -2
  234. package/dist/src/ui/gui/gui-client.js +33 -4
  235. package/dist/src/ui/gui/gui-icons.js +7 -1
  236. package/dist/src/ui/progress-data.js +2 -2
  237. package/dist/src/ui/run-progress-model.js +2 -2
  238. package/dist/src/ui/status-model.js +26 -16
  239. package/dist/src/ui/status-payload.js +149 -326
  240. package/dist/src/ui/status-render.js +131 -80
  241. package/dist/src/ui/team-alert-backlog.js +236 -0
  242. package/dist/src/ui/team-alert-shared.js +92 -0
  243. package/dist/src/ui/team-alert-status.js +326 -0
  244. package/dist/src/ui/team-alert-tail.js +238 -0
  245. package/dist/src/ui/team-alerts.js +16 -984
  246. package/dist/src/ui/transcript-tail.js +137 -94
  247. package/dist/src/ui/tui/tui-backlog.js +1 -1
  248. package/dist/src/ui/tui/tui-editor-answer.js +54 -44
  249. package/dist/src/ui/tui/tui-editor-attach.js +74 -63
  250. package/dist/src/ui/tui/tui-editor-backends.js +62 -56
  251. package/dist/src/ui/tui/tui-editor-brief.js +64 -54
  252. package/dist/src/ui/tui/tui-editor-budget.js +106 -89
  253. package/dist/src/ui/tui/tui-editor-config.js +2 -4
  254. package/dist/src/ui/tui/tui-editor-ctx.js +12 -1
  255. package/dist/src/ui/tui/tui-editor-entry.js +171 -153
  256. package/dist/src/ui/tui/tui-editor-instances.js +2 -1
  257. package/dist/src/ui/tui/tui-editor-instructions.js +50 -107
  258. package/dist/src/ui/tui/tui-editor-interval.js +39 -112
  259. package/dist/src/ui/tui/tui-editor-notes.js +28 -84
  260. package/dist/src/ui/tui/tui-editor-pause.js +210 -183
  261. package/dist/src/ui/tui/tui-editor-prompt.js +46 -35
  262. package/dist/src/ui/tui/tui-editor-queued.js +74 -64
  263. package/dist/src/ui/tui/tui-editor-quiet.js +122 -108
  264. package/dist/src/ui/tui/tui-editor-release.js +85 -0
  265. package/dist/src/ui/tui/tui-editor-retire.js +57 -46
  266. package/dist/src/ui/tui/tui-editor-role-enabled.js +90 -80
  267. package/dist/src/ui/tui/tui-editor-role-file.js +87 -0
  268. package/dist/src/ui/tui/tui-editor-role-key.js +50 -42
  269. package/dist/src/ui/tui/tui-editor-search.js +70 -58
  270. package/dist/src/ui/tui/tui-editor-session.js +3 -0
  271. package/dist/src/ui/tui/tui-editors.js +4 -1
  272. package/dist/src/ui/tui/tui-frame.js +7 -3
  273. package/dist/src/ui/tui/tui-history.js +12 -10
  274. package/dist/src/ui/tui/tui-keys.js +6 -1
  275. package/dist/src/ui/tui/tui-pane-activity.js +62 -50
  276. package/dist/src/ui/tui/tui-pane-backlog.js +4 -4
  277. package/dist/src/ui/tui/tui-pane-ctx.js +1 -1
  278. package/dist/src/ui/tui/tui-pane-health.js +45 -26
  279. package/dist/src/ui/tui/tui-pane-history.js +296 -264
  280. package/dist/src/ui/tui/tui-pane-report.js +74 -57
  281. package/dist/src/ui/tui/tui-pane-session.js +6 -2
  282. package/dist/src/ui/tui/tui-pane-transcript.js +158 -131
  283. package/dist/src/ui/tui/tui-pane.js +201 -174
  284. package/dist/src/ui/tui/tui-panes.js +211 -148
  285. package/dist/src/ui/tui/tui.js +4 -4
  286. package/dist/src/verdict/fix-claim.js +18 -6
  287. package/dist/src/verdict/refusal.js +32 -7
  288. package/package.json +1 -1
  289. package/dist/src/doctor/image-probe.js +0 -257
  290. /package/dist/src/gui/{http-body.js → gui-http-body.js} +0 -0
  291. /package/dist/src/merge/{backlog-conflicts.js → merge-backlog-conflicts.js} +0 -0
@@ -1,239 +1,23 @@
1
- /** Event-log folding for the usage report: turn raw harness events (run_end/merged/…) into
2
- * per-day and whole-window `UsageFold` accumulators, memoizing the day-keyed read per
3
- * (root, fromKey) so a dashboard polling /api/report only folds appended lines instead of
4
- * re-reading the window. The two window collectors (collectReport / collectReportSince) in
5
- * src/report/report-data.ts fold through this one shared layer, so the aggregation rules the
6
- * --since and day windows present cannot drift apart; it lives here matching its own test
7
- * (test/report-fold-cache.test.ts). Zero incidental runtime dependencies: node built-ins and
8
- * the app's own modules only. */
1
+ /** The windowed event-log fold backing the usage report: read the harness's event log over a
2
+ * local-day window, memoize the day-keyed read per (root, fromKey) so a dashboard polling
3
+ * /api/report only folds appended lines instead of re-reading the window, and group events
4
+ * into per-day `DayFold` accumulators plus the base-role scoping helpers the day and failure
5
+ * reports share. The two window collectors — collectReport (src/report/report-data.ts) and
6
+ * collectReportSince (src/report/report-since-data.ts) — fold through this layer, each parsed
7
+ * event through the shared single-event core in src/report/report-usage-fold.ts
8
+ * (`foldUsageEvent`), so the --since and day windows present cannot drift apart; it lives here
9
+ * matching its own test (test/report-fold-cache.test.ts). Zero incidental runtime
10
+ * dependencies: node built-ins and the app's own modules only. */
9
11
  import { sameStatIdentity, statIdentityOrZero, statOrNull } from "../files/files.js";
10
12
  import { readWindowEvents } from "../events/event-window.js";
11
- import { eventDayKey, eventRole, eventUsage, parseEventLine } from "../events/event-read.js";
13
+ import { eventDayKey, parseEventLine } from "../events/event-read.js";
12
14
  import { readCompleteLines } from "../files/tail.js";
13
15
  import { eventsLogPath } from "../paths.js";
14
- import { isPassingFullCheck, isRedFullCheck } from "../build/build-check-events.js";
15
16
  import { isPassingMainCheck, isRedMain } from "../baseline/main-guardrails.js";
16
- import { addTo, memoSetLru } from "../collections.js";
17
+ import { memoSetLru } from "../collections.js";
17
18
  import { asList } from "../text/text.js";
18
- import { finiteNumber } from "../files/json-object.js";
19
- import { OUTCOME_CLASS } from "../failure/time-spend.js";
20
19
  import { baseRoleOf } from "../roles/runner-ids.js";
21
- import { commitTier } from "../roles/roles.js";
22
- /** Fold one event's `providerCost` map (`Record<provider, {costUsd, notionalCostUsd}>` — the
23
- * shape run-finalize.ts's run_end and merge-slot.ts's merge_done/merge_failed emit) into the
24
- * per-provider tallies, one addTo per provider per nonzero figure so zero stays absent. A
25
- * missing, foreign, or empty map folds nothing, so old logs render byte-identically. */
26
- function foldProviderCost(target, providerCost) {
27
- if (typeof providerCost !== "object" || providerCost === null)
28
- return;
29
- for (const [provider, c] of Object.entries(providerCost)) {
30
- if (typeof c !== "object" || c === null)
31
- continue;
32
- const entry = c;
33
- const costUsd = finiteNumber(entry.costUsd, 0);
34
- const notionalCostUsd = finiteNumber(entry.notionalCostUsd, 0);
35
- if (costUsd !== 0)
36
- addTo(target.costByProvider, provider, costUsd);
37
- if (notionalCostUsd !== 0)
38
- addTo(target.notionalByProvider, provider, notionalCostUsd);
39
- }
40
- }
41
- /** Push `ms` into the accumulator list a day median reads, when it is positive: the event
42
- * fields that feed these medians are missing or 0 for the same event kinds (short runs, build
43
- * pre-checks, precedent legacy events, probe-seam waits), and those contribute nothing — a
44
- * median over only real durations/wait times. Every median list folds through here, so the
45
- * "positive-only" rule cannot drift between fields. */
46
- function pushPositiveMs(list, ms) {
47
- if (ms > 0)
48
- list.push(ms);
49
- }
50
- /** Fold one event into a usage accumulator: run_end events add a run (per role and, when
51
- * the target counts runs, in total), their output tokens, and their cost — zero-cost runs
52
- * contribute nothing, so they leave no costByRole key, keeping the render's "$0 roles are
53
- * omitted" rule true on the aggregation itself, not just at display time; merged events add
54
- * a commit; merge_done/merge_failed events add a merge run and
55
- * accumulate their tokens/cost into the merge fields only — costByRole stays a runs-only
56
- * breakdown, and the merge line is how the reviewer's share is named. Both collectors fold
57
- * through this one place, so the two windows' aggregation rules cannot drift apart. */
58
- export function foldUsageEvent(target, ev) {
59
- if (ev.type === "run_end") {
60
- const role = eventRole(ev);
61
- addTo(target.runsByRole, role, 1);
62
- // Error-class runs get their own parallel tally — the day report's ``Failed runs by role``
63
- // line — reusing the digest's OUTCOME_CLASS so "failed" cannot drift from the failure
64
- // report's meaning. Never subtracted from runsByRole; the two are independent counts.
65
- if (OUTCOME_CLASS[ev.result] === "error")
66
- addTo(target.failedRunsByRole, role, 1);
67
- // A refusal is its own deliberate outcome (src/verdict/refusal.ts), not error-class, so
68
- // it needs an explicit result check: the one per-role rejection-without-review count a
69
- // day report should surface beside rejections, revisions and falls. Never subtracted from
70
- // runsByRole; a refused run still counts as a run.
71
- if (ev.result === "refused")
72
- addTo(target.refusalsByRole, role, 1);
73
- if (target.runs !== undefined)
74
- target.runs++;
75
- const { tokens, costUsd, notionalCostUsd } = eventUsage(ev);
76
- pushPositiveMs(target.runDurationsMs, finiteNumber(ev.durationMs, 0));
77
- addTo(target.tokensByRole, role, tokens);
78
- target.tokensOut += tokens;
79
- target.costUsd += costUsd;
80
- target.notionalCostUsd += notionalCostUsd;
81
- if (costUsd !== 0)
82
- addTo(target.costByRole, role, costUsd);
83
- if (notionalCostUsd !== 0)
84
- addTo(target.notionalByRole, role, notionalCostUsd);
85
- // The notional spend's backend (plans/codex-backend.md part 3d/9): the subscription seam
86
- // whose runs made the run's API-equivalent cost, from the run_end's `agent` (run-finalize
87
- // carries it when the run reported billing/rate windows) — falling back to "claude" when
88
- // a legacy window event (no agent, subscription billing) attributed its spend, so the old
89
- // default stays the report's only backend. A run with no backend and no subscription
90
- // billing is unattributed and adds no key, so the render keeps its generic line for it.
91
- const notionalBackend = typeof ev.agent === "string" && ev.agent !== ""
92
- ? ev.agent
93
- : ev.billing === "subscription"
94
- ? "claude"
95
- : undefined;
96
- if (notionalBackend !== undefined && notionalCostUsd !== 0)
97
- addTo(target.notionalByBackend, notionalBackend, notionalCostUsd);
98
- // The agent runs' per-provider spend (run-finalize.ts emits providerCost on run_end)
99
- // — folded per provider beside the per-role maps, so a mixed team's `Cost by provider`
100
- // line names the expensive seam.
101
- foldProviderCost(target, ev.providerCost);
102
- }
103
- else if (ev.type === "review_rejected") {
104
- // One rejection per event, on the day it happened, per role. Zero-means-absent: a role
105
- // that never rejected stays absent, like costByRole.
106
- addTo(target.rejectionsByRole, eventRole(ev), 1);
107
- // A rejection that carried afterMerge: true is a post-merge one — the commit had already
108
- // landed, so the reviewer's turn-back reaches the revert path off main. Kept as its own
109
- // subset of rejectionsByRole because it is a different, costlier kind of rejection.
110
- if (ev.afterMerge === true)
111
- addTo(target.postMergeRejectionsByRole, eventRole(ev), 1);
112
- // A rejection that carried a positive durationMs means a reviewer actually ran the gate
113
- // and reached a verdict (post-merge rejections do; the build pre-check contributes no
114
- // duration), so it joins the review-duration median beside approvals. durationMs: 0 and
115
- // a duration-less event contribute nothing.
116
- pushPositiveMs(target.reviewDurationsMs, finiteNumber(ev.durationMs, 0));
117
- }
118
- else if (ev.type === "review_verdict") {
119
- // An approval that reached a verdict. Every reviewer run that reached a verdict, approval
120
- // or rejection, is one entry in the review-duration median; review_failed stays out — it
121
- // has no verdict. Mirrors the rejection branch above, on the same-day count.
122
- pushPositiveMs(target.reviewDurationsMs, finiteNumber(ev.durationMs, 0));
123
- }
124
- else if (ev.type === "revision") {
125
- // One rework round per event, on the day it happened, per role. Zero-means-absent: a role
126
- // that never reworked a rejected change stays absent, like rejectionsByRole.
127
- addTo(target.revisionsByRole, eventRole(ev), 1);
128
- }
129
- else if (isPassingFullCheck(ev)) {
130
- // The merge slot's passing full-suite checks, for the day report's check-time median.
131
- pushPositiveMs(target.checkDurationsMs, finiteNumber(ev.durationMs, 0));
132
- }
133
- else if (isRedFullCheck(ev)) {
134
- // The merge slot's red full-suite checks, for the day report's red-check count.
135
- target.checkFailures++;
136
- }
137
- else if (ev.type === "model_fallback_started") {
138
- // One episode per start event; durations are not summed (an episode can span the window edge).
139
- addTo(target.fallbacksByRole, eventRole(ev), 1);
140
- }
141
- else if (ev.type === "image_reroute") {
142
- // One reroute per event, preflight or after a blind read.
143
- addTo(target.imageReroutesByRole, eventRole(ev), 1);
144
- }
145
- else if (ev.type === "run_deferred") {
146
- // One deferral per event, a due maintenance run that stood down. Bucket by the raw
147
- // reason code ("backlog", "plans-full", else "no-work" for a missing/foreign/torn
148
- // reason — the same fallback deferredReasonText applies in the feed), so the collector
149
- // stays free of a presentation import and the render prints the code the event carried.
150
- // Team-only: a reason is not a role.
151
- const reason = ev.reason === "backlog" || ev.reason === "plans-full" ? ev.reason : "no-work";
152
- addTo(target.deferredByReason, reason, 1);
153
- }
154
- else if (ev.type === "merged") {
155
- target.commits++;
156
- // Work ratio 4/4: split merged commits by base role and by tier. Instance suffixes fold
157
- // into their role (`feature-2` counts as feature) so the split does not fragment.
158
- const role = baseRoleOf(eventRole(ev));
159
- addTo(target.commitsByRole, role, 1);
160
- const tier = commitTier(role);
161
- if (tier === "work")
162
- target.workCommits++;
163
- else if (tier === "maintenance")
164
- target.maintenanceCommits++;
165
- }
166
- else if (ev.type === "run_start") {
167
- // One tally per run start on a configured model: the same `model` field the CLI's
168
- // `run #N started on <model>` line prints, and the only event that carries a model
169
- // (run_end has none), so a run that started with no configured model contributes no key.
170
- const model = typeof ev.model === "string" && ev.model.length > 0 ? ev.model : null;
171
- if (model !== null)
172
- addTo(target.runsByModel, model, 1);
173
- }
174
- else if (ev.type === "provider_wait") {
175
- // The day report's provider-slot wait median, mirroring the slot_wait push: a wait only
176
- // event-logged at or above the 30 s threshold counts, so a 0 ms wait (a probe seam) adds
177
- // nothing and the median reflects real blocked admissions only.
178
- pushPositiveMs(target.providerWaitsMs, finiteNumber(ev.waitedMs, 0));
179
- }
180
- else if (ev.type === "slot_wait") {
181
- // The day report's workspace-slot wait median, mirroring the provider_wait push: a wait
182
- // only event-logged at or above the 30 s threshold counts, so a 0 ms wait (a probe seam)
183
- // or a pre-waitedMs event adds nothing.
184
- pushPositiveMs(target.slotWaitsMs, finiteNumber(ev.waitedMs, 0));
185
- }
186
- else if (ev.type === "check_wait") {
187
- // The day report's check-slot wait median, mirroring the slot_wait push: only waits logged
188
- // at or above the 30 s threshold count, so a 0 ms or pre-waitedMs event adds nothing.
189
- pushPositiveMs(target.checkWaitsMs, finiteNumber(ev.waitedMs, 0));
190
- }
191
- else if (ev.type === "merge_done" || ev.type === "merge_failed") {
192
- target.mergeRuns = (target.mergeRuns ?? 0) + 1;
193
- if (ev.type === "merge_failed")
194
- target.mergeFailed = (target.mergeFailed ?? 0) + 1;
195
- // Attribute the outcome to the authoring role (writeMergeOutcome logs role: entry.role)
196
- // folded to base role, the same normalization the merged branch uses, so `feature-2`
197
- // aggregates into `feature`. A parallel tally beside the team run counts, never
198
- // subtracted from mergeRuns/mergeFailed.
199
- const role = baseRoleOf(eventRole(ev));
200
- if (ev.type === "merge_done") {
201
- addTo(target.mergedByRole, role, 1);
202
- // Events logged before queuedMs existed carry none: skipped, not counted as zero.
203
- pushPositiveMs(target.mergeWaitsMs, finiteNumber(ev.queuedMs, 0));
204
- // How long the merge slot itself held the change, from the verification's start to the
205
- // outcome. Like mergeWaitsMs, only a landed merge feeds it, and a pre-durationMs event
206
- // (or 0) contributes nothing.
207
- pushPositiveMs(target.mergeDurationsMs, finiteNumber(ev.durationMs, 0));
208
- }
209
- else {
210
- addTo(target.mergeFailedByRole, role, 1);
211
- // The event's result (writeMergeOutcome's RunResult) names WHY the merge bounced
212
- // off the gate — rejected/merge_conflict/merge_blocked/aborted. A legacy event with
213
- // no result folds under `unknown`, so the reason buckets always sum to mergeFailed.
214
- const reason = typeof ev.result === "string" && ev.result.length > 0 ? ev.result : "unknown";
215
- addTo(target.mergeFailedByReason, reason, 1);
216
- }
217
- const { tokens, costUsd, notionalCostUsd } = eventUsage(ev);
218
- target.mergeTokens = (target.mergeTokens ?? 0) + tokens;
219
- target.mergeCostUsd = (target.mergeCostUsd ?? 0) + costUsd;
220
- // A subscription merge's API-equivalent spend joins the notional line, never the
221
- // budgeted cost totals (plans/agent-backends.md part 6a).
222
- target.notionalCostUsd += notionalCostUsd;
223
- // The merge's notional spend per backend (merge-slot.ts emits it on the event, keyed by
224
- // each subscription run's agent) — folded beside the run-end per-backend map, so a codex
225
- // merge's subscription spend names codex instead of falling back to the generic line.
226
- for (const [backend, c] of Object.entries(ev.notionalByBackend ?? {})) {
227
- const amt = finiteNumber(c, 0);
228
- if (amt !== 0)
229
- addTo(target.notionalByBackend, backend, amt);
230
- }
231
- // The merge's per-provider spend (merge-slot.ts keys it to its reviewer seam), so a
232
- // merge's costs appear under the provider that ran the reviewer, matching the author
233
- // side's foldMergeUsage charge.
234
- foldProviderCost(target, ev.providerCost);
235
- }
236
- }
20
+ import { foldUsageEvent } from "./report-usage-fold.js";
237
21
  /** Bounded memo of window folds. Eight entries cover a dashboard flipping among a few day
238
22
  * counts plus the midnight rollover's new fromKey; oldest-insertion eviction (Map preserves
239
23
  * insertion order) keeps the memory bound to a handful of per-day accumulator maps. */
@@ -253,6 +37,13 @@ export function zeroCommitCounts() {
253
37
  export function zeroMergeCounts() {
254
38
  return { mergeRuns: 0, mergeTokens: 0, mergeCostUsd: 0 };
255
39
  }
40
+ /** Build one object-literal spread cell for a per-slice role/product map only when the map has
41
+ * entries (`map` non-empty), so an entry-free window omits the field instead of carrying an
42
+ * empty map. The single home of that guard for both window collectors — the day report's and
43
+ * the trailing-window totals object-literal spreads all followed it inline before. */
44
+ export function nonemptyObj(key, map) {
45
+ return Object.keys(map).length > 0 ? { [key]: map } : {};
46
+ }
256
47
  /** The base roles a report scope names: undefined and an empty list mean no scope (the team
257
48
  * report); otherwise each id folded through `baseRoleOf`, deduplicated in first-seen order, so
258
49
  * `feature-2,feature` is one base. */
@@ -333,8 +124,9 @@ function dayFoldFor(entry, dayKey) {
333
124
  reviewDurationsMs: [],
334
125
  failedRunsByRole: {},
335
126
  fallbacksByRole: {},
336
- imageReroutesByRole: {},
337
127
  tokensByRole: {},
128
+ promptTokensByRole: {},
129
+ cacheReadTokensByRole: {},
338
130
  costByRole: {},
339
131
  notionalByRole: {},
340
132
  ...zeroMergeCounts(),
@@ -363,7 +155,7 @@ function foldEventInto(entry, ev) {
363
155
  entry.hadEvents = true;
364
156
  entry.firstEventTs = typeof ev.ts === "number" ? ev.ts : undefined;
365
157
  }
366
- if (isRedMain(ev) || isPassingMainCheck(ev))
158
+ if (isRedMain(ev) || isPassingMainCheck(ev) || ev.type === "main_culprit")
367
159
  entry.redEvents.push(ev);
368
160
  foldUsageEvent(dayFoldFor(entry, dayKey), ev);
369
161
  }
@@ -6,6 +6,16 @@
6
6
  * only on the team scope. */
7
7
  import { dayKey } from "../text/datetime.js";
8
8
  import { isPassingMainCheck, isRedMain } from "../baseline/main-guardrails.js";
9
+ import { addTo, getOrCreate } from "../collections.js";
10
+ import { shortSha } from "../text/format.js";
11
+ import { roleLabel } from "../roles/role-labels.js";
12
+ /** The events whose `ts` is a number in `[fromMs, toMs)`, in log order. The three byDay walks
13
+ * (mainRedMinutesByDay, mainRedFailingTestsByDay, mainRedCulpritsByDay) each iterate these, so
14
+ * they cannot disagree about which events fall in the window; the `typeof e.ts === "number"`
15
+ * guard also drops a torn log line whose stamp never landed on the event. */
16
+ function inWindowEvents(events, fromMs, toMs) {
17
+ return events.filter((e) => typeof e.ts === "number" && e.ts >= fromMs && e.ts < toMs);
18
+ }
9
19
  /** Add a spell's ms, split across the local calendar days it spans, to `map` (a Map, so the
10
20
  * caller keeps the day-key order the walk produced). */
11
21
  function addSpell(map, start, end) {
@@ -32,9 +42,7 @@ function addSpell(map, start, end) {
32
42
  * window — the report measures main's own red time within the window it was asked for. */
33
43
  export function mainRedMinutesByDay(events, fromMs, toMs) {
34
44
  const map = new Map();
35
- const inWindow = events
36
- .filter((e) => typeof e.ts === "number" && e.ts >= fromMs && e.ts < toMs)
37
- .sort((a, b) => a.ts - b.ts);
45
+ const inWindow = inWindowEvents(events, fromMs, toMs).sort((a, b) => a.ts - b.ts);
38
46
  let openStart;
39
47
  for (const e of inWindow) {
40
48
  const ts = e.ts;
@@ -53,3 +61,62 @@ export function mainRedMinutesByDay(events, fromMs, toMs) {
53
61
  addSpell(map, openStart, toMs);
54
62
  return map;
55
63
  }
64
+ /** Count named items into per-calendar-day maps (`YYYY-MM-DD` → key → count) over the window
65
+ * `[fromMs, toMs)`: walk the events `inWindowEvents` admits and, for each one `keysOf` turns
66
+ * into a non-empty list, add each key once into that event's day (`dayKey`). The two byDay
67
+ * count walks (`mainRedFailingTestsByDay`, `mainRedCulpritsByDay`) share this shape so they
68
+ * cannot disagree about which events fall in the window (`inWindowEvents`) or which day an
69
+ * event lands on (`dayKey`); `keysOf` returns `undefined` (or an empty list) for the events a
70
+ * walk ignores, keeping their loose-event-log guards (a non-`failingTests` red, a
71
+ * commit-less `main_culprit`) in the walk where its own rule reads clearest. */
72
+ function countByDayInWindow(events, fromMs, toMs, keysOf) {
73
+ const map = new Map();
74
+ for (const e of inWindowEvents(events, fromMs, toMs)) {
75
+ const keys = keysOf(e);
76
+ if (keys === undefined || keys.length === 0)
77
+ continue;
78
+ const record = getOrCreate(map, dayKey(e.ts), () => ({}));
79
+ for (const key of keys)
80
+ addTo(record, key, 1);
81
+ }
82
+ return map;
83
+ }
84
+ /** Main's red failing tests per local calendar day (`YYYY-MM-DD` → test → count), over the
85
+ * window `[fromMs, toMs)`: for each event `inWindowEvents` admits (numeric `ts` in `[fromMs,
86
+ * toMs)`) that G1 accepts as red (`isRedMain`) — a `main_check` that ran the full
87
+ * check and failed, or a `main_red_hold` — count each `failingTests` element that is a string
88
+ * once into that event's day (`dayKey`). A passing check is not red, and a red event that
89
+ * carries no `failingTests` (a hold whose red named none, or a torn non-array `failingTests`;
90
+ * a non-string element contributes nothing) records nothing, so the loose event log can never
91
+ * emit `undefined` as a key. Shares `isRedMain` and `dayKey` with
92
+ * `mainRedMinutesByDay`, so the two cannot disagree about which events are main's red ones.
93
+ * This is main's own failing tests, not any role's, so the report carries it only on the team
94
+ * scope. */
95
+ export function mainRedFailingTestsByDay(events, fromMs, toMs) {
96
+ return countByDayInWindow(events, fromMs, toMs, (e) => isRedMain(e) && Array.isArray(e.failingTests)
97
+ ? e.failingTests.filter((t) => typeof t === "string")
98
+ : undefined);
99
+ }
100
+ /** Main's red culprits per local calendar day (`YYYY-MM-DD` → label → count), over the window
101
+ * `[fromMs, toMs)`: for each event `inWindowEvents` admits (numeric `ts` in `[fromMs, toMs)`)
102
+ * that is a `main_culprit` whose `commit` is a non-empty string, label it
103
+ * `<roleLabel(runner)> <shortSha(commit)>` when `runner` is a non-empty string, else
104
+ * `merge <shortSha(commit)>`, and count it once into that event's day (`dayKey`). This is the
105
+ * live `mainred` alert's own event (`src/status/status-polls.ts`'s `MainCheckStatus.red
106
+ * .culprit`), so the report names the very merge the watch blamed, not just the tests it
107
+ * named. A `main_culprit` with no `commit` or an empty/non-string one contributes nothing, so
108
+ * the loose event log can never emit `undefined` as a key. Shares `dayKey` with the sibling
109
+ * walks (`mainRedMinutesByDay`, `mainRedFailingTestsByDay`), so the three cannot disagree
110
+ * about which day an event lands on. This is main's own blame, not any role's, so the report
111
+ * carries it only on the team scope. */
112
+ export function mainRedCulpritsByDay(events, fromMs, toMs) {
113
+ return countByDayInWindow(events, fromMs, toMs, (e) => e.type === "main_culprit" &&
114
+ typeof e.commit === "string" &&
115
+ e.commit.length > 0
116
+ ? [
117
+ typeof e.runner === "string" && e.runner.length > 0
118
+ ? `${roleLabel(e.runner)} ${shortSha(e.commit)}`
119
+ : `merge ${shortSha(e.commit)}`,
120
+ ]
121
+ : undefined);
122
+ }