tumwater 0.2.0 → 0.4.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 (464) hide show
  1. package/README.md +27 -13
  2. package/dist/build-info.json +2 -2
  3. package/dist/src/agent/agent-args.js +8 -9
  4. package/dist/src/agent/agent-backend.js +8 -6
  5. package/dist/src/agent/backend-traits.js +55 -25
  6. package/dist/src/agent/claude/claude-account.js +71 -204
  7. package/dist/src/agent/claude/claude-args.js +9 -6
  8. package/dist/src/agent/claude/claude-backend.js +10 -10
  9. package/dist/src/agent/claude/claude-env.js +1 -1
  10. package/dist/src/agent/claude/claude-stream.js +6 -4
  11. package/dist/src/agent/claude/claude-write-guard.js +49 -0
  12. package/dist/src/agent/claude/write-guard-hook.js +13 -0
  13. package/dist/src/agent/codex/codex-account.js +119 -0
  14. package/dist/src/agent/codex/codex-args.js +181 -0
  15. package/dist/src/agent/codex/codex-backend.js +279 -0
  16. package/dist/src/agent/codex/codex-catalog.js +97 -0
  17. package/dist/src/agent/codex/codex-env.js +18 -0
  18. package/dist/src/agent/codex/codex-hook-trust.js +246 -0
  19. package/dist/src/agent/codex/codex-patch-paths.js +36 -0
  20. package/dist/src/agent/codex/codex-rollout.js +179 -0
  21. package/dist/src/agent/codex/codex-stream.js +404 -0
  22. package/dist/src/agent/codex/codex-write-guard.js +86 -0
  23. package/dist/src/agent/codex/write-guard-hook.js +12 -0
  24. package/dist/src/agent/image-input.js +29 -24
  25. package/dist/src/agent/notes-shim.js +11 -12
  26. package/dist/src/agent/opencode/opencode-args.js +0 -1
  27. package/dist/src/agent/opencode/opencode-backend.js +12 -9
  28. package/dist/src/agent/opencode/opencode-catalog.js +55 -29
  29. package/dist/src/agent/opencode/opencode-config.js +19 -14
  30. package/dist/src/agent/opencode/opencode-env.js +0 -1
  31. package/dist/src/agent/opencode/opencode-session-id.js +1 -2
  32. package/dist/src/agent/opencode/opencode-stream.js +1 -1
  33. package/dist/src/agent/opencode/write-guard-plugin/index.js +48 -0
  34. package/dist/src/agent/role-notes-write.js +5 -6
  35. package/dist/src/agent/run-agent.js +465 -448
  36. package/dist/src/agent/run-settle.js +214 -0
  37. package/dist/src/agent/session-ledger.js +86 -16
  38. package/dist/src/agent/subscription-account.js +220 -0
  39. package/dist/src/agent/write-guard.js +148 -0
  40. package/dist/src/backlog/backlog-archive.js +281 -55
  41. package/dist/src/backlog/backlog-cli-view.js +255 -0
  42. package/dist/src/backlog/backlog-cli.js +91 -335
  43. package/dist/src/backlog/backlog-commit.js +20 -13
  44. package/dist/src/backlog/backlog-eligibility.js +193 -242
  45. package/dist/src/backlog/backlog-index.js +149 -0
  46. package/dist/src/backlog/backlog-md.js +54 -6
  47. package/dist/src/backlog/backlog-parts.js +264 -0
  48. package/dist/src/backlog/backlog-release.js +134 -0
  49. package/dist/src/backlog/backlog-render.js +27 -0
  50. package/dist/src/backlog/backlog-structure.js +99 -156
  51. package/dist/src/backlog/backlog-write.js +40 -127
  52. package/dist/src/backlog/backlog.js +21 -6
  53. package/dist/src/backlog/question-answer.js +21 -15
  54. package/dist/src/baseline/main-baseline.js +89 -20
  55. package/dist/src/baseline/main-guardrails.js +136 -0
  56. package/dist/src/baseline/main-red.js +153 -36
  57. package/dist/src/baseline/main-watch.js +232 -0
  58. package/dist/src/brief/brief-edit.js +12 -12
  59. package/dist/src/brief/brief.js +31 -51
  60. package/dist/src/budget/budget-fallback-view.js +288 -0
  61. package/dist/src/budget/budget-wiring.js +158 -0
  62. package/dist/src/budget/budget.js +31 -20
  63. package/dist/src/budget/fallback-breaker.js +26 -27
  64. package/dist/src/build/build-check-detect.js +23 -15
  65. package/dist/src/build/build-check-events.js +25 -6
  66. package/dist/src/build/build-check-report.js +49 -9
  67. package/dist/src/build/build-check-scoped.js +18 -11
  68. package/dist/src/build/build-check.js +2 -1
  69. package/dist/src/build/build-info.js +2 -3
  70. package/dist/src/build/build-stage.js +9 -14
  71. package/dist/src/build/dep-install.js +65 -3
  72. package/dist/src/change/change-data.js +123 -31
  73. package/dist/src/change/change-render.js +46 -5
  74. package/dist/src/cli/brief-command.js +3 -4
  75. package/dist/src/cli/cli-args.js +181 -33
  76. package/dist/src/cli/cli-command-args.js +46 -18
  77. package/dist/src/cli/cli-editor.js +2 -1
  78. package/dist/src/cli/cli-flag-specs.js +183 -45
  79. package/dist/src/cli/cli-init.js +51 -0
  80. package/dist/src/cli/cli-marker-commands.js +4 -4
  81. package/dist/src/cli/cli-query-commands.js +291 -598
  82. package/dist/src/cli/cli-repo-gate.js +11 -1
  83. package/dist/src/cli/cli-review-commands.js +113 -0
  84. package/dist/src/cli/cli-role-command.js +525 -0
  85. package/dist/src/cli/cli-start-summary.js +167 -0
  86. package/dist/src/cli/{cli-run.js → cli-start.js} +193 -237
  87. package/dist/src/cli/completion.js +169 -22
  88. package/dist/src/cli/config-commands.js +58 -26
  89. package/dist/src/cli/follow-view.js +75 -0
  90. package/dist/src/cli/help.js +258 -82
  91. package/dist/src/cli/log-commands.js +164 -85
  92. package/dist/src/cli/question-commands.js +211 -10
  93. package/dist/src/cli.js +173 -45
  94. package/dist/src/collections.js +56 -13
  95. package/dist/src/concurrency/check-permit.js +20 -3
  96. package/dist/src/concurrency/lock.js +17 -13
  97. package/dist/src/concurrency/provider-lease.js +44 -8
  98. package/dist/src/concurrency/semaphore.js +102 -26
  99. package/dist/src/config/config-aliases.js +0 -1
  100. package/dist/src/config/config-editable-keys.js +16 -1
  101. package/dist/src/config/config-fallbacks.js +94 -56
  102. package/dist/src/config/config-field-checks.js +40 -10
  103. package/dist/src/config/config-keys.js +30 -8
  104. package/dist/src/config/config-live.js +63 -24
  105. package/dist/src/config/config-request.js +137 -0
  106. package/dist/src/config/config-schema.js +31 -24
  107. package/dist/src/config/config-selector-checks.js +123 -0
  108. package/dist/src/config/config-validation-agents.js +80 -0
  109. package/dist/src/config/config-validation-model.js +116 -0
  110. package/dist/src/config/config-validation.js +169 -222
  111. package/dist/src/config/config-views.js +136 -54
  112. package/dist/src/config/config-write-core.js +135 -0
  113. package/dist/src/config/config-write.js +164 -265
  114. package/dist/src/config/config.js +53 -38
  115. package/dist/src/config/model-selector.js +34 -34
  116. package/dist/src/doctor/doctor-backlog-bugs.js +330 -0
  117. package/dist/src/doctor/doctor-backlog-plans.js +398 -0
  118. package/dist/src/doctor/doctor-backlog.js +249 -512
  119. package/dist/src/doctor/doctor-check-names.js +85 -0
  120. package/dist/src/doctor/doctor-checks.js +61 -593
  121. package/dist/src/doctor/doctor-claude-checks.js +142 -0
  122. package/dist/src/doctor/doctor-codex-checks.js +276 -0
  123. package/dist/src/doctor/doctor-contention-checks.js +506 -0
  124. package/dist/src/doctor/doctor-fallback-checks.js +130 -0
  125. package/dist/src/doctor/doctor-holds.js +80 -66
  126. package/dist/src/doctor/doctor-install.js +2 -1
  127. package/dist/src/doctor/doctor-model-checks.js +216 -537
  128. package/dist/src/doctor/doctor-notify.js +9 -5
  129. package/dist/src/doctor/doctor-opencode-checks.js +164 -0
  130. package/dist/src/doctor/doctor-orphans.js +182 -63
  131. package/dist/src/doctor/doctor-pi-checks.js +62 -0
  132. package/dist/src/doctor/doctor-project-checks.js +397 -0
  133. package/dist/src/doctor/doctor.js +126 -77
  134. package/dist/src/doctor/image-probe.js +21 -8
  135. package/dist/src/doctor/outcome.js +3 -2
  136. package/dist/src/doctor/probe.js +21 -12
  137. package/dist/src/events/event-format.js +185 -89
  138. package/dist/src/events/event-labels.js +11 -5
  139. package/dist/src/events/event-legacy.js +39 -15
  140. package/dist/src/events/event-read.js +52 -39
  141. package/dist/src/events/event-tally.js +16 -0
  142. package/dist/src/events/event-window.js +80 -26
  143. package/dist/src/events/events.js +30 -13
  144. package/dist/src/events/notify.js +7 -7
  145. package/dist/src/failure/failure-cluster.js +12 -12
  146. package/dist/src/failure/failure-data.js +65 -60
  147. package/dist/src/failure/failure-render.js +29 -21
  148. package/dist/src/failure/failure-state-change.js +17 -12
  149. package/dist/src/failure/time-spend.js +35 -35
  150. package/dist/src/files/file-queue.js +17 -0
  151. package/dist/src/files/files.js +111 -35
  152. package/dist/src/files/json-files.js +3 -3
  153. package/dist/src/files/json-object.js +28 -7
  154. package/dist/src/files/stat-cache.js +2 -2
  155. package/dist/src/files/tail.js +1 -1
  156. package/dist/src/gates/bootstrap-gates.js +4 -5
  157. package/dist/src/gates/budget-gates.js +88 -225
  158. package/dist/src/gates/disk-gate.js +1 -1
  159. package/dist/src/gates/gate-polls.js +101 -143
  160. package/dist/src/gates/maintenance-quota.js +37 -30
  161. package/dist/src/gates/pause-gates.js +28 -10
  162. package/dist/src/gates/readiness.js +7 -3
  163. package/dist/src/gates/role-cap-gates.js +8 -8
  164. package/dist/src/gates/startup-gate.js +94 -32
  165. package/dist/src/gates/streak-gate.js +54 -30
  166. package/dist/src/git/commit-message.js +89 -46
  167. package/dist/src/git/git-diff.js +16 -10
  168. package/dist/src/git/git-run.js +79 -38
  169. package/dist/src/git/git.js +54 -34
  170. package/dist/src/git/slot-salvage.js +253 -0
  171. package/dist/src/git/slots-state.js +6 -6
  172. package/dist/src/git/worktree-pool.js +132 -38
  173. package/dist/src/git/worktree-use.js +4 -11
  174. package/dist/src/git/worktree.js +94 -37
  175. package/dist/src/gui/gui-admin-endpoints.js +96 -0
  176. package/dist/src/gui/gui-args.js +79 -8
  177. package/dist/src/gui/gui-backlog-commands.js +43 -19
  178. package/dist/src/gui/gui-backlog-endpoints.js +156 -0
  179. package/dist/src/gui/gui-command.js +26 -13
  180. package/dist/src/gui/gui-config-commands.js +168 -80
  181. package/dist/src/gui/gui-endpoint-commands.js +5 -4
  182. package/dist/src/gui/gui-endpoint-common.js +4 -4
  183. package/dist/src/gui/gui-endpoints.js +11 -380
  184. package/dist/src/gui/gui-history-endpoints.js +301 -0
  185. package/dist/src/gui/gui-operator-commands.js +235 -13
  186. package/dist/src/gui/gui-prompt-commands.js +26 -20
  187. package/dist/src/gui/gui-report-endpoints.js +97 -0
  188. package/dist/src/gui/gui-server.js +26 -5
  189. package/dist/src/gui/gui-team-commands.js +7 -7
  190. package/dist/src/gui/http-body.js +1 -1
  191. package/dist/src/history/history-data.js +122 -234
  192. package/dist/src/history/history-queued.js +155 -0
  193. package/dist/src/history/history-render.js +42 -22
  194. package/dist/src/history/history.js +190 -49
  195. package/dist/src/inbox/inbox-attachments.js +29 -30
  196. package/dist/src/inbox/inbox-cancel.js +10 -11
  197. package/dist/src/inbox/inbox-edit.js +9 -9
  198. package/dist/src/inbox/inbox-submit.js +18 -18
  199. package/dist/src/inbox/inbox-top.js +28 -17
  200. package/dist/src/inbox/inbox.js +42 -43
  201. package/dist/src/inbox/pending-prompt.js +26 -26
  202. package/dist/src/inbox/prompt-commands.js +62 -49
  203. package/dist/src/inbox/prompt-not-before.js +2 -2
  204. package/dist/src/init/init-templates.js +1 -2
  205. package/dist/src/init/init.js +28 -23
  206. package/dist/src/merge/backlog-conflicts.js +215 -35
  207. package/dist/src/merge/merge-batch.js +14 -8
  208. package/dist/src/merge/merge-cancel.js +53 -0
  209. package/dist/src/merge/merge-check-failures.js +44 -22
  210. package/dist/src/merge/merge-core.js +58 -32
  211. package/dist/src/merge/merge-diff.js +5 -4
  212. package/dist/src/merge/merge-drain.js +45 -19
  213. package/dist/src/merge/merge-git.js +177 -38
  214. package/dist/src/merge/merge-pipeline.js +15 -3
  215. package/dist/src/merge/merge-post-review.js +214 -0
  216. package/dist/src/merge/merge-queue-render.js +48 -11
  217. package/dist/src/merge/merge-queue.js +52 -15
  218. package/dist/src/merge/merge-requeue.js +103 -0
  219. package/dist/src/merge/merge-slot.js +61 -16
  220. package/dist/src/merge/merge-stack.js +27 -11
  221. package/dist/src/merge/merge-to-main.js +77 -26
  222. package/dist/src/merge/merge-top.js +83 -0
  223. package/dist/src/merge/merge-verify.js +75 -18
  224. package/dist/src/notes-cli.js +35 -15
  225. package/dist/src/operator/operator-commands.js +192 -81
  226. package/dist/src/operator/operator-intent.js +69 -42
  227. package/dist/src/operator/operator-requests.js +35 -40
  228. package/dist/src/operator/retire.js +5 -6
  229. package/dist/src/operator/revert.js +142 -34
  230. package/dist/src/orchestrator/orchestrator-launch.js +60 -52
  231. package/dist/src/orchestrator/orchestrator-poll.js +352 -0
  232. package/dist/src/orchestrator/orchestrator-scheduling.js +71 -370
  233. package/dist/src/orchestrator/orchestrator.js +129 -369
  234. package/dist/src/orchestrator/retention.js +42 -8
  235. package/dist/src/orchestrator/scheduling-admit.js +116 -0
  236. package/dist/src/orchestrator/scheduling-scope.js +245 -0
  237. package/dist/src/orchestrator/scheduling-skip.js +195 -0
  238. package/dist/src/orchestrator/startup-migration-steps.js +8 -7
  239. package/dist/src/orchestrator/startup-migrations.js +11 -7
  240. package/dist/src/paths.js +126 -49
  241. package/dist/src/pi/command-shape.js +12 -1
  242. package/dist/src/pi/pi-args.js +6 -4
  243. package/dist/src/pi/pi-backend.js +6 -6
  244. package/dist/src/pi/pi-bin.js +54 -63
  245. package/dist/src/pi/pi-listed-models.js +53 -0
  246. package/dist/src/pi/pi-models.js +63 -30
  247. package/dist/src/pi/pi-run-result.js +1 -1
  248. package/dist/src/pi/pi-settings.js +1 -1
  249. package/dist/src/pi/pi-stream.js +6 -6
  250. package/dist/src/pi/pi-watchdogs.js +18 -19
  251. package/dist/src/pi-extension/bounded-output.js +5 -3
  252. package/dist/src/pi-extension/context-budget.js +2 -2
  253. package/dist/src/pi-extension/context-shake.js +1 -1
  254. package/dist/src/pi-extension/context-window.js +0 -1
  255. package/dist/src/pi-extension/full-output.js +3 -10
  256. package/dist/src/pi-extension/strip-thinking.js +0 -1
  257. package/dist/src/pi-extension/trim-system-prompt.js +3 -4
  258. package/dist/src/pi-extension/write-guard.js +37 -0
  259. package/dist/src/process/launch-services.js +1 -2
  260. package/dist/src/process/node-bin.js +16 -0
  261. package/dist/src/process/process-group.js +8 -18
  262. package/dist/src/process/process.js +41 -2
  263. package/dist/src/process/run-marker.js +26 -6
  264. package/dist/src/process/supervisor.js +22 -21
  265. package/dist/src/prompt/dialect.js +17 -2
  266. package/dist/src/prompt/gate-notes.js +203 -0
  267. package/dist/src/prompt/gate-prompts.js +150 -217
  268. package/dist/src/prompt/notes-backlog.js +184 -0
  269. package/dist/src/prompt/principles.js +1 -2
  270. package/dist/src/prompt/prompt-followup.js +19 -14
  271. package/dist/src/prompt/prompt.js +131 -204
  272. package/dist/src/redeploy/redeploy-policy.js +24 -19
  273. package/dist/src/redeploy/redeploy-probes.js +1 -1
  274. package/dist/src/redeploy/redeploy.js +2 -3
  275. package/dist/src/redeploy/redeployer.js +20 -20
  276. package/dist/src/redeploy/self-reload.js +2 -1
  277. package/dist/src/report/report-data.js +188 -124
  278. package/dist/src/report/report-fold.js +130 -54
  279. package/dist/src/report/report-main-red.js +55 -0
  280. package/dist/src/report/report-render.js +136 -65
  281. package/dist/src/report/report-types.js +1 -0
  282. package/dist/src/report/report.js +23 -9
  283. package/dist/src/request-timeouts.js +2 -2
  284. package/dist/src/review/diff-rules.js +53 -0
  285. package/dist/src/review/known-flakes.js +112 -1
  286. package/dist/src/review/post-merge-review-accept.js +51 -0
  287. package/dist/src/review/post-merge-review-render.js +112 -0
  288. package/dist/src/review/post-merge-review.js +239 -0
  289. package/dist/src/review/protected-edits.js +24 -10
  290. package/dist/src/review/quarantine-render.js +65 -0
  291. package/dist/src/review/review-followup.js +8 -8
  292. package/dist/src/review/review-precheck.js +66 -23
  293. package/dist/src/review/review-run.js +395 -0
  294. package/dist/src/review/review-verdict.js +2 -2
  295. package/dist/src/review/review.js +180 -401
  296. package/dist/src/review/secret-leaks.js +36 -16
  297. package/dist/src/review/suite-rerun.js +0 -1
  298. package/dist/src/roles/legacy-role-migration.js +80 -7
  299. package/dist/src/roles/role-catalog.js +127 -12
  300. package/dist/src/roles/role-guidance.js +65 -20
  301. package/dist/src/roles/role-labels.js +7 -5
  302. package/dist/src/roles/role-render.js +108 -56
  303. package/dist/src/roles/role-view.js +95 -56
  304. package/dist/src/roles/roles.js +33 -33
  305. package/dist/src/roles/runner-ids.js +97 -0
  306. package/dist/src/run/acceptance-greps.js +119 -0
  307. package/dist/src/run/ci-parity.js +121 -0
  308. package/dist/src/run/feedback-filings.js +51 -0
  309. package/dist/src/run/qa-coverage.js +125 -0
  310. package/dist/src/{tick/tick-apply.js → run/run-apply.js} +95 -94
  311. package/dist/src/run/run-detail-data.js +175 -0
  312. package/dist/src/run/run-detail-render.js +91 -0
  313. package/dist/src/run/run-detail.js +168 -0
  314. package/dist/src/{tick/tick-finalize.js → run/run-finalize.js} +73 -60
  315. package/dist/src/{tick/tick-outcome.js → run/run-outcome.js} +6 -7
  316. package/dist/src/{tick/tick-prompt.js → run/run-prompt.js} +122 -73
  317. package/dist/src/{tick/tick-resume.js → run/run-resume.js} +55 -26
  318. package/dist/src/{tick/tick-stage.js → run/run-stage.js} +170 -83
  319. package/dist/src/{tick/tick-timing.js → run/run-timing.js} +43 -41
  320. package/dist/src/{tick/tick-usage.js → run/run-usage.js} +45 -37
  321. package/dist/src/{tick/tick-verdict.js → run/run-verdict.js} +105 -57
  322. package/dist/src/run/selfcheck-digest.js +40 -0
  323. package/dist/src/run/stage-backlog-checks.js +249 -0
  324. package/dist/src/run/stage-check.js +178 -0
  325. package/dist/src/run/stage-checks-shared.js +65 -0
  326. package/dist/src/run/stage-name-checks.js +204 -0
  327. package/dist/src/run/stage-path-checks.js +231 -0
  328. package/dist/src/run/stage-prose-checks.js +261 -0
  329. package/dist/src/runner/leftover.js +216 -0
  330. package/dist/src/{loop → runner}/model-fallback.js +12 -12
  331. package/dist/src/runner/revision.js +191 -0
  332. package/dist/src/{loop/loop-fallback.js → runner/runner-fallback.js} +8 -8
  333. package/dist/src/runner/runner-outcome.js +109 -0
  334. package/dist/src/{loop/loop-pi.js → runner/runner-pi.js} +140 -131
  335. package/dist/src/{loop/loop-recovery.js → runner/runner-recovery.js} +39 -18
  336. package/dist/src/{loop/loop-tick.js → runner/runner-run.js} +236 -245
  337. package/dist/src/{loop/loop-state.js → runner/runner-state-load.js} +61 -114
  338. package/dist/src/runner/runner-state.js +103 -0
  339. package/dist/src/{loop/loop.js → runner/runner.js} +123 -120
  340. package/dist/src/scheduling/backoff.js +38 -38
  341. package/dist/src/scheduling/claim-entries.js +49 -0
  342. package/dist/src/scheduling/claims.js +35 -22
  343. package/dist/src/scheduling/once-round.js +22 -22
  344. package/dist/src/scheduling/quiet-hours.js +5 -6
  345. package/dist/src/scheduling/scheduling.js +74 -64
  346. package/dist/src/scheduling/work-merged-cache.js +2 -2
  347. package/dist/src/status/status-budget.js +55 -47
  348. package/dist/src/status/status-data.js +49 -17
  349. package/dist/src/status/status-inbox.js +2 -2
  350. package/dist/src/status/status-install.js +0 -1
  351. package/dist/src/status/status-polls.js +43 -28
  352. package/dist/src/status/status-roles.js +50 -13
  353. package/dist/src/team/error-storm.js +8 -8
  354. package/dist/src/team/failure-spread.js +2 -2
  355. package/dist/src/team/orchestrator-info.js +15 -9
  356. package/dist/src/team/reclaim.js +7 -8
  357. package/dist/src/team/team-hold.js +7 -7
  358. package/dist/src/team/team-polls.js +91 -50
  359. package/dist/src/team/team-state.js +8 -2
  360. package/dist/src/text/datetime.js +65 -18
  361. package/dist/src/text/format.js +48 -11
  362. package/dist/src/text/markdown.js +8 -5
  363. package/dist/src/text/phrases.js +60 -23
  364. package/dist/src/text/suggest.js +1 -1
  365. package/dist/src/text/text.js +78 -13
  366. package/dist/src/ui/badges.js +99 -38
  367. package/dist/src/ui/gui/gui-client-backlog.js +314 -0
  368. package/dist/src/ui/gui/gui-client-boot.js +89 -61
  369. package/dist/src/ui/gui/gui-client-composer.js +8 -9
  370. package/dist/src/ui/gui/gui-client-drawer-config.js +254 -0
  371. package/dist/src/ui/gui/gui-client-drawer.js +155 -289
  372. package/dist/src/ui/gui/gui-client-feed.js +271 -0
  373. package/dist/src/ui/gui/gui-client-history.js +437 -68
  374. package/dist/src/ui/gui/gui-client-labels.js +6 -3
  375. package/dist/src/ui/gui/gui-client-markdown.js +1 -2
  376. package/dist/src/ui/gui/gui-client-model.js +27 -36
  377. package/dist/src/ui/gui/gui-client-operator.js +60 -52
  378. package/dist/src/ui/gui/gui-client-pending.js +405 -24
  379. package/dist/src/ui/gui/gui-client-report-chart.js +492 -0
  380. package/dist/src/ui/gui/gui-client-report.js +105 -430
  381. package/dist/src/ui/gui/{gui-client-loops.js → gui-client-roles.js} +43 -44
  382. package/dist/src/ui/gui/gui-client-settings.js +163 -5
  383. package/dist/src/ui/gui/gui-client-team.js +36 -465
  384. package/dist/src/ui/gui/gui-client.js +38 -26
  385. package/dist/src/ui/gui/gui-icons.js +0 -2
  386. package/dist/src/ui/gui/gui-page.js +10 -7
  387. package/dist/src/ui/gui/gui-styles.js +38 -34
  388. package/dist/src/ui/progress-data.js +9 -11
  389. package/dist/src/ui/{tick-progress-model.js → run-progress-model.js} +33 -32
  390. package/dist/src/ui/status-model.js +133 -125
  391. package/dist/src/ui/status-payload.js +364 -49
  392. package/dist/src/ui/status-render.js +109 -82
  393. package/dist/src/ui/team-alerts.js +728 -62
  394. package/dist/src/ui/tone.js +12 -6
  395. package/dist/src/ui/transcript-run.js +58 -0
  396. package/dist/src/ui/transcript-tail.js +28 -20
  397. package/dist/src/ui/transcript.js +63 -23
  398. package/dist/src/ui/tui/tui-app.js +0 -1
  399. package/dist/src/ui/tui/tui-backlog.js +53 -14
  400. package/dist/src/ui/tui/tui-editor-answer.js +27 -31
  401. package/dist/src/ui/tui/tui-editor-attach.js +4 -12
  402. package/dist/src/ui/tui/tui-editor-backends.js +127 -0
  403. package/dist/src/ui/tui/tui-editor-brief.js +14 -36
  404. package/dist/src/ui/tui/tui-editor-budget.js +36 -39
  405. package/dist/src/ui/tui/tui-editor-check.js +29 -0
  406. package/dist/src/ui/tui/tui-editor-checks.js +28 -0
  407. package/dist/src/ui/tui/tui-editor-claude-reserve.js +24 -0
  408. package/dist/src/ui/tui/tui-editor-concurrency.js +25 -0
  409. package/dist/src/ui/tui/tui-editor-config.js +99 -0
  410. package/dist/src/ui/tui/tui-editor-entry.js +39 -64
  411. package/dist/src/ui/tui/tui-editor-fallback.js +31 -0
  412. package/dist/src/ui/tui/tui-editor-instances.js +46 -0
  413. package/dist/src/ui/tui/tui-editor-instructions.js +117 -0
  414. package/dist/src/ui/tui/tui-editor-interval.js +29 -16
  415. package/dist/src/ui/tui/tui-editor-model.js +29 -0
  416. package/dist/src/ui/tui/tui-editor-notes.js +9 -29
  417. package/dist/src/ui/tui/tui-editor-notify.js +25 -0
  418. package/dist/src/ui/tui/tui-editor-pause.js +48 -59
  419. package/dist/src/ui/tui/tui-editor-prompt.js +11 -16
  420. package/dist/src/ui/tui/tui-editor-provider-caps.js +27 -0
  421. package/dist/src/ui/tui/tui-editor-queued.js +5 -9
  422. package/dist/src/ui/tui/tui-editor-quiet.js +95 -41
  423. package/dist/src/ui/tui/tui-editor-retire.js +5 -13
  424. package/dist/src/ui/tui/tui-editor-role-enabled.js +126 -0
  425. package/dist/src/ui/tui/tui-editor-role-key.js +71 -0
  426. package/dist/src/ui/tui/tui-editor-role-model.js +28 -0
  427. package/dist/src/ui/tui/tui-editor-search.js +45 -33
  428. package/dist/src/ui/tui/tui-editor-session.js +101 -34
  429. package/dist/src/ui/tui/tui-editor-slots.js +24 -0
  430. package/dist/src/ui/tui/tui-editors.js +44 -2
  431. package/dist/src/ui/tui/tui-frame.js +68 -11
  432. package/dist/src/ui/tui/tui-history.js +8 -9
  433. package/dist/src/ui/tui/tui-input.js +13 -11
  434. package/dist/src/ui/tui/tui-keymap.js +0 -1
  435. package/dist/src/ui/tui/tui-keys.js +33 -9
  436. package/dist/src/ui/tui/tui-pane-activity.js +35 -19
  437. package/dist/src/ui/tui/tui-pane-backlog.js +198 -126
  438. package/dist/src/ui/tui/tui-pane-brief.js +0 -1
  439. package/dist/src/ui/tui/tui-pane-ctx.js +2 -2
  440. package/dist/src/ui/tui/tui-pane-health.js +168 -8
  441. package/dist/src/ui/tui/tui-pane-history.js +446 -82
  442. package/dist/src/ui/tui/tui-pane-pending.js +539 -15
  443. package/dist/src/ui/tui/tui-pane-queued.js +78 -57
  444. package/dist/src/ui/tui/tui-pane-report.js +54 -16
  445. package/dist/src/ui/tui/tui-pane-session.js +34 -6
  446. package/dist/src/ui/tui/tui-pane-transcript.js +111 -37
  447. package/dist/src/ui/tui/tui-pane.js +91 -28
  448. package/dist/src/ui/tui/tui-panes.js +153 -34
  449. package/dist/src/ui/tui/tui.js +18 -15
  450. package/dist/src/verdict/fix-claim.js +129 -33
  451. package/dist/src/verdict/no-change.js +4 -4
  452. package/dist/src/verdict/refusal.js +40 -16
  453. package/dist/src/verdict/reply-contract.js +77 -31
  454. package/dist/src/version.js +0 -1
  455. package/package.json +2 -1
  456. package/dist/src/loop/leftover.js +0 -130
  457. package/dist/src/loop/revision.js +0 -70
  458. package/dist/src/roles/loop-ids.js +0 -77
  459. package/dist/src/tick/qa-coverage.js +0 -108
  460. package/dist/src/tick/stage-check.js +0 -419
  461. package/dist/src/tick/telemetry-digest.js +0 -25
  462. package/dist/src/tick/tick-detail-data.js +0 -108
  463. package/dist/src/tick/tick-detail.js +0 -163
  464. package/dist/src/ui/gui/gui-client-sound.js +0 -53
@@ -1,32 +1,33 @@
1
- // vocabulary: plain
2
1
  /** Collection half of the usage report: read the harness's event log (through the shared
3
2
  * event-folding layer in src/report/report-fold.ts) and the backlog history over a window of
4
3
  * 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
4
+ * run/token/commit/cost counts plus features-done and bugs-fixed tallies from PLANS.md and
6
5
  * BUGS.md. The Markdown rendering of this data lives in src/report/report-render.ts, a pure
7
6
  * function of it; the split mirrors the failure report's (src/failure/failure-data.ts /
8
7
  * src/failure/failure-render.ts) and keeps "what happened" (window math, aggregation, bounds)
9
8
  * apart from "how it prints" (bars, cell wording), which change for different reasons — and
10
9
  * keeps core data collection out of the presentation layer, so a core consumer (as
11
10
  * /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). */
14
- import { assertReportSinceMs, eventWindowCovers, readWindowEvents } from "../events/event-window.js";
15
- import { DONE_DATE_RE, FIXED_DATE_RE, completionDate, finishedFiles, fixedBugEntries, sectionCompletionDates, sectionFilingDates, } from "../backlog/backlog.js";
11
+ * collectors; the report shapes they produce live in src/report/report-types.ts, and the
12
+ * event-log folding and its memoized per-window cache in src/report/report-fold.ts (its own
13
+ * test, report-fold-cache). */
14
+ import { assertReportSinceMs, eventWindowCovers, readWindowEvents, sinceWindowTiming } from "../events/event-window.js";
15
+ import { DONE_DATE_RE, FIXED_DATE_RE, completionDate, finishedFiles, fixedBugEntries, sectionCompletionDates, sectionCompletionLags, sectionFilingDates, } from "../backlog/backlog.js";
16
16
  import { validationGapTag } from "../backlog/backlog-md.js";
17
17
  import { dayAt, dayKey, dayWindow, formatDate } from "../text/datetime.js";
18
18
  import { addTo, lowerMedian } from "../collections.js";
19
- import { baseRoleOf } from "../roles/loop-ids.js";
20
19
  import { commitTier } from "../roles/roles.js";
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
20
+ import { foldUsageEvent, foldWindowEvents, narrowToRole, roleSum, scopeBases, scopedByRole, zeroCommitCounts, zeroMergeCounts, } from "./report-fold.js";
21
+ import { mainRedMinutesByDay } from "./report-main-red.js";
22
+ /** Aggregate team usage over the trailing `sinceMs` window ending now (or `untilMs` ago, which
23
+ * drops events at or after that end; `coversFullWindow` still judges the far edge
24
+ * only), from the event log only (run_end/merged — see SinceReport for why the backlog files
25
+ * are not read). Kept a
24
26
  * separate function rather than a `collectReport` variant flag: the day collector's
25
27
  * zero-filled series and backlog-file reads have no place in a window totals view. */
26
- export function collectReportSince(root, sinceMs, role) {
28
+ export function collectReportSince(root, sinceMs, role, untilMs) {
27
29
  assertReportSinceMs(sinceMs);
28
- const now = Date.now();
29
- const cutoff = now - sinceMs;
30
+ const { now, cutoff, end, slice } = sinceWindowTiming(sinceMs, untilMs);
30
31
  // The window key is the cutoff's local calendar day, from the shared dayKey helper
31
32
  // eventDayKey buckets events with, so the read's day keys cannot disagree with the ts
32
33
  // filter below; the day-keyed read may include earlier hours of that day, which the ts
@@ -38,13 +39,18 @@ export function collectReportSince(root, sinceMs, role) {
38
39
  runs: 0,
39
40
  runsByRole: {},
40
41
  rejectionsByRole: {},
42
+ postMergeRejectionsByRole: {},
41
43
  revisionsByRole: {},
42
44
  refusalsByRole: {},
43
45
  checkDurationsMs: [],
44
46
  checkFailures: 0,
45
47
  mergeWaitsMs: [],
48
+ mergeDurationsMs: [],
49
+ runDurationsMs: [],
46
50
  providerWaitsMs: [],
47
51
  slotWaitsMs: [],
52
+ checkWaitsMs: [],
53
+ reviewDurationsMs: [],
48
54
  failedRunsByRole: {},
49
55
  fallbacksByRole: {},
50
56
  imageReroutesByRole: {},
@@ -61,11 +67,12 @@ export function collectReportSince(root, sinceMs, role) {
61
67
  mergeFailedByReason: {},
62
68
  costByProvider: {},
63
69
  notionalByProvider: {},
70
+ notionalByBackend: {},
64
71
  runsByModel: {},
65
72
  deferredByReason: {},
66
73
  };
67
74
  for (const ev of raw.events) {
68
- if (typeof ev.ts !== "number" || ev.ts < cutoff || ev.ts > now)
75
+ if (typeof ev.ts !== "number" || ev.ts < cutoff || ev.ts > now || (untilMs !== undefined && ev.ts >= end))
69
76
  continue;
70
77
  foldUsageEvent(acc, ev);
71
78
  }
@@ -74,18 +81,19 @@ export function collectReportSince(root, sinceMs, role) {
74
81
  // history that never existed. When neither proof holds, the oldest retained event lies inside
75
82
  // the window and the render notes the possible truncation.
76
83
  const coversFullWindow = eventWindowCovers(raw, cutoff);
77
- // Role scope: every total derives from the fold's per-role maps for the named base role alone
84
+ // Role scope: every total derives from the fold's per-role maps for the named base roles alone
78
85
  // — merge usage (the merge slot's, not the role's) and the team-only fields stay out, so
79
86
  // a scoped --since report sums exactly the same events the scoped day report would. The scope
80
- // is the base role, so instances (`feature-2`) aggregate into their group as the cap does.
81
- const base = role === undefined ? undefined : baseRoleOf(role);
82
- const scopedTotals = base === undefined
87
+ // is the base roles, so instances (`feature-2`) aggregate into their group as the cap does.
88
+ const bases = scopeBases(role);
89
+ const scopedTotals = bases === undefined
83
90
  ? {
84
91
  tokensOut: acc.tokensOut + (acc.mergeTokens ?? 0),
85
92
  runs: acc.runs ?? 0,
86
93
  commits: acc.commits,
87
94
  costUsd: acc.costUsd + (acc.mergeCostUsd ?? 0),
88
95
  ...(acc.notionalCostUsd > 0 ? { notionalCostUsd: acc.notionalCostUsd } : {}),
96
+ ...nonemptyObj("notionalByBackend", acc.notionalByBackend),
89
97
  mergeRuns: acc.mergeRuns ?? 0,
90
98
  mergeTokens: acc.mergeTokens ?? 0,
91
99
  mergeCostUsd: acc.mergeCostUsd ?? 0,
@@ -94,12 +102,12 @@ export function collectReportSince(root, sinceMs, role) {
94
102
  : (() => {
95
103
  // The role's own notional spend, from its run_end events; the merge slot's
96
104
  // notional is team-wide and stays out like its cost.
97
- const notional = roleSum(acc.notionalByRole, base);
105
+ const notional = roleSum(acc.notionalByRole, bases);
98
106
  return {
99
- tokensOut: roleSum(acc.tokensByRole, base),
100
- runs: roleSum(acc.runsByRole, base),
101
- commits: roleSum(acc.commitsByRole, base),
102
- costUsd: roleSum(acc.costByRole, base),
107
+ tokensOut: roleSum(acc.tokensByRole, bases),
108
+ runs: roleSum(acc.runsByRole, bases),
109
+ commits: roleSum(acc.commitsByRole, bases),
110
+ costUsd: roleSum(acc.costByRole, bases),
103
111
  ...(notional > 0 ? { notionalCostUsd: notional } : {}),
104
112
  ...zeroMergeCounts(),
105
113
  };
@@ -108,49 +116,81 @@ export function collectReportSince(root, sinceMs, role) {
108
116
  // notional spend, scoped to base when a role is given — the same conditional the day
109
117
  // collector applies to ReportDay.notionalByRole (line ~573), so a role-scoped window
110
118
  // whose role spent no notional carries no notionalByRole instead of an empty map.
111
- const notionalByRole = scopedByRole(acc.notionalByRole, base);
119
+ const notionalByRole = scopedByRole(acc.notionalByRole, bases);
112
120
  // Deferred runs are bucketed by a reason code (not a role), so a role-scoped window
113
121
  // carries no entries — the same team-only rule runsByModel follows. Only set when the
114
122
  // window actually deferred (zero means absent), so a deferral-free --since JSON omits it.
115
- const deferred = scopedByRole(acc.deferredByReason, base);
123
+ const deferred = scopedByRole(acc.deferredByReason, bases);
116
124
  return {
117
- sinceMs,
118
- fromIso: new Date(cutoff).toISOString(),
119
- ...(base === undefined ? {} : { role: base }),
125
+ ...slice,
126
+ ...(bases === undefined ? {} : { role: bases.join(",") }),
120
127
  totals: scopedTotals,
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),
128
+ runsByRole: scopedByRole(acc.runsByRole, bases),
129
+ costByRole: scopedByRole(acc.costByRole, bases),
130
+ rejectionsByRole: scopedByRole(acc.rejectionsByRole, bases),
131
+ postMergeRejectionsByRole: scopedByRole(acc.postMergeRejectionsByRole, bases),
132
+ revisionsByRole: scopedByRole(acc.revisionsByRole, bases),
133
+ refusalsByRole: scopedByRole(acc.refusalsByRole, bases),
134
+ failedRunsByRole: scopedByRole(acc.failedRunsByRole, bases),
135
+ fallbacksByRole: scopedByRole(acc.fallbacksByRole, bases),
136
+ imageReroutesByRole: scopedByRole(acc.imageReroutesByRole, bases),
137
+ tokensByRole: scopedByRole(acc.tokensByRole, bases),
138
+ commitsByRole: scopedByRole(acc.commitsByRole, bases),
139
+ mergedByRole: scopedByRole(acc.mergedByRole, bases),
140
+ mergeFailedByRole: scopedByRole(acc.mergeFailedByRole, bases),
133
141
  // A result code is not a role, so a role-scoped window carries an empty map (a code
134
142
  // 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 } : {}),
143
+ mergeFailedByReason: scopedByRole(acc.mergeFailedByReason, bases),
144
+ ...nonemptyObj("notionalByRole", notionalByRole),
137
145
  // Provider spend is a team attribution (a provider is not a role), so a role-scoped
138
146
  // window carries empty maps — providers never baseRoleOf to the role — and the render
139
147
  // 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),
148
+ costByProvider: scopedByRole(acc.costByProvider, bases),
149
+ notionalByProvider: scopedByRole(acc.notionalByProvider, bases),
150
+ // Backend spend is a team attribution (a backend is not a role), so a role-scoped window
151
+ // omits it — the same absent-when-empty rule the day collector applies.
152
+ ...nonemptyObj("notionalByBackend", scopedByRole(acc.notionalByBackend, bases)),
142
153
  // Model attribution is team-only (a model is not a role, so no model folds to the
143
154
  // scoped role), the same full-map-for-team / empty-for-scoped rule costByProvider
144
155
  // 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 } : {}),
156
+ runsByModel: scopedByRole(acc.runsByModel, bases),
157
+ ...nonemptyObj("deferredByReason", deferred),
158
+ // Main's red time is main's own (G1's rule), not any role's, so a role-scoped window
159
+ // carries 0 — the type's always-present default for the team-only field.
160
+ mainRedMs: bases === undefined
161
+ ? [...mainRedMinutesByDay(raw.events, cutoff, end).values()].reduce((a, b) => a + b, 0)
162
+ : 0,
147
163
  coversFullWindow,
148
164
  };
149
165
  }
166
+ /** Set one per-day median cell only when the day actually has samples (`ms` non-empty), so a
167
+ * medians-free day keeps its exact pre-feature shape (the field stays absent). The single home
168
+ * of that guard — the day collector's eight `…MedianMs` cells all followed it inline before. */
169
+ function setLowerMedian(day, key, ms) {
170
+ if (ms.length > 0)
171
+ day[key] = lowerMedian(ms);
172
+ }
173
+ /** Set one per-day role/product map cell only when the day actually has entries (`map`
174
+ * non-empty), so a map-free day keeps its exact pre-feature shape (the field stays absent).
175
+ * The single home of that guard — the day collector's per-role-and-product map cells all
176
+ * followed it inline before. */
177
+ function setMapIfNonempty(day, key, map) {
178
+ if (map !== undefined && Object.keys(map).length > 0) {
179
+ day[key] = map;
180
+ }
181
+ }
182
+ /** Build one object-literal spread cell for a per-slice role/product map only when the map has
183
+ * entries (`map` non-empty), so an entry-free window omits the field instead of carrying an
184
+ * empty map. The single home of that guard — the window and totals object-literal spreads all
185
+ * followed it inline before. */
186
+ function nonemptyObj(key, map) {
187
+ return Object.keys(map).length > 0 ? { [key]: map } : {};
188
+ }
150
189
  /** Aggregate team usage over exactly `days` local calendar days ending today, from the event
151
190
  * log (run_end/merged) and the backlog history (`## Done` in PLANS.md and PLANS-DONE.md,
152
191
  * `## Fixed` in BUGS.md and BUGS-FIXED.md). */
153
192
  export function collectReport(root, days, role) {
193
+ const bases = scopeBases(role);
154
194
  const now = new Date();
155
195
  // One captured instant for the whole window, so every day key derives from the same day.
156
196
  const { from, to } = dayWindow(days, now);
@@ -175,38 +215,35 @@ export function collectReport(root, days, role) {
175
215
  const day = byDate.get(dayKey);
176
216
  if (!day)
177
217
  continue; // Out-of-window day key (e.g. future-dated) — no series slot to fill.
178
- if (role === undefined) {
218
+ if (bases === undefined) {
179
219
  day.tokensOut = fold.tokensOut;
180
220
  day.runsByRole = fold.runsByRole;
181
221
  day.tokensByRole = fold.tokensByRole;
182
222
  day.costByRole = fold.costByRole;
183
223
  // 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;
224
+ setMapIfNonempty(day, "rejectionsByRole", fold.rejectionsByRole);
225
+ setMapIfNonempty(day, "postMergeRejectionsByRole", fold.postMergeRejectionsByRole);
226
+ setMapIfNonempty(day, "revisionsByRole", fold.revisionsByRole);
227
+ setMapIfNonempty(day, "refusalsByRole", fold.refusalsByRole);
190
228
  // 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
- }
229
+ setLowerMedian(day, "checkMedianMs", fold.checkDurationsMs);
194
230
  if (fold.checkFailures > 0)
195
231
  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);
232
+ setLowerMedian(day, "mergeWaitMedianMs", fold.mergeWaitsMs);
233
+ setLowerMedian(day, "mergeSlotMedianMs", fold.mergeDurationsMs);
234
+ setLowerMedian(day, "runMedianMs", fold.runDurationsMs);
235
+ setLowerMedian(day, "providerWaitMedianMs", fold.providerWaitsMs);
200
236
  // 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);
237
+ setLowerMedian(day, "slotWaitMedianMs", fold.slotWaitsMs);
238
+ // A check permit is the team's, not a role's, so only the team series carries it.
239
+ setLowerMedian(day, "checkWaitMedianMs", fold.checkWaitsMs);
240
+ // A review belongs to the merge slot/reviewer, not the author role, so only the team
241
+ // series carries the median, mirroring checkMedianMs.
242
+ setLowerMedian(day, "reviewMedianMs", fold.reviewDurationsMs);
203
243
  // 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;
244
+ setMapIfNonempty(day, "failedRunsByRole", fold.failedRunsByRole);
245
+ setMapIfNonempty(day, "fallbacksByRole", fold.fallbacksByRole);
246
+ setMapIfNonempty(day, "imageReroutesByRole", fold.imageReroutesByRole);
210
247
  day.commits = fold.commits;
211
248
  day.commitsByRole = fold.commitsByRole;
212
249
  day.workCommits = fold.workCommits;
@@ -215,24 +252,23 @@ export function collectReport(root, days, role) {
215
252
  // Only set when nonzero, so zero-notional days keep their exact pre-feature shape.
216
253
  if (fold.notionalCostUsd > 0)
217
254
  day.notionalCostUsd = fold.notionalCostUsd;
218
- if (Object.keys(fold.notionalByRole).length > 0)
219
- day.notionalByRole = fold.notionalByRole;
255
+ setMapIfNonempty(day, "notionalByRole", fold.notionalByRole);
220
256
  // Only set when a provider actually spent (zero means absent), so a provider-free day
221
257
  // 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;
258
+ setMapIfNonempty(day, "costByProvider", fold.costByProvider);
259
+ setMapIfNonempty(day, "notionalByProvider", fold.notionalByProvider);
260
+ // Backends are a team attribution (a backend is not a role), so only the team series
261
+ // carries the map; only set when a backend actually spent (zero means absent), so a
262
+ // backend-free day keeps its exact pre-feature shape.
263
+ setMapIfNonempty(day, "notionalByBackend", fold.notionalByBackend);
226
264
  // Models are a team attribution (a model is not a role), so only the team series
227
265
  // carries the map; only set when a modelled start happened (zero means absent), so a
228
266
  // model-less day keeps its exact pre-feature shape.
229
- if (Object.keys(fold.runsByModel).length > 0)
230
- day.runsByModel = fold.runsByModel;
267
+ setMapIfNonempty(day, "runsByModel", fold.runsByModel);
231
268
  // Deferred runs are a team attribution (a reason is not a role), so only the team
232
269
  // series carries the map; only set when a day actually deferred (zero means absent),
233
270
  // so a deferral-free day keeps its exact pre-feature shape.
234
- if (Object.keys(fold.deferredByReason).length > 0)
235
- day.deferredByReason = fold.deferredByReason;
271
+ setMapIfNonempty(day, "deferredByReason", fold.deferredByReason);
236
272
  day.mergeRuns = fold.mergeRuns;
237
273
  day.mergeTokens = fold.mergeTokens;
238
274
  day.mergeCostUsd = fold.mergeCostUsd;
@@ -241,68 +277,49 @@ export function collectReport(root, days, role) {
241
277
  day.mergeFailed = fold.mergeFailed;
242
278
  // Only set when a role actually merged/failed that day (zero means absent), so a
243
279
  // 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;
280
+ setMapIfNonempty(day, "mergedByRole", fold.mergedByRole);
281
+ setMapIfNonempty(day, "mergeFailedByRole", fold.mergeFailedByRole);
248
282
  // Reason codes are a team attribution (a result is not a role), so only the team
249
283
  // series carries the map; only set when nonzero, so a failure-free day keeps its
250
284
  // exact pre-feature shape. The role branch below leaves it unset — merge runs are
251
285
  // the merge slot's.
252
- if (Object.keys(fold.mergeFailedByReason).length > 0)
253
- day.mergeFailedByReason = fold.mergeFailedByReason;
286
+ setMapIfNonempty(day, "mergeFailedByReason", fold.mergeFailedByReason);
254
287
  }
255
288
  else {
256
289
  // Role scope: derive this day from the fold's per-role maps, so the series carries only
257
- // the named role's ticks/tokens/cost and (base-role-keyed) commits. The scope is the
290
+ // the named role's runs/tokens/cost and (base-role-keyed) commits. The scope is the
258
291
  // base role, so an instance id's usage and its siblings' commits fold into one group and
259
292
  // the numbers match the per-role cap, which is base-role keyed too. The merge fields
260
293
  // stay unset — merge runs are the merge slot's, not this role's — and the work /
261
294
  // maintenance split follows the base role's own tier rather than the day's whole split.
262
- const base = baseRoleOf(role);
263
- const commits = roleSum(fold.commitsByRole, base);
264
- const tier = commitTier(base);
265
- day.tokensOut = roleSum(fold.tokensByRole, base);
266
- day.runsByRole = narrowToRole(fold.runsByRole, base);
267
- day.tokensByRole = narrowToRole(fold.tokensByRole, base);
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;
295
+ const commits = roleSum(fold.commitsByRole, bases);
296
+ day.tokensOut = roleSum(fold.tokensByRole, bases);
297
+ day.runsByRole = narrowToRole(fold.runsByRole, bases);
298
+ day.tokensByRole = narrowToRole(fold.tokensByRole, bases);
299
+ day.costByRole = narrowToRole(fold.costByRole, bases);
300
+ setMapIfNonempty(day, "rejectionsByRole", narrowToRole(fold.rejectionsByRole, bases));
301
+ setMapIfNonempty(day, "postMergeRejectionsByRole", narrowToRole(fold.postMergeRejectionsByRole, bases));
302
+ setMapIfNonempty(day, "revisionsByRole", narrowToRole(fold.revisionsByRole, bases));
303
+ setMapIfNonempty(day, "refusalsByRole", narrowToRole(fold.refusalsByRole, bases));
304
+ setMapIfNonempty(day, "failedRunsByRole", narrowToRole(fold.failedRunsByRole, bases));
305
+ setMapIfNonempty(day, "fallbacksByRole", narrowToRole(fold.fallbacksByRole, bases));
306
+ setMapIfNonempty(day, "imageReroutesByRole", narrowToRole(fold.imageReroutesByRole, bases));
287
307
  day.commits = commits;
288
- day.commitsByRole = narrowToRole(fold.commitsByRole, base);
289
- day.workCommits = tier === "work" ? commits : 0;
290
- day.maintenanceCommits = tier === "maintenance" ? commits : 0;
291
- day.costUsd = roleSum(fold.costByRole, base);
292
- const notional = roleSum(fold.notionalByRole, base);
308
+ day.commitsByRole = narrowToRole(fold.commitsByRole, bases);
309
+ const tierCommits = (tier) => bases.filter((b) => commitTier(b) === tier).reduce((n, b) => n + roleSum(fold.commitsByRole, b), 0);
310
+ day.workCommits = tierCommits("work");
311
+ day.maintenanceCommits = tierCommits("maintenance");
312
+ day.costUsd = roleSum(fold.costByRole, bases);
313
+ const notional = roleSum(fold.notionalByRole, bases);
293
314
  if (notional > 0) {
294
315
  day.notionalCostUsd = notional;
295
- day.notionalByRole = narrowToRole(fold.notionalByRole, base);
316
+ day.notionalByRole = narrowToRole(fold.notionalByRole, bases);
296
317
  }
297
318
  // Merge runs belong to the merge slot, not this role, so the role-scoped series
298
319
  // keeps them unset (matching the merge-share fields) — the render omits the merge
299
320
  // 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;
321
+ setMapIfNonempty(day, "mergedByRole", narrowToRole(fold.mergedByRole, bases));
322
+ setMapIfNonempty(day, "mergeFailedByRole", narrowToRole(fold.mergeFailedByRole, bases));
306
323
  }
307
324
  }
308
325
  // The merge share joins its day's tokensOut/costUsd (the budget charges these same
@@ -312,6 +329,19 @@ export function collectReport(root, days, role) {
312
329
  d.tokensOut += d.mergeTokens ?? 0;
313
330
  d.costUsd += d.mergeCostUsd ?? 0;
314
331
  }
332
+ // Main's red wall-clock per day, from G1's rule (main_red_hold / failed ran main_check open a
333
+ // spell, a passing ran main_check closes it, an open spell runs to now). Team-only: main's
334
+ // red time is main's own, not any role's, so a role-scoped series leaves every day unset —
335
+ // exactly as checkFailures is team-only. Only set when a day actually was red (zero-absent,
336
+ // like checkMedianMs).
337
+ if (bases === undefined) {
338
+ const redByDay = mainRedMinutesByDay(foldEntry.redEvents, dayAt(days - 1, now).getTime(), now.getTime());
339
+ for (const d of series) {
340
+ const ms = redByDay.get(d.date);
341
+ if (ms !== undefined)
342
+ d.mainRedMs = ms;
343
+ }
344
+ }
315
345
  const countOn = (date, field) => {
316
346
  const day = byDate.get(date);
317
347
  if (day)
@@ -366,6 +396,36 @@ export function collectReport(root, days, role) {
366
396
  }
367
397
  }
368
398
  }
399
+ // The per-day backlog lag median: how long each finished plan or bug waited from its filing
400
+ // stamp to its done/fixed stamp, folded per completion day — the median backlog age at
401
+ // which the day's items closed. Team-only (a lag has no role, like the validation tally
402
+ // and the filing stamps above) and zero-means-absent: a day with no lag-carrying completion
403
+ // keeps its exact pre-feature shape, and an out-of-window completion date drops out (it
404
+ // must be a byDate key, the same drop countOn applies). The same entry detection and date
405
+ // rules (sectionCompletionLags / entryLags) the completion and filing reads use, so the
406
+ // spans pair the days `featuresDone`/`bugsFixed` count.
407
+ if (role === undefined) {
408
+ const lagsByDay = new Map();
409
+ const foldLag = (date, lagDays) => {
410
+ const list = lagsByDay.get(date);
411
+ if (list)
412
+ list.push(lagDays);
413
+ else
414
+ lagsByDay.set(date, [lagDays]);
415
+ };
416
+ for (const file of finishedFiles("PLANS.md")) {
417
+ for (const { date, lagDays } of sectionCompletionLags(root, file, "Done", DONE_DATE_RE))
418
+ if (byDate.has(date))
419
+ foldLag(date, lagDays);
420
+ }
421
+ for (const file of finishedFiles("BUGS.md")) {
422
+ for (const { date, lagDays } of sectionCompletionLags(root, file, "Fixed", FIXED_DATE_RE))
423
+ if (byDate.has(date))
424
+ foldLag(date, lagDays);
425
+ }
426
+ for (const [date, lags] of lagsByDay)
427
+ byDate.get(date).backlogLagMedianDays = lowerMedian(lags);
428
+ }
369
429
  const totals = {
370
430
  tokensOut: 0,
371
431
  runs: 0,
@@ -383,6 +443,7 @@ export function collectReport(root, days, role) {
383
443
  let mergeFailed = 0;
384
444
  let plansAdded = 0;
385
445
  let bugsFiled = 0;
446
+ const notionalByBackend = {};
386
447
  for (const d of series) {
387
448
  totals.tokensOut += d.tokensOut;
388
449
  for (const n of Object.values(d.runsByRole))
@@ -394,6 +455,8 @@ export function collectReport(root, days, role) {
394
455
  totals.maintenanceCommits += d.maintenanceCommits ?? 0;
395
456
  totals.costUsd += d.costUsd;
396
457
  notionalCostUsd += d.notionalCostUsd ?? 0;
458
+ for (const [backend, c] of Object.entries(d.notionalByBackend ?? {}))
459
+ addTo(notionalByBackend, backend, c);
397
460
  totals.featuresDone += d.featuresDone;
398
461
  totals.bugsFixed += d.bugsFixed;
399
462
  plansAdded += d.plansAdded ?? 0;
@@ -419,15 +482,16 @@ export function collectReport(root, days, role) {
419
482
  days,
420
483
  from,
421
484
  to,
422
- ...(role === undefined ? {} : { role: baseRoleOf(role) }),
485
+ ...(bases === undefined ? {} : { role: bases.join(",") }),
423
486
  series,
424
487
  totals: {
425
488
  ...totals,
426
489
  ...(notionalCostUsd > 0 ? { notionalCostUsd } : {}),
490
+ ...nonemptyObj("notionalByBackend", notionalByBackend),
427
491
  ...(mergeFailed > 0 ? { mergeFailed } : {}),
428
492
  ...(plansAdded > 0 ? { plansAdded } : {}),
429
493
  ...(bugsFiled > 0 ? { bugsFiled } : {}),
430
- ...(Object.keys(validationGaps).length > 0 ? { validationGapsByTag: validationGaps } : {}),
494
+ ...nonemptyObj("validationGapsByTag", validationGaps),
431
495
  },
432
496
  coversFullWindow,
433
497
  };