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
@@ -0,0 +1,353 @@
1
+ // vocabulary: plain
2
+ /** Event-log folding for the usage report: turn raw harness events (run_end/merged/…) into
3
+ * per-day and whole-window `UsageFold` accumulators, memoizing the day-keyed read per
4
+ * (root, fromKey) so a dashboard polling /api/report only folds appended lines instead of
5
+ * re-reading the window. The two window collectors (collectReport / collectReportSince) in
6
+ * src/report/report-data.ts fold through this one shared layer, so the aggregation rules the
7
+ * --since and day windows present cannot drift apart; it lives here matching its own test
8
+ * (test/report-fold-cache.test.ts). Zero incidental runtime dependencies: node built-ins and
9
+ * the app's own modules only. */
10
+ import { sameStatIdentity, statOrNull } from "../files/files.js";
11
+ import { 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 { isPassingFullCheck, isRedFullCheck } from "../build/build-check-events.js";
16
+ import { addTo, memoSetLru } from "../collections.js";
17
+ import { finiteNumber } from "../files/json-object.js";
18
+ import { OUTCOME_CLASS } from "../failure/time-spend.js";
19
+ import { baseRoleOf } from "../roles/loop-ids.js";
20
+ import { commitTier } from "../roles/roles.js";
21
+ /** Fold one event's `providerCost` map (`Record<provider, {costUsd, notionalCostUsd}>` — the
22
+ * shape tick-finalize.ts's run_end and merge-slot.ts's merge_done/merge_failed emit) into the
23
+ * per-provider tallies, one addTo per provider per nonzero figure so zero stays absent. A
24
+ * missing, foreign, or empty map folds nothing, so old logs render byte-identically. */
25
+ function foldProviderCost(target, providerCost) {
26
+ if (typeof providerCost !== "object" || providerCost === null)
27
+ return;
28
+ for (const [provider, c] of Object.entries(providerCost)) {
29
+ if (typeof c !== "object" || c === null)
30
+ continue;
31
+ const entry = c;
32
+ const costUsd = finiteNumber(entry.costUsd, 0);
33
+ const notionalCostUsd = finiteNumber(entry.notionalCostUsd, 0);
34
+ if (costUsd !== 0)
35
+ addTo(target.costByProvider, provider, costUsd);
36
+ if (notionalCostUsd !== 0)
37
+ addTo(target.notionalByProvider, provider, notionalCostUsd);
38
+ }
39
+ }
40
+ /** Push a provider- or workspace-slot wait's ms into the accumulator list a median reads,
41
+ * when the event carried a positive wait: a wait is only event-logged at or above the 30 s
42
+ * threshold, so a 0 ms wait (a probe seam) or a pre-waitedMs event adds nothing and the two
43
+ * wait medians reflect real blocked admissions only. */
44
+ function pushPositiveWaitedMs(list, ev) {
45
+ const waited = finiteNumber(ev.waitedMs, 0);
46
+ if (waited > 0)
47
+ list.push(waited);
48
+ }
49
+ /** Fold one event into a usage accumulator: run_end events add a tick (per role and, when
50
+ * the target counts ticks, in total), their output tokens, and their cost — zero-cost ticks
51
+ * contribute nothing, so they leave no costByRole key, keeping the render's "$0 roles are
52
+ * omitted" rule true on the aggregation itself, not just at display time; merged events add
53
+ * a commit; merge_done/merge_failed events add a merge run and
54
+ * accumulate their tokens/cost into the merge fields only — costByRole stays a ticks-only
55
+ * breakdown, and the merge line is how the reviewer's share is named. Both collectors fold
56
+ * through this one place, so the two windows' aggregation rules cannot drift apart. */
57
+ export function foldUsageEvent(target, ev) {
58
+ if (ev.type === "run_end") {
59
+ const role = eventRole(ev);
60
+ addTo(target.runsByRole, role, 1);
61
+ // Error-class ticks get their own parallel tally — the day report's ``Failed ticks by role``
62
+ // line — reusing the digest's OUTCOME_CLASS so "failed" cannot drift from the failure
63
+ // report's meaning. Never subtracted from runsByRole; the two are independent counts.
64
+ if (OUTCOME_CLASS[ev.result] === "error")
65
+ addTo(target.failedRunsByRole, role, 1);
66
+ // A refusal is its own deliberate outcome (src/verdict/refusal.ts), not error-class, so
67
+ // it needs an explicit result check: the one per-role rejection-without-review count a
68
+ // day report should surface beside rejections, revisions and falls. Never subtracted from
69
+ // runsByRole; a refused tick still counts as a run.
70
+ if (ev.result === "refused")
71
+ addTo(target.refusalsByRole, role, 1);
72
+ if (target.runs !== undefined)
73
+ target.runs++;
74
+ const { tokens, costUsd, notionalCostUsd } = eventUsage(ev);
75
+ addTo(target.tokensByRole, role, tokens);
76
+ target.tokensOut += tokens;
77
+ target.costUsd += costUsd;
78
+ target.notionalCostUsd += notionalCostUsd;
79
+ if (costUsd !== 0)
80
+ addTo(target.costByRole, role, costUsd);
81
+ if (notionalCostUsd !== 0)
82
+ addTo(target.notionalByRole, role, notionalCostUsd);
83
+ // The tick's own runs' per-provider spend (tick-finalize.ts emits providerCost on run_end)
84
+ // — folded per provider beside the per-role maps, so a mixed team's `Cost by provider`
85
+ // line names the expensive seam.
86
+ foldProviderCost(target, ev.providerCost);
87
+ }
88
+ else if (ev.type === "review_rejected") {
89
+ // One rejection per event, on the day it happened, per loop. Zero-means-absent: a role
90
+ // that never rejected stays absent, like costByRole.
91
+ addTo(target.rejectionsByRole, eventRole(ev), 1);
92
+ }
93
+ else if (ev.type === "revision") {
94
+ // One rework round per event, on the day it happened, per loop. Zero-means-absent: a role
95
+ // that never reworked a rejected change stays absent, like rejectionsByRole.
96
+ addTo(target.revisionsByRole, eventRole(ev), 1);
97
+ }
98
+ else if (isPassingFullCheck(ev)) {
99
+ // The merge slot's passing full-suite checks, for the day report's check-time median.
100
+ const durationMs = finiteNumber(ev.durationMs, 0);
101
+ if (durationMs > 0)
102
+ target.checkDurationsMs.push(durationMs);
103
+ }
104
+ else if (isRedFullCheck(ev)) {
105
+ // The merge slot's red full-suite checks, for the day report's red-check count.
106
+ target.checkFailures++;
107
+ }
108
+ else if (ev.type === "model_fallback_started") {
109
+ // One episode per start event; durations are not summed (an episode can span the window edge).
110
+ addTo(target.fallbacksByRole, eventRole(ev), 1);
111
+ }
112
+ else if (ev.type === "image_reroute") {
113
+ // One reroute per event, preflight or after a blind read.
114
+ addTo(target.imageReroutesByRole, eventRole(ev), 1);
115
+ }
116
+ else if (ev.type === "run_deferred") {
117
+ // One deferral per event, a due maintenance tick that stood down. Bucket by the raw
118
+ // reason code ("backlog", "plans-full", else "no-work" for a missing/foreign/torn
119
+ // reason — the same fallback deferredReasonText applies in the feed), so the collector
120
+ // stays free of a presentation import and the render prints the code the event carried.
121
+ // Team-only: a reason is not a role.
122
+ const reason = ev.reason === "backlog" || ev.reason === "plans-full" ? ev.reason : "no-work";
123
+ addTo(target.deferredByReason, reason, 1);
124
+ }
125
+ else if (ev.type === "merged") {
126
+ target.commits++;
127
+ // Work ratio 4/4: split merged commits by base role and by tier. Instance suffixes fold
128
+ // into their role (`feature-2` counts as feature) so the split does not fragment.
129
+ const role = baseRoleOf(eventRole(ev));
130
+ addTo(target.commitsByRole, role, 1);
131
+ const tier = commitTier(role);
132
+ if (tier === "work")
133
+ target.workCommits++;
134
+ else if (tier === "maintenance")
135
+ target.maintenanceCommits++;
136
+ }
137
+ else if (ev.type === "run_start") {
138
+ // One tally per run start on a configured model: the same `model` field the CLI's
139
+ // `run #N started on <model>` line prints, and the only event that carries a model
140
+ // (run_end has none), so a run that started with no configured model contributes no key.
141
+ const model = typeof ev.model === "string" && ev.model.length > 0 ? ev.model : null;
142
+ if (model !== null)
143
+ addTo(target.runsByModel, model, 1);
144
+ }
145
+ else if (ev.type === "provider_wait") {
146
+ // The day report's provider-slot wait median, mirroring the slot_wait push: a wait only
147
+ // event-logged at or above the 30 s threshold counts, so a 0 ms wait (a probe seam) adds
148
+ // nothing and the median reflects real blocked admissions only.
149
+ pushPositiveWaitedMs(target.providerWaitsMs, ev);
150
+ }
151
+ else if (ev.type === "slot_wait") {
152
+ // The day report's workspace-slot wait median, mirroring the provider_wait push: a wait
153
+ // only event-logged at or above the 30 s threshold counts, so a 0 ms wait (a probe seam)
154
+ // or a pre-waitedMs event adds nothing.
155
+ pushPositiveWaitedMs(target.slotWaitsMs, ev);
156
+ }
157
+ else if (ev.type === "merge_done" || ev.type === "merge_failed") {
158
+ target.mergeRuns = (target.mergeRuns ?? 0) + 1;
159
+ if (ev.type === "merge_failed")
160
+ target.mergeFailed = (target.mergeFailed ?? 0) + 1;
161
+ // Attribute the outcome to the authoring role (writeMergeOutcome logs loop: entry.role)
162
+ // folded to base role, the same normalization the merged branch uses, so `feature-2`
163
+ // aggregates into `feature`. A parallel tally beside the team run counts, never
164
+ // subtracted from mergeRuns/mergeFailed.
165
+ const role = baseRoleOf(eventRole(ev));
166
+ if (ev.type === "merge_done") {
167
+ addTo(target.mergedByRole, role, 1);
168
+ // Events logged before queuedMs existed carry none: skipped, not counted as zero.
169
+ const waited = finiteNumber(ev.queuedMs, 0);
170
+ if (waited > 0)
171
+ target.mergeWaitsMs.push(waited);
172
+ }
173
+ else {
174
+ addTo(target.mergeFailedByRole, role, 1);
175
+ // The event's result (writeMergeOutcome's TickResult) names WHY the merge bounced
176
+ // off the gate — rejected/merge_conflict/merge_blocked/aborted. A legacy event with
177
+ // no result folds under `unknown`, so the reason buckets always sum to mergeFailed.
178
+ const reason = typeof ev.result === "string" && ev.result.length > 0 ? ev.result : "unknown";
179
+ addTo(target.mergeFailedByReason, reason, 1);
180
+ }
181
+ const { tokens, costUsd, notionalCostUsd } = eventUsage(ev);
182
+ target.mergeTokens = (target.mergeTokens ?? 0) + tokens;
183
+ target.mergeCostUsd = (target.mergeCostUsd ?? 0) + costUsd;
184
+ // A subscription merge's API-equivalent spend joins the notional line, never the
185
+ // budgeted cost totals (plans/agent-backends.md part 6a).
186
+ target.notionalCostUsd += notionalCostUsd;
187
+ // The merge's per-provider spend (merge-slot.ts keys it to its reviewer seam), so a
188
+ // merge's costs appear under the provider that ran the reviewer, matching the author
189
+ // side's foldMergeUsage charge.
190
+ foldProviderCost(target, ev.providerCost);
191
+ }
192
+ }
193
+ /** Bounded memo of window folds. Eight entries cover a dashboard flipping among a few day
194
+ * counts plus the midnight rollover's new fromKey; oldest-insertion eviction (Map preserves
195
+ * insertion order) keeps the memory bound to a handful of per-day accumulator maps. */
196
+ const reportFoldCache = new Map();
197
+ const REPORT_FOLD_CACHE_MAX = 8;
198
+ /** The empty commit-family counters every accumulator starts from: the total and per-base-role
199
+ * merged-commit count plus the work/maintenance split (Work ratio 4/4). Four shapes carry them —
200
+ * DayFold, UsageFold, the ReportDay series slots, and the window totals — so the empty quartet
201
+ * lives once here and a new commit counter is added in one place. */
202
+ export function zeroCommitCounts() {
203
+ return { commits: 0, commitsByRole: {}, workCommits: 0, maintenanceCommits: 0 };
204
+ }
205
+ /** The empty merge-run counters every accumulator starts from: the merge slot's run count
206
+ * and its own token/cost share (foldUsageEvent's merge_done/merge_failed branch). Four shapes carry
207
+ * them — DayFold, UsageFold, the scoped window totals, and the window totals — so the empty
208
+ * triple lives once here and a new merge counter is added in one place. */
209
+ export function zeroMergeCounts() {
210
+ return { mergeRuns: 0, mergeTokens: 0, mergeCostUsd: 0 };
211
+ }
212
+ /** Sum a per-loop-id count/cost map over one base role: instance keys (`feature-2`) fold into
213
+ * their base (`feature`), so a scoped report aggregates a whole role group exactly as the
214
+ * per-role cap's group does. Ticks/tokens/cost are keyed by exact loop id while commits are
215
+ * already base-role keyed; running every key through `baseRoleOf` reads both shapes the same
216
+ * and returns 0 when nothing matches. */
217
+ export function roleSum(record, role) {
218
+ let sum = 0;
219
+ for (const [key, n] of Object.entries(record)) {
220
+ if (baseRoleOf(key) === role)
221
+ sum += n;
222
+ }
223
+ return sum;
224
+ }
225
+ /** Narrow a per-loop-id count/cost map to one base role, keyed by the base role; a role absent
226
+ * from the map (no ticks, or no nonzero cost) yields an empty map, never a zero entry — the
227
+ * same zero-means-absent rule the team render applies. Used by the role-scoped collectors so a
228
+ * scoped series derives from the fold's per-role maps without inventing entries. */
229
+ export function narrowToRole(record, role) {
230
+ const n = roleSum(record, role);
231
+ return n === 0 ? {} : { [role]: n };
232
+ }
233
+ /** A per-loop map scoped to one base role for a report: the full map as-is when no scope is
234
+ * set (a team report), else the same map narrowed to `base` — narrowToRole's zero-means-absent
235
+ * rule, so a scoped report never invents a zero entry. The one home of the "keep everything,
236
+ * or keep only the named base role" rule: the 13 per-role/pair fields of the `--since` report's
237
+ * output all shape through it, so the scoping rule cannot drift between them. */
238
+ export function scopedByRole(record, base) {
239
+ return base === undefined ? record : narrowToRole(record, base);
240
+ }
241
+ function dayFoldFor(entry, dayKey) {
242
+ let fold = entry.days.get(dayKey);
243
+ if (!fold) {
244
+ fold = {
245
+ tokensOut: 0,
246
+ ...zeroCommitCounts(),
247
+ costUsd: 0,
248
+ notionalCostUsd: 0,
249
+ runsByRole: {},
250
+ rejectionsByRole: {},
251
+ revisionsByRole: {},
252
+ refusalsByRole: {},
253
+ checkDurationsMs: [],
254
+ checkFailures: 0,
255
+ mergeWaitsMs: [],
256
+ providerWaitsMs: [],
257
+ slotWaitsMs: [],
258
+ failedRunsByRole: {},
259
+ fallbacksByRole: {},
260
+ imageReroutesByRole: {},
261
+ tokensByRole: {},
262
+ costByRole: {},
263
+ notionalByRole: {},
264
+ ...zeroMergeCounts(),
265
+ mergedByRole: {},
266
+ mergeFailedByRole: {},
267
+ mergeFailedByReason: {},
268
+ costByProvider: {},
269
+ notionalByProvider: {},
270
+ runsByModel: {},
271
+ deferredByReason: {},
272
+ };
273
+ entry.days.set(dayKey, fold);
274
+ }
275
+ return fold;
276
+ }
277
+ /** Fold one already-parsed event into a cache entry: only in-window events (a non-null day
278
+ * on or after fromKey) count, matching readWindowEvents's event list; the first such event
279
+ * seen takes firstEventTs. The one home of the fold body, shared by the incremental append
280
+ * path (foldLineInto) and the full re-read path, so the two can never drift. */
281
+ function foldEventInto(entry, ev) {
282
+ const dayKey = eventDayKey(ev);
283
+ if (dayKey === null || dayKey < entry.fromKey)
284
+ return;
285
+ if (!entry.hadEvents) {
286
+ entry.hadEvents = true;
287
+ entry.firstEventTs = typeof ev.ts === "number" ? ev.ts : undefined;
288
+ }
289
+ foldUsageEvent(dayFoldFor(entry, dayKey), ev);
290
+ }
291
+ /** Parse one appended log line and fold it; a torn or non-event line folds nothing. */
292
+ function foldLineInto(entry, line) {
293
+ const ev = parseEventLine(line);
294
+ if (ev)
295
+ foldEventInto(entry, ev);
296
+ }
297
+ /** The event-log half of collectReport, memoized: a dashboard re-fetching /api/report every
298
+ * poll re-read and re-parsed the whole window's log bytes each time (~6.8 MB and tens of ms
299
+ * on this repo's own day log, growing toward the 16 MB rotation cap). Now a request whose
300
+ * log has not changed costs one stat; growth folds only the appended complete lines
301
+ * (readCompleteLines holds back a torn trailing write, so it is folded whole next call).
302
+ * Same size with a changed mtime, a shrunken file (rotation or rewrite), or a new fromKey
303
+ * falls back to the full readWindowEvents pass — the fallback re-derives everything, so a
304
+ * mis-fold can only come from the append assumption, and rotation's whole-file replace
305
+ * always hits the shrink arm. The entry's coverage parts are pinned by the cache key:
306
+ * fromKey and file bytes decide them, so an advancing window re-derives fresh instead of
307
+ * carrying yesterday's proof forward. */
308
+ export function foldWindowEvents(root, fromKey) {
309
+ const key = `${root}\0${fromKey}`;
310
+ const liveStat = statOrNull(eventsLogPath(root));
311
+ const size = liveStat?.size ?? 0;
312
+ const mtimeMs = liveStat?.mtimeMs ?? 0;
313
+ const dev = liveStat?.dev ?? 0;
314
+ const ino = liveStat?.ino ?? 0;
315
+ const cached = reportFoldCache.get(key);
316
+ if (cached &&
317
+ sameStatIdentity({ dev, ino, mtimeMs, size }, { dev: cached.dev, ino: cached.ino, mtimeMs: cached.mtimeMs, size: cached.offset })) {
318
+ memoSetLru(reportFoldCache, key, cached, REPORT_FOLD_CACHE_MAX);
319
+ return cached;
320
+ }
321
+ if (cached && size > cached.offset && dev === cached.dev && ino === cached.ino) {
322
+ const { lines, end } = readCompleteLines(eventsLogPath(root), cached.offset, size);
323
+ if (end > cached.offset) {
324
+ for (const line of lines)
325
+ foldLineInto(cached, line);
326
+ cached.offset = end;
327
+ cached.mtimeMs = mtimeMs;
328
+ return cached;
329
+ }
330
+ // Grown but no complete line yet (a torn write in flight): reuse the folds; the next
331
+ // request sees the completed line and folds it then.
332
+ return cached;
333
+ }
334
+ const raw = readWindowEvents(root, fromKey);
335
+ const entry = {
336
+ root,
337
+ fromKey,
338
+ // The scan's own end, not the outer `size`: an event appended between that stat and the
339
+ // read is already folded, and seeding the offset from the stale size would fold it again
340
+ // on the next append (readTailTextWithEnd's coveredEnd is exactly where the scan stopped).
341
+ offset: raw.liveEnd,
342
+ mtimeMs,
343
+ dev,
344
+ ino,
345
+ fileCovers: raw.coversFullWindow,
346
+ hadEvents: false,
347
+ days: new Map(),
348
+ };
349
+ for (const ev of raw.events)
350
+ foldEventInto(entry, ev);
351
+ memoSetLru(reportFoldCache, key, entry, REPORT_FOLD_CACHE_MAX);
352
+ return entry;
353
+ }