tumwater 0.1.2 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (361) hide show
  1. package/README.md +74 -108
  2. package/dist/build-info.json +2 -2
  3. package/dist/src/agent/agent-args.js +24 -0
  4. package/dist/src/agent/agent-backend.js +2 -2
  5. package/dist/src/agent/backend-traits.js +60 -0
  6. package/dist/src/agent/claude/claude-account.js +61 -8
  7. package/dist/src/agent/claude/claude-args.js +12 -15
  8. package/dist/src/agent/claude/claude-backend.js +51 -41
  9. package/dist/src/agent/claude/claude-env.js +7 -3
  10. package/dist/src/agent/claude/claude-stream.js +7 -16
  11. package/dist/src/agent/image-input.js +107 -0
  12. package/dist/src/agent/notes-shim.js +41 -0
  13. package/dist/src/agent/opencode/opencode-args.js +61 -0
  14. package/dist/src/agent/opencode/opencode-backend.js +255 -0
  15. package/dist/src/agent/opencode/opencode-catalog.js +184 -0
  16. package/dist/src/agent/opencode/opencode-config.js +188 -0
  17. package/dist/src/agent/opencode/opencode-env.js +36 -0
  18. package/dist/src/agent/opencode/opencode-session-id.js +37 -0
  19. package/dist/src/agent/opencode/opencode-stream.js +278 -0
  20. package/dist/src/agent/role-notes-write.js +16 -32
  21. package/dist/src/agent/run-agent.js +95 -18
  22. package/dist/src/agent/session-ledger.js +57 -1
  23. package/dist/src/backlog/backlog-archive.js +296 -0
  24. package/dist/src/backlog/backlog-cli.js +573 -0
  25. package/dist/src/backlog/backlog-commit.js +31 -0
  26. package/dist/src/backlog/backlog-eligibility.js +357 -33
  27. package/dist/src/backlog/backlog-md.js +98 -18
  28. package/dist/src/backlog/backlog-render.js +62 -11
  29. package/dist/src/backlog/backlog-structure.js +261 -58
  30. package/dist/src/backlog/backlog-write.js +197 -319
  31. package/dist/src/backlog/backlog.js +113 -16
  32. package/dist/src/backlog/question-answer.js +339 -0
  33. package/dist/src/baseline/main-baseline.js +75 -88
  34. package/dist/src/baseline/main-red.js +38 -30
  35. package/dist/src/brief/brief-edit.js +43 -0
  36. package/dist/src/{brief.js → brief/brief.js} +66 -10
  37. package/dist/src/budget/budget.js +32 -15
  38. package/dist/src/budget/fallback-breaker.js +8 -7
  39. package/dist/src/build/build-check-detect.js +8 -7
  40. package/dist/src/build/build-check-events.js +68 -22
  41. package/dist/src/build/build-check-report.js +22 -23
  42. package/dist/src/build/build-check-scoped.js +51 -62
  43. package/dist/src/build/build-check.js +15 -14
  44. package/dist/src/build/build-info.js +2 -1
  45. package/dist/src/build/build-stage.js +23 -22
  46. package/dist/src/build/dep-install.js +22 -21
  47. package/dist/src/build/host-sleep.js +1 -1
  48. package/dist/src/change/change-data.js +25 -14
  49. package/dist/src/change/change-render.js +11 -10
  50. package/dist/src/cli/brief-command.js +58 -0
  51. package/dist/src/cli/cli-args.js +25 -5
  52. package/dist/src/cli/cli-command-args.js +46 -14
  53. package/dist/src/cli/cli-editor.js +64 -0
  54. package/dist/src/cli/cli-flag-specs.js +54 -16
  55. package/dist/src/cli/cli-marker-commands.js +13 -10
  56. package/dist/src/cli/cli-output.js +9 -0
  57. package/dist/src/cli/cli-query-commands.js +458 -49
  58. package/dist/src/cli/cli-repo-gate.js +1 -1
  59. package/dist/src/cli/cli-run.js +208 -59
  60. package/dist/src/cli/completion.js +207 -0
  61. package/dist/src/cli/config-commands.js +14 -11
  62. package/dist/src/cli/help.js +305 -110
  63. package/dist/src/cli/log-commands.js +127 -21
  64. package/dist/src/cli/notify-command.js +33 -18
  65. package/dist/src/cli/question-commands.js +29 -224
  66. package/dist/src/cli.js +121 -40
  67. package/dist/src/collections.js +66 -16
  68. package/dist/src/concurrency/check-permit.js +16 -17
  69. package/dist/src/concurrency/lock.js +6 -5
  70. package/dist/src/concurrency/provider-lease.js +69 -0
  71. package/dist/src/concurrency/semaphore.js +258 -36
  72. package/dist/src/config/config-aliases.js +106 -0
  73. package/dist/src/config/config-editable-keys.js +3 -3
  74. package/dist/src/config/config-fallbacks.js +379 -0
  75. package/dist/src/config/config-field-checks.js +19 -10
  76. package/dist/src/config/config-keys.js +160 -0
  77. package/dist/src/config/config-live.js +64 -12
  78. package/dist/src/config/config-schema.js +34 -22
  79. package/dist/src/config/config-validation.js +132 -48
  80. package/dist/src/config/config-views.js +119 -337
  81. package/dist/src/config/config-write.js +138 -177
  82. package/dist/src/config/config.js +60 -49
  83. package/dist/src/config/model-selector.js +28 -2
  84. package/dist/src/doctor/doctor-backlog.js +546 -49
  85. package/dist/src/doctor/doctor-checks.js +534 -56
  86. package/dist/src/doctor/doctor-holds.js +193 -53
  87. package/dist/src/doctor/doctor-install.js +30 -0
  88. package/dist/src/doctor/doctor-launch-services.js +2 -2
  89. package/dist/src/doctor/doctor-model-checks.js +549 -157
  90. package/dist/src/doctor/doctor-notify.js +27 -0
  91. package/dist/src/doctor/doctor-orphans.js +10 -9
  92. package/dist/src/doctor/doctor-render.js +36 -9
  93. package/dist/src/doctor/doctor.js +80 -22
  94. package/dist/src/doctor/image-probe.js +244 -0
  95. package/dist/src/doctor/probe.js +64 -0
  96. package/dist/src/events/event-format.js +226 -102
  97. package/dist/src/events/event-labels.js +116 -0
  98. package/dist/src/events/event-legacy.js +140 -0
  99. package/dist/src/events/event-read.js +58 -22
  100. package/dist/src/events/event-window.js +27 -7
  101. package/dist/src/events/events.js +18 -14
  102. package/dist/src/events/notify.js +137 -38
  103. package/dist/src/failure/failure-cluster.js +12 -9
  104. package/dist/src/failure/failure-data.js +96 -48
  105. package/dist/src/failure/failure-render.js +69 -58
  106. package/dist/src/failure/failure-state-change.js +33 -26
  107. package/dist/src/failure/time-spend.js +79 -59
  108. package/dist/src/files/file-queue.js +16 -4
  109. package/dist/src/files/files.js +18 -6
  110. package/dist/src/files/json-object.js +18 -3
  111. package/dist/src/files/tail.js +1 -1
  112. package/dist/src/gates/bootstrap-gates.js +5 -4
  113. package/dist/src/gates/budget-gates.js +250 -34
  114. package/dist/src/gates/disk-gate.js +18 -18
  115. package/dist/src/gates/gate-polls.js +117 -69
  116. package/dist/src/gates/maintenance-quota.js +29 -29
  117. package/dist/src/gates/pause-gates.js +11 -11
  118. package/dist/src/gates/readiness.js +2 -1
  119. package/dist/src/gates/role-cap-gates.js +16 -21
  120. package/dist/src/gates/startup-gate.js +75 -31
  121. package/dist/src/gates/streak-gate.js +18 -16
  122. package/dist/src/git/commit-message.js +12 -5
  123. package/dist/src/git/git-diff.js +16 -16
  124. package/dist/src/git/git-run.js +8 -8
  125. package/dist/src/git/git.js +86 -55
  126. package/dist/src/git/slots-state.js +14 -5
  127. package/dist/src/git/worktree-pool.js +25 -18
  128. package/dist/src/git/worktree-use.js +15 -14
  129. package/dist/src/git/worktree.js +18 -18
  130. package/dist/src/gui/gui-args.js +10 -14
  131. package/dist/src/gui/gui-backlog-commands.js +273 -0
  132. package/dist/src/gui/gui-command.js +1 -1
  133. package/dist/src/gui/gui-config-commands.js +440 -0
  134. package/dist/src/gui/gui-endpoint-commands.js +16 -669
  135. package/dist/src/gui/gui-endpoint-common.js +49 -0
  136. package/dist/src/gui/gui-endpoints.js +116 -56
  137. package/dist/src/gui/gui-operator-commands.js +145 -0
  138. package/dist/src/gui/gui-prompt-commands.js +234 -0
  139. package/dist/src/gui/gui-server.js +28 -8
  140. package/dist/src/gui/gui-team-commands.js +164 -0
  141. package/dist/src/gui/http-body.js +17 -1
  142. package/dist/src/history/history-data.js +194 -98
  143. package/dist/src/history/history-render.js +76 -0
  144. package/dist/src/history/history.js +37 -58
  145. package/dist/src/inbox/inbox-attachments.js +75 -14
  146. package/dist/src/inbox/inbox-cancel.js +7 -5
  147. package/dist/src/inbox/inbox-edit.js +1 -1
  148. package/dist/src/inbox/inbox-submit.js +44 -4
  149. package/dist/src/inbox/inbox-top.js +83 -0
  150. package/dist/src/inbox/inbox.js +8 -2
  151. package/dist/src/inbox/pending-prompt.js +1 -1
  152. package/dist/src/inbox/prompt-commands.js +170 -80
  153. package/dist/src/init/init-templates.js +16 -14
  154. package/dist/src/init/init.js +12 -11
  155. package/dist/src/loop/leftover.js +28 -27
  156. package/dist/src/loop/loop-fallback.js +2 -2
  157. package/dist/src/loop/loop-pi.js +106 -52
  158. package/dist/src/loop/loop-recovery.js +15 -15
  159. package/dist/src/loop/loop-state.js +139 -13
  160. package/dist/src/loop/loop-tick.js +194 -86
  161. package/dist/src/loop/loop.js +72 -36
  162. package/dist/src/loop/model-fallback.js +3 -2
  163. package/dist/src/loop/revision.js +16 -7
  164. package/dist/src/merge/backlog-conflicts.js +155 -0
  165. package/dist/src/{landing/landing-batch.js → merge/merge-batch.js} +90 -89
  166. package/dist/src/{landing/landing-check-failures.js → merge/merge-check-failures.js} +87 -41
  167. package/dist/src/{landing/landing-core.js → merge/merge-core.js} +119 -95
  168. package/dist/src/{landing/landing-diff.js → merge/merge-diff.js} +1 -1
  169. package/dist/src/merge/merge-drain.js +212 -0
  170. package/dist/src/{landing/landing-git.js → merge/merge-git.js} +94 -21
  171. package/dist/src/merge/merge-pipeline.js +99 -0
  172. package/dist/src/{landing/landing-questions.js → merge/merge-questions.js} +9 -6
  173. package/dist/src/merge/merge-queue-render.js +75 -0
  174. package/dist/src/{landing/landing-queue.js → merge/merge-queue.js} +95 -36
  175. package/dist/src/merge/merge-slot.js +340 -0
  176. package/dist/src/{landing/landing-stack.js → merge/merge-stack.js} +66 -62
  177. package/dist/src/{landing/landing-merge.js → merge/merge-to-main.js} +72 -62
  178. package/dist/src/merge/merge-verify.js +212 -0
  179. package/dist/src/notes-cli.js +97 -0
  180. package/dist/src/operator/operator-commands.js +154 -47
  181. package/dist/src/operator/operator-intent.js +87 -49
  182. package/dist/src/operator/operator-requests.js +52 -52
  183. package/dist/src/operator/retire.js +62 -27
  184. package/dist/src/operator/revert.js +71 -0
  185. package/dist/src/orchestrator/orchestrator-launch.js +74 -14
  186. package/dist/src/orchestrator/orchestrator-scheduling.js +129 -44
  187. package/dist/src/orchestrator/orchestrator.js +158 -100
  188. package/dist/src/orchestrator/retention.js +29 -12
  189. package/dist/src/orchestrator/startup-migration-steps.js +51 -0
  190. package/dist/src/orchestrator/startup-migrations.js +40 -0
  191. package/dist/src/paths.js +128 -37
  192. package/dist/src/pi/pi-args.js +26 -3
  193. package/dist/src/pi/pi-backend.js +15 -23
  194. package/dist/src/pi/pi-bin.js +18 -12
  195. package/dist/src/pi/pi-event-line.js +17 -1
  196. package/dist/src/pi/pi-failure.js +87 -0
  197. package/dist/src/pi/pi-models.js +126 -38
  198. package/dist/src/pi/pi-run-result.js +1 -1
  199. package/dist/src/pi/pi-settings.js +86 -0
  200. package/dist/src/pi/pi-stream.js +131 -107
  201. package/dist/src/pi/pi-watchdogs.js +47 -43
  202. package/dist/src/pi-extension/bounded-output.js +20 -5
  203. package/dist/src/pi-extension/context-budget.js +3 -1
  204. package/dist/src/pi-extension/context-shake.js +54 -21
  205. package/dist/src/pi-extension/context-window.js +37 -0
  206. package/dist/src/pi-extension/full-output.js +2 -2
  207. package/dist/src/pi-extension/strip-thinking.js +80 -0
  208. package/dist/src/pi-extension/trim-system-prompt.js +38 -0
  209. package/dist/src/process/launch-services.js +11 -10
  210. package/dist/src/process/process-table.js +1 -1
  211. package/dist/src/process/process.js +7 -7
  212. package/dist/src/process/run-marker.js +29 -6
  213. package/dist/src/process/supervisor.js +47 -25
  214. package/dist/src/prompt/dialect.js +30 -10
  215. package/dist/src/prompt/gate-prompts.js +170 -67
  216. package/dist/src/prompt/principles.js +1 -0
  217. package/dist/src/prompt/prompt-followup.js +35 -20
  218. package/dist/src/prompt/prompt.js +249 -91
  219. package/dist/src/redeploy/redeploy-policy.js +10 -10
  220. package/dist/src/redeploy/redeploy.js +6 -4
  221. package/dist/src/redeploy/redeployer.js +63 -55
  222. package/dist/src/redeploy/self-reload.js +1 -1
  223. package/dist/src/report/report-data.js +264 -255
  224. package/dist/src/report/report-fold.js +353 -0
  225. package/dist/src/report/report-render.js +270 -33
  226. package/dist/src/request-timeouts.js +1 -1
  227. package/dist/src/review/known-flakes.js +34 -14
  228. package/dist/src/review/protected-edits.js +33 -0
  229. package/dist/src/review/review-followup.js +16 -4
  230. package/dist/src/review/review-precheck.js +14 -13
  231. package/dist/src/review/review-verdict.js +14 -0
  232. package/dist/src/review/review.js +81 -39
  233. package/dist/src/review/secret-leaks.js +127 -0
  234. package/dist/src/review/suite-rerun.js +4 -3
  235. package/dist/src/roles/legacy-role-ids.js +21 -0
  236. package/dist/src/roles/legacy-role-migration.js +62 -0
  237. package/dist/src/roles/loop-ids.js +19 -2
  238. package/dist/src/roles/role-catalog.js +125 -135
  239. package/dist/src/roles/role-guidance.js +64 -31
  240. package/dist/src/roles/role-labels.js +10 -0
  241. package/dist/src/roles/role-render.js +194 -18
  242. package/dist/src/roles/role-view.js +216 -17
  243. package/dist/src/roles/roles.js +18 -15
  244. package/dist/src/scheduling/backoff.js +29 -19
  245. package/dist/src/scheduling/claims.js +25 -9
  246. package/dist/src/scheduling/once-round.js +11 -11
  247. package/dist/src/scheduling/quiet-hours.js +25 -32
  248. package/dist/src/scheduling/scheduling.js +43 -24
  249. package/dist/src/scheduling/{work-landed-cache.js → work-merged-cache.js} +14 -14
  250. package/dist/src/status/status-budget.js +115 -0
  251. package/dist/src/status/status-data.js +77 -184
  252. package/dist/src/status/status-inbox.js +47 -0
  253. package/dist/src/status/status-install.js +27 -0
  254. package/dist/src/status/status-polls.js +68 -33
  255. package/dist/src/status/status-roles.js +105 -0
  256. package/dist/src/{fleet → team}/error-storm.js +10 -10
  257. package/dist/src/{fleet → team}/failure-spread.js +8 -8
  258. package/dist/src/{fleet → team}/orchestrator-info.js +86 -18
  259. package/dist/src/{fleet → team}/reclaim.js +5 -4
  260. package/dist/src/{fleet/fleet-hold.js → team/team-hold.js} +55 -21
  261. package/dist/src/{fleet/fleet-polls.js → team/team-polls.js} +116 -65
  262. package/dist/src/{fleet/fleet-state.js → team/team-state.js} +62 -37
  263. package/dist/src/text/datetime.js +42 -2
  264. package/dist/src/text/format.js +10 -1
  265. package/dist/src/text/markdown.js +1 -1
  266. package/dist/src/text/phrases.js +35 -34
  267. package/dist/src/text/suggest.js +3 -2
  268. package/dist/src/text/text.js +2 -2
  269. package/dist/src/tick/qa-coverage.js +2 -1
  270. package/dist/src/tick/stage-check.js +147 -26
  271. package/dist/src/tick/telemetry-digest.js +10 -9
  272. package/dist/src/tick/tick-apply.js +101 -72
  273. package/dist/src/tick/tick-detail-data.js +32 -18
  274. package/dist/src/tick/tick-detail.js +62 -21
  275. package/dist/src/tick/tick-finalize.js +29 -22
  276. package/dist/src/tick/tick-outcome.js +2 -1
  277. package/dist/src/tick/tick-prompt.js +94 -26
  278. package/dist/src/tick/tick-resume.js +29 -13
  279. package/dist/src/tick/tick-stage.js +91 -47
  280. package/dist/src/tick/tick-timing.js +60 -59
  281. package/dist/src/tick/tick-usage.js +53 -23
  282. package/dist/src/tick/tick-verdict.js +76 -10
  283. package/dist/src/ui/badges.js +118 -67
  284. package/dist/src/ui/gui/gui-client-boot.js +209 -30
  285. package/dist/src/ui/gui/gui-client-composer.js +102 -23
  286. package/dist/src/ui/gui/gui-client-drawer.js +330 -75
  287. package/dist/src/ui/gui/gui-client-history.js +96 -38
  288. package/dist/src/ui/gui/gui-client-labels.js +24 -0
  289. package/dist/src/ui/gui/gui-client-loops.js +94 -52
  290. package/dist/src/ui/gui/gui-client-markdown.js +1 -0
  291. package/dist/src/ui/gui/gui-client-model.js +63 -43
  292. package/dist/src/ui/gui/gui-client-operator.js +129 -39
  293. package/dist/src/ui/gui/gui-client-pending.js +36 -32
  294. package/dist/src/ui/gui/gui-client-report.js +339 -46
  295. package/dist/src/ui/gui/gui-client-settings.js +148 -14
  296. package/dist/src/ui/gui/gui-client-sound.js +2 -1
  297. package/dist/src/ui/gui/{gui-client-fleet.js → gui-client-team.js} +196 -72
  298. package/dist/src/ui/gui/gui-client.js +26 -20
  299. package/dist/src/ui/gui/gui-page.js +19 -19
  300. package/dist/src/ui/gui/gui-styles.js +12 -4
  301. package/dist/src/ui/progress-data.js +5 -3
  302. package/dist/src/ui/status-model.js +184 -122
  303. package/dist/src/ui/status-payload.js +119 -32
  304. package/dist/src/ui/status-render.js +122 -52
  305. package/dist/src/ui/team-alerts.js +341 -0
  306. package/dist/src/ui/tick-progress-model.js +14 -14
  307. package/dist/src/ui/tone.js +11 -8
  308. package/dist/src/ui/transcript-tail.js +1 -0
  309. package/dist/src/ui/transcript.js +23 -16
  310. package/dist/src/ui/tui/tui-app.js +1 -0
  311. package/dist/src/ui/tui/tui-backlog.js +85 -11
  312. package/dist/src/ui/tui/tui-editor-answer.js +97 -0
  313. package/dist/src/ui/tui/tui-editor-attach.js +107 -0
  314. package/dist/src/ui/tui/tui-editor-brief.js +107 -0
  315. package/dist/src/ui/tui/tui-editor-budget.js +155 -0
  316. package/dist/src/ui/tui/tui-editor-ctx.js +1 -0
  317. package/dist/src/ui/tui/tui-editor-entry.js +230 -0
  318. package/dist/src/ui/tui/tui-editor-interval.js +122 -0
  319. package/dist/src/ui/tui/tui-editor-notes.js +114 -0
  320. package/dist/src/ui/tui/tui-editor-pause.js +266 -0
  321. package/dist/src/ui/tui/tui-editor-prompt.js +95 -0
  322. package/dist/src/ui/tui/tui-editor-queued.js +98 -0
  323. package/dist/src/ui/tui/tui-editor-quiet.js +106 -0
  324. package/dist/src/ui/tui/tui-editor-retire.js +83 -0
  325. package/dist/src/ui/tui/tui-editor-search.js +73 -0
  326. package/dist/src/ui/tui/tui-editor-session.js +97 -0
  327. package/dist/src/ui/tui/tui-editors.js +152 -0
  328. package/dist/src/ui/tui/tui-frame.js +67 -55
  329. package/dist/src/ui/tui/tui-history.js +12 -7
  330. package/dist/src/ui/tui/tui-input.js +49 -44
  331. package/dist/src/ui/tui/tui-keymap.js +1 -0
  332. package/dist/src/ui/tui/tui-keys.js +91 -1704
  333. package/dist/src/ui/tui/tui-pane-activity.js +99 -0
  334. package/dist/src/ui/tui/tui-pane-backlog.js +224 -0
  335. package/dist/src/ui/tui/tui-pane-brief.js +30 -0
  336. package/dist/src/ui/tui/tui-pane-ctx.js +11 -0
  337. package/dist/src/ui/tui/tui-pane-health.js +124 -0
  338. package/dist/src/ui/tui/tui-pane-history.js +316 -0
  339. package/dist/src/ui/tui/tui-pane-pending.js +74 -0
  340. package/dist/src/ui/tui/tui-pane-queued.js +143 -0
  341. package/dist/src/ui/tui/tui-pane-report.js +122 -0
  342. package/dist/src/ui/tui/tui-pane-session.js +101 -0
  343. package/dist/src/ui/tui/tui-pane-transcript.js +150 -0
  344. package/dist/src/ui/tui/tui-pane.js +66 -28
  345. package/dist/src/ui/tui/tui-panes.js +294 -0
  346. package/dist/src/ui/tui/tui.js +45 -25
  347. package/dist/src/verdict/fix-claim.js +141 -38
  348. package/dist/src/verdict/no-change.js +19 -0
  349. package/dist/src/verdict/refusal.js +92 -8
  350. package/dist/src/verdict/reply-contract.js +17 -5
  351. package/dist/src/version.js +3 -2
  352. package/package.json +4 -2
  353. package/dist/src/agent/mcp/role-notes-server.js +0 -160
  354. package/dist/src/landing/backlog-conflicts.js +0 -116
  355. package/dist/src/landing/landing-drain.js +0 -204
  356. package/dist/src/landing/landing-pipeline.js +0 -96
  357. package/dist/src/landing/landing-queue-render.js +0 -31
  358. package/dist/src/landing/landing-slot.js +0 -246
  359. package/dist/src/landing/landing-vetting.js +0 -196
  360. package/dist/src/pi-extension/role-notes.js +0 -40
  361. package/dist/src/ui/fleet-alerts.js +0 -174
@@ -1,211 +1,26 @@
1
- /** Collection half of the usage report: read the harness's event log and backlog history over
2
- * a window of local calendar days and distill it into a `ReportData` — per-day tick/token/
3
- * commit/cost counts plus features-done and bugs-fixed tallies from PLANS.md and BUGS.md.
4
- * The Markdown rendering of this data lives in src/report/report-render.ts, a pure function
5
- * of it; the split mirrors the failure report's (src/failure/failure-data.ts /
1
+ // vocabulary: plain
2
+ /** Collection half of the usage report: read the harness's event log (through the shared
3
+ * event-folding layer in src/report/report-fold.ts) and the backlog history over a window of
4
+ * local calendar days and distill it into a `ReportData` / `SinceReport` — per-day
5
+ * tick/token/commit/cost counts plus features-done and bugs-fixed tallies from PLANS.md and
6
+ * BUGS.md. The Markdown rendering of this data lives in src/report/report-render.ts, a pure
7
+ * function of it; the split mirrors the failure report's (src/failure/failure-data.ts /
6
8
  * src/failure/failure-render.ts) and keeps "what happened" (window math, aggregation, bounds)
7
9
  * apart from "how it prints" (bars, cell wording), which change for different reasons — and
8
10
  * keeps core data collection out of the presentation layer, so a core consumer (as
9
- * /api/report already is) never forces a core→ui import. */
10
- import { sameStatIdentity, statOrNull } from "../files/files.js";
11
+ * /api/report already is) never forces a core→ui import. This file holds the two window
12
+ * collectors and the report shapes they produce; the event-log folding and its memoized
13
+ * per-window cache live in src/report/report-fold.ts (its own test, report-fold-cache). */
11
14
  import { assertReportSinceMs, eventWindowCovers, readWindowEvents } from "../events/event-window.js";
12
- import { eventDayKey, eventRole, eventUsage, parseEventLine } from "../events/event-read.js";
13
- import { readCompleteLines } from "../files/tail.js";
14
- import { eventsLogPath } from "../paths.js";
15
- import { sectionCompletionDates } from "../backlog/backlog.js";
15
+ import { DONE_DATE_RE, FIXED_DATE_RE, completionDate, finishedFiles, fixedBugEntries, sectionCompletionDates, sectionFilingDates, } from "../backlog/backlog.js";
16
+ import { validationGapTag } from "../backlog/backlog-md.js";
16
17
  import { dayAt, dayKey, dayWindow, formatDate } from "../text/datetime.js";
17
- import { addTo } from "../collections.js";
18
+ import { addTo, lowerMedian } from "../collections.js";
18
19
  import { baseRoleOf } from "../roles/loop-ids.js";
19
20
  import { commitTier } from "../roles/roles.js";
20
- /** Fold one event into a usage accumulator: tick_end events add a tick (per role and, when
21
- * the target counts ticks, in total), their output tokens, and their cost — zero-cost ticks
22
- * contribute nothing, so they leave no costByRole key, keeping the render's "$0 roles are
23
- * omitted" rule true on the aggregation itself, not just at display time; merged events add
24
- * a commit; landed/land_failed events add a landing run and
25
- * accumulate their tokens/cost into the landing fields only — costByRole stays a ticks-only
26
- * breakdown, and the landing line is how the reviewer's share is named. Both collectors fold
27
- * through this one place, so the two windows' aggregation rules cannot drift apart. */
28
- function foldUsageEvent(target, ev) {
29
- if (ev.type === "tick_end") {
30
- const role = eventRole(ev);
31
- addTo(target.ticksByRole, role, 1);
32
- if (target.ticks !== undefined)
33
- target.ticks++;
34
- const { tokens, costUsd, notionalCostUsd } = eventUsage(ev);
35
- addTo(target.tokensByRole, role, tokens);
36
- target.tokensOut += tokens;
37
- target.costUsd += costUsd;
38
- target.notionalCostUsd += notionalCostUsd;
39
- if (costUsd !== 0)
40
- addTo(target.costByRole, role, costUsd);
41
- if (notionalCostUsd !== 0)
42
- addTo(target.notionalByRole, role, notionalCostUsd);
43
- }
44
- else if (ev.type === "merged") {
45
- target.commits++;
46
- // Work ratio 4/4: split landed commits by base role and by tier. Instance suffixes fold
47
- // into their role (`feature-2` counts as feature) so the split does not fragment.
48
- const role = baseRoleOf(eventRole(ev));
49
- addTo(target.commitsByRole, role, 1);
50
- const tier = commitTier(role);
51
- if (tier === "work")
52
- target.workCommits++;
53
- else if (tier === "maintenance")
54
- target.maintenanceCommits++;
55
- }
56
- else if (ev.type === "landed" || ev.type === "land_failed") {
57
- target.landingRuns = (target.landingRuns ?? 0) + 1;
58
- const { tokens, costUsd, notionalCostUsd } = eventUsage(ev);
59
- target.landingTokens = (target.landingTokens ?? 0) + tokens;
60
- target.landingCostUsd = (target.landingCostUsd ?? 0) + costUsd;
61
- // A subscription landing's API-equivalent spend joins the notional line, never the
62
- // budgeted cost totals (plans/agent-backends.md part 6a).
63
- target.notionalCostUsd += notionalCostUsd;
64
- }
65
- }
66
- /** Bounded memo of window folds. Eight entries cover a dashboard flipping among a few day
67
- * counts plus the midnight rollover's new fromKey; oldest-insertion eviction (Map preserves
68
- * insertion order) keeps the memory bound to a handful of per-day accumulator maps. */
69
- const reportFoldCache = new Map();
70
- const REPORT_FOLD_CACHE_MAX = 8;
71
- /** The empty commit-family counters every accumulator starts from: the total and per-base-role
72
- * landed-commit count plus the work/maintenance split (Work ratio 4/4). Four shapes carry them —
73
- * DayFold, UsageFold, the ReportDay series slots, and the window totals — so the empty quartet
74
- * lives once here and a new commit counter is added in one place. */
75
- function zeroCommitCounts() {
76
- return { commits: 0, commitsByRole: {}, workCommits: 0, maintenanceCommits: 0 };
77
- }
78
- /** The empty landing-run counters every accumulator starts from: the landing slot's run count
79
- * and its own token/cost share (foldUsageEvent's landed/land_failed branch). Four shapes carry
80
- * them — DayFold, UsageFold, the scoped window totals, and the window totals — so the empty
81
- * triple lives once here and a new landing counter is added in one place. */
82
- function zeroLandingCounts() {
83
- return { landingRuns: 0, landingTokens: 0, landingCostUsd: 0 };
84
- }
85
- /** Sum a per-loop-id count/cost map over one base role: instance keys (`feature-2`) fold into
86
- * their base (`feature`), so a scoped report aggregates a whole role group exactly as the
87
- * per-role cap's group does. Ticks/tokens/cost are keyed by exact loop id while commits are
88
- * already base-role keyed; running every key through `baseRoleOf` reads both shapes the same
89
- * and returns 0 when nothing matches. */
90
- function roleSum(record, role) {
91
- let sum = 0;
92
- for (const [key, n] of Object.entries(record)) {
93
- if (baseRoleOf(key) === role)
94
- sum += n;
95
- }
96
- return sum;
97
- }
98
- /** Narrow a per-loop-id count/cost map to one base role, keyed by the base role; a role absent
99
- * from the map (no ticks, or no nonzero cost) yields an empty map, never a zero entry — the
100
- * same zero-means-absent rule the fleet render applies. Used by the role-scoped collectors so a
101
- * scoped series derives from the fold's per-role maps without inventing entries. */
102
- function narrowToRole(record, role) {
103
- const n = roleSum(record, role);
104
- return n === 0 ? {} : { [role]: n };
105
- }
106
- function dayFoldFor(entry, dayKey) {
107
- let fold = entry.days.get(dayKey);
108
- if (!fold) {
109
- fold = {
110
- tokensOut: 0,
111
- ...zeroCommitCounts(),
112
- costUsd: 0,
113
- notionalCostUsd: 0,
114
- ticksByRole: {},
115
- tokensByRole: {},
116
- costByRole: {},
117
- notionalByRole: {},
118
- ...zeroLandingCounts(),
119
- };
120
- entry.days.set(dayKey, fold);
121
- }
122
- return fold;
123
- }
124
- /** Fold one already-parsed event into a cache entry: only in-window events (a non-null day
125
- * on or after fromKey) count, matching readWindowEvents's event list; the first such event
126
- * seen takes firstEventTs. The one home of the fold body, shared by the incremental append
127
- * path (foldLineInto) and the full re-read path, so the two can never drift. */
128
- function foldEventInto(entry, ev) {
129
- const dayKey = eventDayKey(ev);
130
- if (dayKey === null || dayKey < entry.fromKey)
131
- return;
132
- if (!entry.hadEvents) {
133
- entry.hadEvents = true;
134
- entry.firstEventTs = typeof ev.ts === "number" ? ev.ts : undefined;
135
- }
136
- foldUsageEvent(dayFoldFor(entry, dayKey), ev);
137
- }
138
- /** Parse one appended log line and fold it; a torn or non-event line folds nothing. */
139
- function foldLineInto(entry, line) {
140
- const ev = parseEventLine(line);
141
- if (ev)
142
- foldEventInto(entry, ev);
143
- }
144
- /** The event-log half of collectReport, memoized: a dashboard re-fetching /api/report every
145
- * poll re-read and re-parsed the whole window's log bytes each time (~6.8 MB and tens of ms
146
- * on this repo's own day log, growing toward the 16 MB rotation cap). Now a request whose
147
- * log has not changed costs one stat; growth folds only the appended complete lines
148
- * (readCompleteLines holds back a torn trailing write, so it is folded whole next call).
149
- * Same size with a changed mtime, a shrunken file (rotation or rewrite), or a new fromKey
150
- * falls back to the full readWindowEvents pass — the fallback re-derives everything, so a
151
- * mis-fold can only come from the append assumption, and rotation's whole-file replace
152
- * always hits the shrink arm. The entry's coverage parts are pinned by the cache key:
153
- * fromKey and file bytes decide them, so an advancing window re-derives fresh instead of
154
- * carrying yesterday's proof forward. */
155
- function foldWindowEvents(root, fromKey) {
156
- const key = `${root}\0${fromKey}`;
157
- const liveStat = statOrNull(eventsLogPath(root));
158
- const size = liveStat?.size ?? 0;
159
- const mtimeMs = liveStat?.mtimeMs ?? 0;
160
- const dev = liveStat?.dev ?? 0;
161
- const ino = liveStat?.ino ?? 0;
162
- const cached = reportFoldCache.get(key);
163
- if (cached &&
164
- sameStatIdentity({ dev, ino, mtimeMs, size }, { dev: cached.dev, ino: cached.ino, mtimeMs: cached.mtimeMs, size: cached.offset })) {
165
- reportFoldCache.delete(key); // Refresh LRU position.
166
- reportFoldCache.set(key, cached);
167
- return cached;
168
- }
169
- if (cached && size > cached.offset && dev === cached.dev && ino === cached.ino) {
170
- const { lines, end } = readCompleteLines(eventsLogPath(root), cached.offset, size);
171
- if (end > cached.offset) {
172
- for (const line of lines)
173
- foldLineInto(cached, line);
174
- cached.offset = end;
175
- cached.mtimeMs = mtimeMs;
176
- return cached;
177
- }
178
- // Grown but no complete line yet (a torn write in flight): reuse the folds; the next
179
- // request sees the completed line and folds it then.
180
- return cached;
181
- }
182
- const raw = readWindowEvents(root, fromKey);
183
- const entry = {
184
- root,
185
- fromKey,
186
- // The scan's own end, not the outer `size`: an event appended between that stat and the
187
- // read is already folded, and seeding the offset from the stale size would fold it again
188
- // on the next append (readTailTextWithEnd's coveredEnd is exactly where the scan stopped).
189
- offset: raw.liveEnd,
190
- mtimeMs,
191
- dev,
192
- ino,
193
- fileCovers: raw.coversFullWindow,
194
- hadEvents: false,
195
- days: new Map(),
196
- };
197
- for (const ev of raw.events)
198
- foldEventInto(entry, ev);
199
- if (reportFoldCache.size >= REPORT_FOLD_CACHE_MAX) {
200
- const oldest = reportFoldCache.keys().next().value;
201
- if (oldest !== undefined)
202
- reportFoldCache.delete(oldest);
203
- }
204
- reportFoldCache.set(key, entry);
205
- return entry;
206
- }
207
- /** Aggregate fleet usage over the trailing `sinceMs` window ending now, from the event log
208
- * only (tick_end/merged — see SinceReport for why the backlog files are not read). Kept a
21
+ import { foldUsageEvent, foldWindowEvents, narrowToRole, roleSum, scopedByRole, zeroCommitCounts, zeroMergeCounts, } from "./report-fold.js";
22
+ /** Aggregate team usage over the trailing `sinceMs` window ending now, from the event log
23
+ * only (run_end/merged — see SinceReport for why the backlog files are not read). Kept a
209
24
  * separate function rather than a `collectReport` variant flag: the day collector's
210
25
  * zero-filled series and backlog-file reads have no place in a window totals view. */
211
26
  export function collectReportSince(root, sinceMs, role) {
@@ -220,8 +35,19 @@ export function collectReportSince(root, sinceMs, role) {
220
35
  const fromKey = dayKey(cutoff);
221
36
  const raw = readWindowEvents(root, fromKey);
222
37
  const acc = {
223
- ticks: 0,
224
- ticksByRole: {},
38
+ runs: 0,
39
+ runsByRole: {},
40
+ rejectionsByRole: {},
41
+ revisionsByRole: {},
42
+ refusalsByRole: {},
43
+ checkDurationsMs: [],
44
+ checkFailures: 0,
45
+ mergeWaitsMs: [],
46
+ providerWaitsMs: [],
47
+ slotWaitsMs: [],
48
+ failedRunsByRole: {},
49
+ fallbacksByRole: {},
50
+ imageReroutesByRole: {},
225
51
  tokensByRole: {},
226
52
  costByRole: {},
227
53
  tokensOut: 0,
@@ -229,7 +55,14 @@ export function collectReportSince(root, sinceMs, role) {
229
55
  costUsd: 0,
230
56
  notionalCostUsd: 0,
231
57
  notionalByRole: {},
232
- ...zeroLandingCounts(),
58
+ ...zeroMergeCounts(),
59
+ mergedByRole: {},
60
+ mergeFailedByRole: {},
61
+ mergeFailedByReason: {},
62
+ costByProvider: {},
63
+ notionalByProvider: {},
64
+ runsByModel: {},
65
+ deferredByReason: {},
233
66
  };
234
67
  for (const ev of raw.events) {
235
68
  if (typeof ev.ts !== "number" || ev.ts < cutoff || ev.ts > now)
@@ -242,46 +75,81 @@ export function collectReportSince(root, sinceMs, role) {
242
75
  // the window and the render notes the possible truncation.
243
76
  const coversFullWindow = eventWindowCovers(raw, cutoff);
244
77
  // Role scope: every total derives from the fold's per-role maps for the named base role alone
245
- // — landing usage (the landing slot's, not the role's) and the fleet-only fields stay out, so
78
+ // — merge usage (the merge slot's, not the role's) and the team-only fields stay out, so
246
79
  // a scoped --since report sums exactly the same events the scoped day report would. The scope
247
80
  // is the base role, so instances (`feature-2`) aggregate into their group as the cap does.
248
81
  const base = role === undefined ? undefined : baseRoleOf(role);
249
82
  const scopedTotals = base === undefined
250
83
  ? {
251
- tokensOut: acc.tokensOut + (acc.landingTokens ?? 0),
252
- ticks: acc.ticks ?? 0,
84
+ tokensOut: acc.tokensOut + (acc.mergeTokens ?? 0),
85
+ runs: acc.runs ?? 0,
253
86
  commits: acc.commits,
254
- costUsd: acc.costUsd + (acc.landingCostUsd ?? 0),
87
+ costUsd: acc.costUsd + (acc.mergeCostUsd ?? 0),
255
88
  ...(acc.notionalCostUsd > 0 ? { notionalCostUsd: acc.notionalCostUsd } : {}),
256
- landingRuns: acc.landingRuns ?? 0,
257
- landingTokens: acc.landingTokens ?? 0,
258
- landingCostUsd: acc.landingCostUsd ?? 0,
89
+ mergeRuns: acc.mergeRuns ?? 0,
90
+ mergeTokens: acc.mergeTokens ?? 0,
91
+ mergeCostUsd: acc.mergeCostUsd ?? 0,
92
+ ...((acc.mergeFailed ?? 0) > 0 ? { mergeFailed: acc.mergeFailed } : {}),
259
93
  }
260
94
  : (() => {
261
- // The role's own notional spend, from its tick_end events; the landing slot's
262
- // notional is fleet-wide and stays out like its cost.
95
+ // The role's own notional spend, from its run_end events; the merge slot's
96
+ // notional is team-wide and stays out like its cost.
263
97
  const notional = roleSum(acc.notionalByRole, base);
264
98
  return {
265
99
  tokensOut: roleSum(acc.tokensByRole, base),
266
- ticks: roleSum(acc.ticksByRole, base),
100
+ runs: roleSum(acc.runsByRole, base),
267
101
  commits: roleSum(acc.commitsByRole, base),
268
102
  costUsd: roleSum(acc.costByRole, base),
269
103
  ...(notional > 0 ? { notionalCostUsd: notional } : {}),
270
- ...zeroLandingCounts(),
104
+ ...zeroMergeCounts(),
271
105
  };
272
106
  })();
107
+ // The --since render/JSON's Notional by role line is conditional on the window's own
108
+ // notional spend, scoped to base when a role is given — the same conditional the day
109
+ // collector applies to ReportDay.notionalByRole (line ~573), so a role-scoped window
110
+ // whose role spent no notional carries no notionalByRole instead of an empty map.
111
+ const notionalByRole = scopedByRole(acc.notionalByRole, base);
112
+ // Deferred runs are bucketed by a reason code (not a role), so a role-scoped window
113
+ // carries no entries — the same team-only rule runsByModel follows. Only set when the
114
+ // window actually deferred (zero means absent), so a deferral-free --since JSON omits it.
115
+ const deferred = scopedByRole(acc.deferredByReason, base);
273
116
  return {
274
117
  sinceMs,
275
118
  fromIso: new Date(cutoff).toISOString(),
276
119
  ...(base === undefined ? {} : { role: base }),
277
120
  totals: scopedTotals,
278
- ticksByRole: base === undefined ? acc.ticksByRole : narrowToRole(acc.ticksByRole, base),
279
- costByRole: base === undefined ? acc.costByRole : narrowToRole(acc.costByRole, base),
121
+ runsByRole: scopedByRole(acc.runsByRole, base),
122
+ costByRole: scopedByRole(acc.costByRole, base),
123
+ rejectionsByRole: scopedByRole(acc.rejectionsByRole, base),
124
+ revisionsByRole: scopedByRole(acc.revisionsByRole, base),
125
+ refusalsByRole: scopedByRole(acc.refusalsByRole, base),
126
+ failedRunsByRole: scopedByRole(acc.failedRunsByRole, base),
127
+ fallbacksByRole: scopedByRole(acc.fallbacksByRole, base),
128
+ imageReroutesByRole: scopedByRole(acc.imageReroutesByRole, base),
129
+ tokensByRole: scopedByRole(acc.tokensByRole, base),
130
+ commitsByRole: scopedByRole(acc.commitsByRole, base),
131
+ mergedByRole: scopedByRole(acc.mergedByRole, base),
132
+ mergeFailedByRole: scopedByRole(acc.mergeFailedByRole, base),
133
+ // A result code is not a role, so a role-scoped window carries an empty map (a code
134
+ // never baseRoleOf to the role) — the render gates the reason line to the team report.
135
+ mergeFailedByReason: scopedByRole(acc.mergeFailedByReason, base),
136
+ ...(Object.keys(notionalByRole).length > 0 ? { notionalByRole } : {}),
137
+ // Provider spend is a team attribution (a provider is not a role), so a role-scoped
138
+ // window carries empty maps — providers never baseRoleOf to the role — and the render
139
+ // gates the provider lines to the team report anyway. Always present, empty when none.
140
+ costByProvider: scopedByRole(acc.costByProvider, base),
141
+ notionalByProvider: scopedByRole(acc.notionalByProvider, base),
142
+ // Model attribution is team-only (a model is not a role, so no model folds to the
143
+ // scoped role), the same full-map-for-team / empty-for-scoped rule costByProvider
144
+ // follows. Always present so the render can gate on the window's own events.
145
+ runsByModel: scopedByRole(acc.runsByModel, base),
146
+ ...(Object.keys(deferred).length > 0 ? { deferredByReason: deferred } : {}),
280
147
  coversFullWindow,
281
148
  };
282
149
  }
283
- /** Aggregate fleet usage over exactly `days` local calendar days ending today, from the event
284
- * log (tick_end/merged) and the backlog history files (PLANS.md ## Done, BUGS.md ## Fixed). */
150
+ /** Aggregate team usage over exactly `days` local calendar days ending today, from the event
151
+ * log (run_end/merged) and the backlog history (`## Done` in PLANS.md and PLANS-DONE.md,
152
+ * `## Fixed` in BUGS.md and BUGS-FIXED.md). */
285
153
  export function collectReport(root, days, role) {
286
154
  const now = new Date();
287
155
  // One captured instant for the whole window, so every day key derives from the same day.
@@ -291,7 +159,7 @@ export function collectReport(root, days, role) {
291
159
  series.push({
292
160
  date: formatDate(dayAt(i, now)),
293
161
  tokensOut: 0,
294
- ticksByRole: {},
162
+ runsByRole: {},
295
163
  costByRole: {},
296
164
  ...zeroCommitCounts(),
297
165
  costUsd: 0,
@@ -309,8 +177,36 @@ export function collectReport(root, days, role) {
309
177
  continue; // Out-of-window day key (e.g. future-dated) — no series slot to fill.
310
178
  if (role === undefined) {
311
179
  day.tokensOut = fold.tokensOut;
312
- day.ticksByRole = fold.ticksByRole;
180
+ day.runsByRole = fold.runsByRole;
181
+ day.tokensByRole = fold.tokensByRole;
313
182
  day.costByRole = fold.costByRole;
183
+ // Only set when nonzero, so a rejection-free day keeps its exact pre-feature shape.
184
+ if (Object.keys(fold.rejectionsByRole).length > 0)
185
+ day.rejectionsByRole = fold.rejectionsByRole;
186
+ if (Object.keys(fold.revisionsByRole).length > 0)
187
+ day.revisionsByRole = fold.revisionsByRole;
188
+ if (Object.keys(fold.refusalsByRole).length > 0)
189
+ day.refusalsByRole = fold.refusalsByRole;
190
+ // Checks belong to the merge slot, so only the team series carries the median.
191
+ if (fold.checkDurationsMs.length > 0) {
192
+ day.checkMedianMs = lowerMedian(fold.checkDurationsMs);
193
+ }
194
+ if (fold.checkFailures > 0)
195
+ day.checkFailures = fold.checkFailures;
196
+ if (fold.mergeWaitsMs.length > 0)
197
+ day.mergeWaitMedianMs = lowerMedian(fold.mergeWaitsMs);
198
+ if (fold.providerWaitsMs.length > 0)
199
+ day.providerWaitMedianMs = lowerMedian(fold.providerWaitsMs);
200
+ // A workspace wait is the pool's, not a role's, so only the team series carries it.
201
+ if (fold.slotWaitsMs.length > 0)
202
+ day.slotWaitMedianMs = lowerMedian(fold.slotWaitsMs);
203
+ // Only set when nonzero, so a failure-free day keeps its exact pre-feature shape.
204
+ if (Object.keys(fold.failedRunsByRole).length > 0)
205
+ day.failedRunsByRole = fold.failedRunsByRole;
206
+ if (Object.keys(fold.fallbacksByRole).length > 0)
207
+ day.fallbacksByRole = fold.fallbacksByRole;
208
+ if (Object.keys(fold.imageReroutesByRole).length > 0)
209
+ day.imageReroutesByRole = fold.imageReroutesByRole;
314
210
  day.commits = fold.commits;
315
211
  day.commitsByRole = fold.commitsByRole;
316
212
  day.workCommits = fold.workCommits;
@@ -319,75 +215,178 @@ export function collectReport(root, days, role) {
319
215
  // Only set when nonzero, so zero-notional days keep their exact pre-feature shape.
320
216
  if (fold.notionalCostUsd > 0)
321
217
  day.notionalCostUsd = fold.notionalCostUsd;
322
- day.landingRuns = fold.landingRuns;
323
- day.landingTokens = fold.landingTokens;
324
- day.landingCostUsd = fold.landingCostUsd;
218
+ if (Object.keys(fold.notionalByRole).length > 0)
219
+ day.notionalByRole = fold.notionalByRole;
220
+ // Only set when a provider actually spent (zero means absent), so a provider-free day
221
+ // keeps its exact pre-feature shape and `report --json` omits the maps when empty.
222
+ if (Object.keys(fold.costByProvider).length > 0)
223
+ day.costByProvider = fold.costByProvider;
224
+ if (Object.keys(fold.notionalByProvider).length > 0)
225
+ day.notionalByProvider = fold.notionalByProvider;
226
+ // Models are a team attribution (a model is not a role), so only the team series
227
+ // carries the map; only set when a modelled start happened (zero means absent), so a
228
+ // model-less day keeps its exact pre-feature shape.
229
+ if (Object.keys(fold.runsByModel).length > 0)
230
+ day.runsByModel = fold.runsByModel;
231
+ // Deferred runs are a team attribution (a reason is not a role), so only the team
232
+ // series carries the map; only set when a day actually deferred (zero means absent),
233
+ // so a deferral-free day keeps its exact pre-feature shape.
234
+ if (Object.keys(fold.deferredByReason).length > 0)
235
+ day.deferredByReason = fold.deferredByReason;
236
+ day.mergeRuns = fold.mergeRuns;
237
+ day.mergeTokens = fold.mergeTokens;
238
+ day.mergeCostUsd = fold.mergeCostUsd;
239
+ // Only set when nonzero, so a failure-free day keeps its exact pre-feature shape.
240
+ if ((fold.mergeFailed ?? 0) > 0)
241
+ day.mergeFailed = fold.mergeFailed;
242
+ // Only set when a role actually merged/failed that day (zero means absent), so a
243
+ // merge-free day keeps its exact pre-feature shape.
244
+ if (Object.keys(fold.mergedByRole).length > 0)
245
+ day.mergedByRole = fold.mergedByRole;
246
+ if (Object.keys(fold.mergeFailedByRole).length > 0)
247
+ day.mergeFailedByRole = fold.mergeFailedByRole;
248
+ // Reason codes are a team attribution (a result is not a role), so only the team
249
+ // series carries the map; only set when nonzero, so a failure-free day keeps its
250
+ // exact pre-feature shape. The role branch below leaves it unset — merge runs are
251
+ // the merge slot's.
252
+ if (Object.keys(fold.mergeFailedByReason).length > 0)
253
+ day.mergeFailedByReason = fold.mergeFailedByReason;
325
254
  }
326
255
  else {
327
256
  // Role scope: derive this day from the fold's per-role maps, so the series carries only
328
257
  // the named role's ticks/tokens/cost and (base-role-keyed) commits. The scope is the
329
258
  // base role, so an instance id's usage and its siblings' commits fold into one group and
330
- // the numbers match the per-role cap, which is base-role keyed too. The landing fields
331
- // stay unset — landing runs are the landing slot's, not this role's — and the work /
259
+ // the numbers match the per-role cap, which is base-role keyed too. The merge fields
260
+ // stay unset — merge runs are the merge slot's, not this role's — and the work /
332
261
  // maintenance split follows the base role's own tier rather than the day's whole split.
333
262
  const base = baseRoleOf(role);
334
263
  const commits = roleSum(fold.commitsByRole, base);
335
264
  const tier = commitTier(base);
336
265
  day.tokensOut = roleSum(fold.tokensByRole, base);
337
- day.ticksByRole = narrowToRole(fold.ticksByRole, base);
266
+ day.runsByRole = narrowToRole(fold.runsByRole, base);
267
+ day.tokensByRole = narrowToRole(fold.tokensByRole, base);
338
268
  day.costByRole = narrowToRole(fold.costByRole, base);
269
+ const rejs = narrowToRole(fold.rejectionsByRole, base);
270
+ if (Object.keys(rejs).length > 0)
271
+ day.rejectionsByRole = rejs;
272
+ const revs = narrowToRole(fold.revisionsByRole, base);
273
+ if (Object.keys(revs).length > 0)
274
+ day.revisionsByRole = revs;
275
+ const refusals = narrowToRole(fold.refusalsByRole, base);
276
+ if (Object.keys(refusals).length > 0)
277
+ day.refusalsByRole = refusals;
278
+ const fails = narrowToRole(fold.failedRunsByRole, base);
279
+ if (Object.keys(fails).length > 0)
280
+ day.failedRunsByRole = fails;
281
+ const falls = narrowToRole(fold.fallbacksByRole, base);
282
+ if (Object.keys(falls).length > 0)
283
+ day.fallbacksByRole = falls;
284
+ const reroutes = narrowToRole(fold.imageReroutesByRole, base);
285
+ if (Object.keys(reroutes).length > 0)
286
+ day.imageReroutesByRole = reroutes;
339
287
  day.commits = commits;
340
288
  day.commitsByRole = narrowToRole(fold.commitsByRole, base);
341
289
  day.workCommits = tier === "work" ? commits : 0;
342
290
  day.maintenanceCommits = tier === "maintenance" ? commits : 0;
343
291
  day.costUsd = roleSum(fold.costByRole, base);
344
292
  const notional = roleSum(fold.notionalByRole, base);
345
- if (notional > 0)
293
+ if (notional > 0) {
346
294
  day.notionalCostUsd = notional;
295
+ day.notionalByRole = narrowToRole(fold.notionalByRole, base);
296
+ }
297
+ // Merge runs belong to the merge slot, not this role, so the role-scoped series
298
+ // keeps them unset (matching the merge-share fields) — the render omits the merge
299
+ // line for a scoped report, so these are carried only for shape consistency.
300
+ const mergeDone = narrowToRole(fold.mergedByRole, base);
301
+ if (Object.keys(mergeDone).length > 0)
302
+ day.mergedByRole = mergeDone;
303
+ const mergeDoneFailed = narrowToRole(fold.mergeFailedByRole, base);
304
+ if (Object.keys(mergeDoneFailed).length > 0)
305
+ day.mergeFailedByRole = mergeDoneFailed;
347
306
  }
348
307
  }
349
- // The landing share joins its day's tokensOut/costUsd (the budget charges these same
308
+ // The merge share joins its day's tokensOut/costUsd (the budget charges these same
350
309
  // events, so the day series must sum to the totals the budget agrees with) while the
351
- // landing fields keep the reviewer's part of it visible on its own.
310
+ // merge fields keep the reviewer's part of it visible on its own.
352
311
  for (const d of series) {
353
- d.tokensOut += d.landingTokens ?? 0;
354
- d.costUsd += d.landingCostUsd ?? 0;
312
+ d.tokensOut += d.mergeTokens ?? 0;
313
+ d.costUsd += d.mergeCostUsd ?? 0;
355
314
  }
356
315
  const countOn = (date, field) => {
357
316
  const day = byDate.get(date);
358
317
  if (day)
359
- day[field] += 1; // Out-of-window dates drop out here.
318
+ day[field] = (day[field] ?? 0) + 1; // Out-of-window dates drop out here.
360
319
  };
361
- // Role scope leaves both tallies at zero: their dates are day-granular file metadata with no
362
- // role, so a scoped report cannot attribute a completion (the render states the omission).
320
+ // Role scope leaves the day-granular backlog tallies at zero: their dates are day-granular
321
+ // file metadata with no role, so a scoped report cannot attribute a completion or a filing
322
+ // (the render states the omission).
323
+ if (role === undefined) {
324
+ // Finished entries sit in the live file's finished section and in its archive file
325
+ // (plans/backlog-archive.md); each per-file read is stat-cached.
326
+ for (const file of finishedFiles("PLANS.md")) {
327
+ for (const d of sectionCompletionDates(root, file, "Done", DONE_DATE_RE))
328
+ countOn(d, "featuresDone");
329
+ }
330
+ for (const file of finishedFiles("BUGS.md")) {
331
+ for (const d of sectionCompletionDates(root, file, "Fixed", FIXED_DATE_RE))
332
+ countOn(d, "bugsFixed");
333
+ }
334
+ // Fold the filing stamps too: plans/bugs that opened this day, over the same entry
335
+ // detection `entryDates` applies to the completion reads, so an entry's opened and done
336
+ // days are read the same way.
337
+ for (const d of sectionFilingDates(root, "PLANS.md", "Planned"))
338
+ countOn(d, "plansAdded");
339
+ for (const file of finishedFiles("PLANS.md")) {
340
+ for (const d of sectionFilingDates(root, file, "Done"))
341
+ countOn(d, "plansAdded");
342
+ }
343
+ for (const d of sectionFilingDates(root, "BUGS.md", "Open"))
344
+ countOn(d, "bugsFiled");
345
+ for (const file of finishedFiles("BUGS.md")) {
346
+ for (const d of sectionFilingDates(root, file, "Fixed"))
347
+ countOn(d, "bugsFiled");
348
+ }
349
+ }
350
+ // Tally the fixed bugs' `Validation gap` tags over the window, so the team can see where
351
+ // its own tests were weakest. An entry counts only when it is in the window (the same
352
+ // out-of-window drop countOn applies: its completion date must be a key of byDate), and
353
+ // only the team report carries the tally — a tag has no role (the render gates on the
354
+ // same `role === undefined`). Zero-means-absent: a window with no tagged fixed bug spreads
355
+ // no key, so the byte-for-byte assertions on pre-feature windows hold.
356
+ const validationGaps = {};
363
357
  if (role === undefined) {
364
- for (const d of sectionCompletionDates(root, "PLANS.md", "Done", /done (\d{4}-\d{2}-\d{2})/))
365
- countOn(d, "featuresDone");
366
- // The completion verbs track the phrasing the harness's own loops actually write: the
367
- // bugfix prompt pins the date, not the verb, and the re-land flow's own epitaph —
368
- // "re-landed <date>" on an entry whose first landing was rejected in review — is a
369
- // completion the report must count (a rejected landing is not a fix, the re-land is).
370
- // Body verbs without a date ("was fixed;") match nothing, and a bare "landed <date>"
371
- // stays out: headings mention sibling landings, and a sibling's date is not this entry's.
372
- for (const d of sectionCompletionDates(root, "BUGS.md", "Fixed", /\b(?:re-landed|fixed|closed|resolved) (\d{4}-\d{2}-\d{2})/))
373
- countOn(d, "bugsFixed");
358
+ for (const e of fixedBugEntries(root)) {
359
+ const date = completionDate(e.title, FIXED_DATE_RE);
360
+ if (date === null || !byDate.has(date))
361
+ continue;
362
+ const tag = validationGapTag(e.body);
363
+ if (tag !== null) {
364
+ addTo(validationGaps, tag, 1);
365
+ addTo((byDate.get(date).validationGapsByTag ??= {}), tag, 1);
366
+ }
367
+ }
374
368
  }
375
369
  const totals = {
376
370
  tokensOut: 0,
377
- ticks: 0,
371
+ runs: 0,
378
372
  ...zeroCommitCounts(),
379
373
  costUsd: 0,
380
374
  featuresDone: 0,
381
375
  bugsFixed: 0,
382
- ...zeroLandingCounts(),
376
+ ...zeroMergeCounts(),
383
377
  };
384
378
  // Notional spend is kept out of the totals object until it is nonzero, so a pre-feature
385
- // window's totals keep their exact shape.
379
+ // window's totals keep their exact shape. Merge failures likewise: a failure-free window
380
+ // carries no mergeFailed (zero means absent, like the merge-share line's other fields
381
+ // are folded in below).
386
382
  let notionalCostUsd = 0;
383
+ let mergeFailed = 0;
384
+ let plansAdded = 0;
385
+ let bugsFiled = 0;
387
386
  for (const d of series) {
388
387
  totals.tokensOut += d.tokensOut;
389
- for (const n of Object.values(d.ticksByRole))
390
- totals.ticks += n;
388
+ for (const n of Object.values(d.runsByRole))
389
+ totals.runs += n;
391
390
  totals.commits += d.commits;
392
391
  for (const [role, n] of Object.entries(d.commitsByRole ?? {}))
393
392
  addTo(totals.commitsByRole, role, n);
@@ -397,9 +396,12 @@ export function collectReport(root, days, role) {
397
396
  notionalCostUsd += d.notionalCostUsd ?? 0;
398
397
  totals.featuresDone += d.featuresDone;
399
398
  totals.bugsFixed += d.bugsFixed;
400
- totals.landingRuns += d.landingRuns ?? 0;
401
- totals.landingTokens += d.landingTokens ?? 0;
402
- totals.landingCostUsd += d.landingCostUsd ?? 0;
399
+ plansAdded += d.plansAdded ?? 0;
400
+ bugsFiled += d.bugsFiled ?? 0;
401
+ totals.mergeRuns += d.mergeRuns ?? 0;
402
+ totals.mergeTokens += d.mergeTokens ?? 0;
403
+ totals.mergeCostUsd += d.mergeCostUsd ?? 0;
404
+ mergeFailed += d.mergeFailed ?? 0;
403
405
  }
404
406
  // The shared coverage proof (event-window.ts), rebuilt from the memoized read's pinned
405
407
  // parts: the day-keyed proof (fileCovers — a property of the retained bytes and fromKey,
@@ -408,7 +410,7 @@ export function collectReport(root, days, role) {
408
410
  // earliest the window could contain, so the same-day arm degenerates harmlessly. Without
409
411
  // this field the day report was the one windowed consumer that could undercount in
410
412
  // silence — the --since report and the failure digest both carry the note, and a silent
411
- // number invites an operator to trust a truncated window as an idle fleet.
413
+ // number invites an operator to trust a truncated window as an idle team.
412
414
  const cutoffMs = dayAt(days - 1, now).getTime();
413
415
  const coversFullWindow = foldEntry.fileCovers ||
414
416
  !foldEntry.hadEvents ||
@@ -419,7 +421,14 @@ export function collectReport(root, days, role) {
419
421
  to,
420
422
  ...(role === undefined ? {} : { role: baseRoleOf(role) }),
421
423
  series,
422
- totals: { ...totals, ...(notionalCostUsd > 0 ? { notionalCostUsd } : {}) },
424
+ totals: {
425
+ ...totals,
426
+ ...(notionalCostUsd > 0 ? { notionalCostUsd } : {}),
427
+ ...(mergeFailed > 0 ? { mergeFailed } : {}),
428
+ ...(plansAdded > 0 ? { plansAdded } : {}),
429
+ ...(bugsFiled > 0 ? { bugsFiled } : {}),
430
+ ...(Object.keys(validationGaps).length > 0 ? { validationGapsByTag: validationGaps } : {}),
431
+ },
423
432
  coversFullWindow,
424
433
  };
425
434
  }