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,7 +1,7 @@
1
1
  /** The cluster-key rules shared by everything that groups failure messages into causes: the
2
2
  * failure digest's collection (failure-data.ts) and the error-storm reducer
3
3
  * (src/team/error-storm.ts) both count by these keys, so the normalization, the two
4
- * tick-timeout shapes and their pooling, and the grouping engine live here rather than
4
+ * run-timeout shapes and their pooling, and the grouping engine live here rather than
5
5
  * inside either consumer. Pure string
6
6
  * and grouping logic — no event reads, no clock. */
7
7
  import { rankByCount } from "../collections.js";
@@ -76,34 +76,34 @@ export function normalizeClusterKey(message) {
76
76
  .replace(/(?<!exited\s)\b\d+\b/g, "<n>");
77
77
  return normalized.trim().slice(0, EXAMPLE_MAX);
78
78
  }
79
- /** normalizeClusterKey's rendering of the two tick-timeout errors src/agent/run-agent.ts emits —
79
+ /** normalizeClusterKey's rendering of the two run-timeout errors src/agent/run-agent.ts emits —
80
80
  * the plain kill and the still-making-progress variant (session and worktree edits preserved
81
81
  * for resume, BUGS.md 2026-09-29). Both are one cause pointing at one knob (runTimeoutSeconds),
82
82
  * so every consumer that counts by cluster key pools them under the plain one. */
83
- export const TICK_TIMEOUT_KEY = "timed out after <dur>";
84
- const TICK_TIMEOUT_PROGRESSING_KEY = "timed out after <dur> while still making progress — session and worktree edits preserved for resume";
83
+ export const RUN_TIMEOUT_KEY = "timed out after <dur>";
84
+ const RUN_TIMEOUT_PROGRESSING_KEY = "timed out after <dur> while still making progress — session and worktree edits preserved for resume";
85
85
  // The review run's rewording of the same cause (BUGS.md 2026-09-30): the reviewer never
86
86
  // resumes, so its timeout names what actually happens — the commit is kept, the next
87
87
  // attempt re-reviews from scratch. Same cause, same knob (review.timeoutSeconds), one row.
88
- const TICK_TIMEOUT_PROGRESSING_REVIEW_KEY = "timed out after <dur> while still making progress — the commit is kept; the next attempt reviews it from scratch";
88
+ const RUN_TIMEOUT_PROGRESSING_REVIEW_KEY = "timed out after <dur> while still making progress — the commit is kept; the next attempt reviews it from scratch";
89
89
  // The review run's wording when a resumable session remains:
90
90
  // the next attempt continues this review instead of re-reviewing. Continuing the timed-out
91
91
  // session is a different cause from a from-scratch re-review, so this wording is its own
92
- // cluster rather than joining the pooled plain tick-timeout row. Exported for its unit tests
93
- // (like TICK_TIMEOUT_KEY), so a display or digest test can pin the mapping by name.
94
- export const TICK_TIMEOUT_PROGRESSING_REVIEW_RESUME_KEY = "timed out after <dur> while still making progress — the commit is kept; the next attempt continues this review";
92
+ // cluster rather than joining the pooled plain run-timeout row. Exported for its unit tests
93
+ // (like RUN_TIMEOUT_KEY), so a display or digest test can pin the mapping by name.
94
+ export const RUN_TIMEOUT_PROGRESSING_REVIEW_RESUME_KEY = "timed out after <dur> while still making progress — the commit is kept; the next attempt continues this review";
95
95
  /** The known progressing-timeout wordings and the key each pools under. The two review
96
96
  * wordings name one cause on one knob (review.timeoutSeconds): a still-making-progress kill
97
- * pools into the plain tick-timeout row (a mixed team of plain and progressing kills is one
97
+ * pools into the plain run-timeout row (a mixed team of plain and progressing kills is one
98
98
  * cause's agent-hours, not two half-size rows the top-N cut can drop — the same pooling the
99
99
  * error-storm reducer applies, src/team/error-storm.ts), and a resumed review's wording maps
100
100
  * to itself, because continuing the timed-out session is a different cause from a from-scratch
101
101
  * re-review. Enumerated so a future wording edit lands in
102
102
  * this table rather than silently changing which row its timeouts count under. */
103
103
  const TIMEOUT_POOL = new Map([
104
- [TICK_TIMEOUT_PROGRESSING_KEY, TICK_TIMEOUT_KEY],
105
- [TICK_TIMEOUT_PROGRESSING_REVIEW_KEY, TICK_TIMEOUT_KEY],
106
- [TICK_TIMEOUT_PROGRESSING_REVIEW_RESUME_KEY, TICK_TIMEOUT_PROGRESSING_REVIEW_RESUME_KEY],
104
+ [RUN_TIMEOUT_PROGRESSING_KEY, RUN_TIMEOUT_KEY],
105
+ [RUN_TIMEOUT_PROGRESSING_REVIEW_KEY, RUN_TIMEOUT_KEY],
106
+ [RUN_TIMEOUT_PROGRESSING_REVIEW_RESUME_KEY, RUN_TIMEOUT_PROGRESSING_REVIEW_RESUME_KEY],
107
107
  ]);
108
108
  /** The cluster key a normalized error clusters under: the pooled wordings above map to their
109
109
  * row, and every other cause stands as normalizeClusterKey rendered it. */
@@ -1,13 +1,14 @@
1
- import { assertReportSinceMs, eventWindowCovers, readWindowEvents } from "../events/event-window.js";
1
+ import { assertReportSinceMs, eventWindowCovers, readWindowEvents, sinceWindowTiming } from "../events/event-window.js";
2
2
  import { eventDayKey, eventRole } from "../events/event-read.js";
3
- import { baseRoleOf } from "../roles/loop-ids.js";
3
+ import { baseRoleOf } from "../roles/runner-ids.js";
4
+ import { scopeBases } from "../report/report-fold.js";
4
5
  import { timeAndSpend } from "./time-spend.js";
5
6
  import { dayAt, dayKey, dayWindow, formatDate } from "../text/datetime.js";
6
7
  import { describeStateChange, STATE_CHANGE_TOP, STATE_CHANGE_TYPES } from "./failure-state-change.js";
7
8
  import { clusterMessages, errorTextOrPlaceholder, truncateExample } from "./failure-cluster.js";
8
9
  import { addTo, getOrCreate, memoSetLru, rankByCount } from "../collections.js";
9
10
  import { firstReason } from "../text/phrases.js";
10
- import { sameStatIdentity, statOrNull } from "../files/files.js";
11
+ import { sameStatIdentity, statIdentityOrZero, statOrNull } from "../files/files.js";
11
12
  import { nonNegativeNumber } from "../files/json-object.js";
12
13
  import { eventsLogPath } from "../paths.js";
13
14
  /** Caps that keep the digest bounded regardless of how bad the window was — the top-N
@@ -34,18 +35,18 @@ const MERGE_FAILURE_TOP = 10;
34
35
  const NO_MERGE_REASON = "no reason recorded";
35
36
  const MERGED_TOP = 10;
36
37
  const SUMMARY_MAX = 100;
37
- /** Keep only events that belong to one base role, so a scoped digest aggregates instances
38
- * (`feature-2`) into their group exactly as `report --role` and the per-role cap do. Loop
39
- * events match on their own loop id; the role-scoped harness transitions (a role pause, its
40
- * streak breaker, its cap, its fallback) log with loop "harness" and name the affected role
38
+ /** Keep only events that belong to the scoped base roles, so a scoped digest aggregates instances
39
+ * (`feature-2`) into their group exactly as `report --role` and the per-role cap do. Role
40
+ * events match on their own role id; the role-scoped harness transitions (a role pause, its
41
+ * streak breaker, its cap, its fallback) log with source "harness" and name the affected role
41
42
  * in `role`, so those match on that field too — otherwise a scoped digest would drop exactly
42
- * the events that explain why the loop stopped. Applied to both partitions and the whole read,
43
+ * the events that explain why the role stopped. Applied to both partitions and the whole read,
43
44
  * so every section derived from them narrows. */
44
- function scopeToRole(events, base) {
45
- return events.filter((ev) => baseRoleOf(eventRole(ev)) === base ||
45
+ function scopeToRole(events, bases) {
46
+ return events.filter((ev) => bases.includes(baseRoleOf(eventRole(ev))) ||
46
47
  (STATE_CHANGE_TYPES.has(ev.type) &&
47
48
  typeof ev.role === "string" &&
48
- baseRoleOf(ev.role) === base));
49
+ bases.includes(baseRoleOf(ev.role))));
49
50
  }
50
51
  /** Attach a section's window total (events the section owns, counted before any filter the
51
52
  * clustering applies — for rejections, the reasons-less ones too) to the clustered result. */
@@ -65,10 +66,10 @@ function messageSection(events, type, top, urgent) {
65
66
  }
66
67
  /** The merge-failure section: each blocked or conflicted `merge_failed` clusters on
67
68
  * (role, result, reason).
68
- * `merge_failed` carries no message, so the reason is the same role's nearest preceding
69
- * `warning` (a red merge check, a structure or fix-claim block) logged after that role's
70
- * previous merge outcome; with none, the placeholder. The total counts every blocked or
71
- * conflicted `merge_failed` in the window. */
69
+ * A `merge_failed` with a `reason` field uses it; an older one carries none, so the reason is
70
+ * the same role's nearest preceding `warning` (a red merge check, a structure or fix-claim
71
+ * block) logged after that role's previous merge outcome; with none, the placeholder. The
72
+ * total counts every blocked or conflicted `merge_failed` in the window. */
72
73
  function mergeFailureSection(events) {
73
74
  const ordered = [...events].sort((a, b) => a.ts - b.ts);
74
75
  const lastWarning = new Map();
@@ -91,7 +92,8 @@ function mergeFailureSection(events) {
91
92
  continue;
92
93
  }
93
94
  total++;
94
- const reason = lastWarning.get(role) ?? NO_MERGE_REASON;
95
+ const own = typeof ev.reason === "string" ? ev.reason.trim() : "";
96
+ const reason = own !== "" ? own : (lastWarning.get(role) ?? NO_MERGE_REASON);
95
97
  lastWarning.delete(role);
96
98
  messages.push({ message: `${result} — ${reason}`, role, ts: ev.ts, keyPrefix: `${role}\u0000` });
97
99
  }
@@ -102,8 +104,8 @@ function roleStats(events) {
102
104
  const byRole = new Map();
103
105
  const statsFor = (role) => getOrCreate(byRole, role, () => ({ runs: 0, errors: 0, quietKills: 0, rejections: 0 }));
104
106
  for (const ev of events) {
105
- // Ticks are run_end events; a rejection is now recorded by the merge slot, AFTER the
106
- // authoring tick has already ended `queued` (plans/merge-queue.md 3/5). review_rejected is
107
+ // Runs are run_end events; a rejection is now recorded by the merge slot, AFTER the
108
+ // authoring run has already ended `queued` (plans/merge-queue.md 3/5). review_rejected is
107
109
  // the one event every reject path logs exactly once (src/review/review.ts), so it — not a
108
110
  // run_end result no production path emits — is the rejection source.
109
111
  if (ev.type === "run_end") {
@@ -121,7 +123,7 @@ function roleStats(events) {
121
123
  return byRole;
122
124
  }
123
125
  /** Bounded memo of whole failure digests, keyed by root and day count. The dashboard's Failures
124
- * tab, the TUI's Ctrl+T pane, and every telemetry tick re-request the same digest, and the
126
+ * tab, the TUI's Ctrl+T pane, and every selfcheck run re-request the same digest, and the
125
127
  * collector re-reads and re-parses up to 2× the window's log bytes each time (~4 MB on this
126
128
  * repo's own log, growing toward the rotation cap). An unchanged log and window cost one stat
127
129
  * and return the prior result; any change — an append, a rotation, or a new local day —
@@ -131,10 +133,7 @@ const failureDigestCache = new Map();
131
133
  const FAILURE_DIGEST_CACHE_MAX = 8;
132
134
  /** The live log's identity, all zero when the file is missing (the empty-log case). */
133
135
  function digestStat(root) {
134
- const st = statOrNull(eventsLogPath(root));
135
- return st
136
- ? { size: st.size, mtimeMs: st.mtimeMs, dev: st.dev, ino: st.ino }
137
- : { size: 0, mtimeMs: 0, dev: 0, ino: 0 };
136
+ return statIdentityOrZero(statOrNull(eventsLogPath(root)));
138
137
  }
139
138
  /** Collect the digest over the last `days` local calendar days, reading a 2×-long window once
140
139
  * and partitioning it in memory so the delta against the preceding equal window costs no
@@ -144,10 +143,10 @@ export function collectFailureReport(root, days, role) {
144
143
  const now = new Date();
145
144
  const { from, to } = dayWindow(days, now);
146
145
  const priorFrom = formatDate(dayAt(days * 2 - 1, now));
147
- const base = role === undefined ? undefined : baseRoleOf(role);
146
+ const bases = scopeBases(role);
148
147
  // The base role rides the key, so a scoped and a team digest of the same log cannot
149
148
  // collide in the memo.
150
- const key = `${root}\u0000${days}\u0000${base ?? ""}`;
149
+ const key = `${root}\u0000${days}\u0000${bases?.join(",") ?? ""}`;
151
150
  const stat = digestStat(root);
152
151
  const cached = failureDigestCache.get(key);
153
152
  if (cached?.from === from &&
@@ -177,29 +176,30 @@ export function collectFailureReport(root, days, role) {
177
176
  * the window and the equal-length window immediately before it (the same current/prior split
178
177
  * the day collector builds, so the Deltas table compares like with like). The read starts at the
179
178
  * prior half's own day key, exactly as the day collector reads 2× its window; the cutoff instant
180
- * for coverage is the prior half's start, which a day key alone cannot prove. */
181
- export function collectFailureReportSince(root, sinceMs, role) {
179
+ * for coverage is the prior half's start, which a day key alone cannot prove. With `untilMs` the
180
+ * window is the bounded `[now - sinceMs, now - untilMs)`; the prior window then has that
181
+ * window's own length, `sinceMs - untilMs`. */
182
+ export function collectFailureReportSince(root, sinceMs, role, untilMs) {
182
183
  assertReportSinceMs(sinceMs);
183
- const now = Date.now();
184
- const cutoff = now - sinceMs;
185
- const priorFrom = cutoff - sinceMs;
184
+ const { now, cutoff, end, slice } = sinceWindowTiming(sinceMs, untilMs);
185
+ const length = sinceMs - (untilMs ?? 0);
186
+ const priorFrom = cutoff - length;
186
187
  const raw = readWindowEvents(root, dayKey(priorFrom));
187
188
  const current = [];
188
189
  const prior = [];
189
190
  for (const ev of raw.events) {
190
191
  if (typeof ev.ts !== "number")
191
192
  continue;
192
- if (ev.ts >= cutoff && ev.ts <= now)
193
+ if (ev.ts >= cutoff && (untilMs === undefined ? ev.ts <= now : ev.ts < end))
193
194
  current.push(ev);
194
195
  else if (ev.ts >= priorFrom && ev.ts < cutoff)
195
196
  prior.push(ev);
196
197
  }
197
198
  return assembleFailureReport(current, prior, raw.events, eventWindowCovers(raw, priorFrom), raw.logState, {
198
- days: Math.ceil(sinceMs / 86_400_000),
199
+ days: Math.ceil(length / 86_400_000),
199
200
  from: dayKey(cutoff),
200
- to: dayKey(now),
201
- sinceMs,
202
- fromIso: new Date(cutoff).toISOString(),
201
+ to: dayKey(end),
202
+ ...slice,
203
203
  }, role);
204
204
  }
205
205
  /** Assemble the digest's sections from the two event partitions, the full read, and the
@@ -210,16 +210,16 @@ function assembleFailureReport(currentRaw, priorRaw, allEventsRaw, coversFullWin
210
210
  // Scope first when a role was named: every section below derives from these partitions, so
211
211
  // narrowing them at the source scopes outcomes, deltas, clusters, merges, and time/spend at
212
212
  // once. The unfiltered read's first event is kept for the retention line, so a scoped partial
213
- // window still names when the log starts, not when the scoped role's own first tick does.
214
- const base = role === undefined ? undefined : baseRoleOf(role);
215
- const current = base === undefined ? currentRaw : scopeToRole(currentRaw, base);
216
- const prior = base === undefined ? priorRaw : scopeToRole(priorRaw, base);
217
- const allEvents = base === undefined ? allEventsRaw : scopeToRole(allEventsRaw, base);
213
+ // window still names when the log starts, not when the scoped role's own first run does.
214
+ const bases = scopeBases(role);
215
+ const current = bases === undefined ? currentRaw : scopeToRole(currentRaw, bases);
216
+ const prior = bases === undefined ? priorRaw : scopeToRole(priorRaw, bases);
217
+ const allEvents = bases === undefined ? allEventsRaw : scopeToRole(allEventsRaw, bases);
218
218
  const oldestRetained = allEventsRaw.length > 0 ? eventDayKey(allEventsRaw[0]) : null;
219
- const tickEvents = current.filter((ev) => ev.type === "run_end");
219
+ const runEvents = current.filter((ev) => ev.type === "run_end");
220
220
  // Outcome table: one row per role, counts per result that occurred.
221
221
  const outcomeMap = new Map();
222
- for (const ev of tickEvents) {
222
+ for (const ev of runEvents) {
223
223
  const role = eventRole(ev);
224
224
  const counts = getOrCreate(outcomeMap, role, () => ({}));
225
225
  const result = ev.result;
@@ -231,12 +231,12 @@ function assembleFailureReport(currentRaw, priorRaw, allEventsRaw, coversFullWin
231
231
  // (pinned by the tie test in test/failure-render.test.ts).
232
232
  const outcomes = rankByCount([...outcomeMap.entries()].map(([role, counts]) => ({ role, counts })), (row) => total(row.counts), (row) => row.role);
233
233
  // Time and spend: the same runs priced by wall-clock span and cost, per role × outcome
234
- // class, plus the loss ranking that weighs causes by agent-hours rather than tick counts.
235
- const { timeSpend, lossCauses, lossCausesHidden } = timeAndSpend(tickEvents, allEvents);
236
- // Prompt-token telemetry: per role, the median prompt tokens a tick sent and how much of
234
+ // class, plus the loss ranking that weighs causes by agent-hours rather than run counts.
235
+ const { timeSpend, lossCauses, lossCausesHidden } = timeAndSpend(runEvents, allEvents);
236
+ // Prompt-token telemetry: per role, the median prompt tokens a run sent and how much of
237
237
  // the prompt budget went out before the first edit. Only runs carrying the field count, so
238
238
  // a pre-feature log leaves the section off entirely.
239
- const promptStats = promptStatsFor(tickEvents);
239
+ const promptStats = promptStatsFor(runEvents);
240
240
  // Deltas: current vs preceding window, per role.
241
241
  const curStats = roleStats(current);
242
242
  const prevStats = roleStats(prior);
@@ -260,8 +260,8 @@ function assembleFailureReport(currentRaw, priorRaw, allEventsRaw, coversFullWin
260
260
  .sort((a, b) => b.runs - a.runs ||
261
261
  b.prevRuns - a.prevRuns ||
262
262
  a.role.localeCompare(b.role));
263
- // Only runs that ENDED as errors carry a tick error: `state.lastError` is shared state the
264
- // merge slot also writes, so a successful tick's `run_end` can carry a leftover merge failure's
263
+ // Only runs that ENDED as errors carry a run error: `state.lastError` is shared state the
264
+ // merge slot also writes, so a successful run's `run_end` can carry a leftover merge failure's
265
265
  // text (BUGS.md 2026-09-21). `main_red` joins them because its cause is written for that result
266
266
  // — the baseline gate's own text (src/baseline/main-red.ts), never a leftover — so the
267
267
  // section itemizes the outcome that blocks every merge instead of leaving it a bare count
@@ -271,7 +271,7 @@ function assembleFailureReport(currentRaw, priorRaw, allEventsRaw, coversFullWin
271
271
  // error-text test — so a malformed run_end that skipped the text can never be counted above
272
272
  // but itemized nowhere: those events cluster under a placeholder cause, keeping the section's
273
273
  // total equal to the tables it cross-checks against (BUGS.md 2026-09-30).
274
- const errorEvents = tickEvents.filter((ev) => ev.result === "error" || ev.result === "main_red");
274
+ const errorEvents = runEvents.filter((ev) => ev.result === "error" || ev.result === "main_red");
275
275
  const errors = section(clusterMessages(errorEvents.map((ev) => ({
276
276
  message: errorTextOrPlaceholder(ev.error),
277
277
  role: eventRole(ev),
@@ -281,22 +281,26 @@ function assembleFailureReport(currentRaw, priorRaw, allEventsRaw, coversFullWin
281
281
  ? { ...ev, message: ev.message.replace(OLD_MERGE_CHECK_WORDING, "merge check failed (kept for one retry)") }
282
282
  : ev), "warning", WARNING_TOP, (m) => URGENT_WARNING.test(m));
283
283
  // Rejections cluster on (role, reasons[0]) — the field the event feed renders — so two
284
- // different rejection reasons from the same role stay separate rows. The section's total
285
- // counts every `review_rejected` in the window, reasons or not, so it equals the Deltas
286
- // column and the render's remainder line cross-checks against it.
284
+ // different rejection reasons from the same role stay separate rows, and an already-landed
285
+ // one (ev.afterMerge === true) splits apart from a same-reason pre-merge one so the row names
286
+ // the post-merge class whose rejection stands recorded on main. The section's total counts
287
+ // every `review_rejected` in the window, reasons or not, so it equals the Deltas column and
288
+ // the render's remainder line cross-checks against it.
287
289
  const rejectedEvents = current.filter((ev) => ev.type === "review_rejected");
290
+ const postMergeRejections = rejectedEvents.filter((ev) => ev.afterMerge === true).length;
288
291
  const rejections = section(clusterMessages(rejectedEvents
289
292
  .filter((ev) => Array.isArray(ev.reasons))
290
293
  .map((ev) => {
291
294
  const reasons = ev.reasons;
292
- return { message: firstReason(reasons), role: eventRole(ev), ts: ev.ts, keyPrefix: `${eventRole(ev)}\u0000` };
295
+ const message = ev.afterMerge === true ? `post-merge rejection: ${firstReason(reasons)}` : firstReason(reasons);
296
+ return { message, role: eventRole(ev), ts: ev.ts, keyPrefix: `${eventRole(ev)}\u0000` };
293
297
  }), REJECTION_TOP), rejectedEvents.length);
294
298
  const mergeFailures = mergeFailureSection(current);
295
299
  // Merge review failures: a reviewer that could not return a parseable verdict (a dead
296
- // backend, a transport error) fails closed and keeps the commit, but its authoring tick ends
300
+ // backend, a transport error) fails closed and keeps the commit, but its authoring run ends
297
301
  // `queued`/`no_change` — so the failure lives on the `review_failed` event, never on
298
- // `run_end.error`. Clustered on their own so the telemetry role can see a gate that keeps
299
- // failing without misreading it as a tick error (BUGS.md 2026-09-21).
302
+ // `run_end.error`. Clustered on their own so the selfcheck role can see a gate that keeps
303
+ // failing without misreading it as a run error (BUGS.md 2026-09-21).
300
304
  const reviewFailures = messageSection(current, "review_failed", REVIEW_FAILURE_TOP);
301
305
  // The newest MERGED_TOP merges, newest first. The pre-slice count rides along, as it does for
302
306
  // the state changes below, so the render can name what the cut left out — a busy window must
@@ -335,8 +339,8 @@ function assembleFailureReport(currentRaw, priorRaw, allEventsRaw, coversFullWin
335
339
  const windowComplete = coversFullWindow;
336
340
  return {
337
341
  ...window,
338
- ...(base === undefined ? {} : { role: base }),
339
- runs: tickEvents.length,
342
+ ...(bases === undefined ? {} : { role: bases.join(",") }),
343
+ runs: runEvents.length,
340
344
  hasEvents,
341
345
  emptyLog: !hasEvents && !coversFullWindow,
342
346
  logState,
@@ -352,6 +356,7 @@ function assembleFailureReport(currentRaw, priorRaw, allEventsRaw, coversFullWin
352
356
  warnings,
353
357
  reviewFailures,
354
358
  rejections,
359
+ postMergeRejections,
355
360
  mergeFailures,
356
361
  merged,
357
362
  mergedTotal: allMerged.length,
@@ -365,9 +370,9 @@ function median(values) {
365
370
  const mid = Math.floor(sorted.length / 2);
366
371
  return sorted.length % 2 === 0 ? (sorted[mid - 1] + sorted[mid]) / 2 : sorted[mid];
367
372
  }
368
- /** Per-role prompt-token stats from the window's run_end events. Ticks without a
373
+ /** Per-role prompt-token stats from the window's run_end events. Runs without a
369
374
  * `promptTokens` reading contribute nothing (and skip a role entirely), so the section only
370
- * appears once some tick ran on a build that records the field. Both reads go through
375
+ * appears once some run ran on a build that records the field. Both reads go through
371
376
  * nonNegativeNumber like every other event-log number: a torn or hand-edited line whose
372
377
  * `promptTokens` is `1e999` (JSON parses to Infinity) would otherwise land in the median and
373
378
  * the `preEditSum / promptSum` ratio as a non-finite value no report can render. */
@@ -1,14 +1,14 @@
1
1
  import { agree, plural } from "../text/phrases.js";
2
2
  import { shortSha, usd } from "../text/format.js";
3
- import { dayKey, dayLabel, formatTime, monthDay, reportWindow, sinceWindowLine } from "../text/datetime.js";
3
+ import { boundedWindowLine, clockHm, dayKey, dayLabel, monthDay, reportWindow, sinceWindowLine } from "../text/datetime.js";
4
4
  import { durationLabel } from "../cli/cli-args.js";
5
5
  import { eventsRotationLabel } from "../events/events.js";
6
6
  import { eventTypeLabel } from "../events/event-labels.js";
7
7
  import { markdownTable } from "../text/markdown.js";
8
8
  /** A cluster's role list shows at most this many names before a "+N more" remainder. */
9
9
  const ROLES_SHOWN = 4;
10
- /** Every `TickResult`, mapped to its display/column rank. Typed as a `Record<TickResult, …>`,
11
- * so adding a result to src/tick/tick-outcome.ts and forgetting it here is a compile error — the
10
+ /** Every `RunResult`, mapped to its display/column rank. Typed as a `Record<RunResult, …>`,
11
+ * so adding a result to src/run/run-outcome.ts and forgetting it here is a compile error — the
12
12
  * vocabulary is closed by the type checker, not by a comment. Order runs from "made progress"
13
13
  * through "did not" to the operator/shutdown outcomes. */
14
14
  const RESULT_ORDER = {
@@ -27,7 +27,7 @@ const RESULT_ORDER = {
27
27
  aborted: 12,
28
28
  user_aborted: 13,
29
29
  };
30
- /** The digest table's columns: every TickResult that at least one role logged in the window, ranked
30
+ /** The digest table's columns: every RunResult that at least one role logged in the window, ranked
31
31
  * by RESULT_ORDER (ties alphabetical), so the table's shape follows the window's evidence instead
32
32
  * of the whole fixed vocabulary — a team with no errors gets no error column. */
33
33
  function columns(outcomes) {
@@ -41,13 +41,13 @@ function columns(outcomes) {
41
41
  /** A delta cell: `prev → cur`, with "—" on a side where the role had no runs (the "new role"
42
42
  * case for the preceding window), so an absence is never rendered as an infinite increase.
43
43
  * The displayed halves may be raw counts (stringified here) or pre-rendered rate strings —
44
- * the tick-count halves exist only to decide the "—", not to be printed. */
44
+ * the run-count halves exist only to decide the "—", not to be printed. */
45
45
  function deltaCell(prevRuns, prev, runs, cur) {
46
46
  return `${prevRuns === 0 ? "—" : prev} → ${runs === 0 ? "—" : cur}`;
47
47
  }
48
48
  /** The rejections column's cell. It cannot reuse deltaCell: a rejection is logged by the
49
49
  * merge slot, so its authoring run_end can fall in the other window — a role with a
50
- * rejection must show it even when its tick count is 0. Presence is judged on the rejection
50
+ * rejection must show it even when its run count is 0. Presence is judged on the rejection
51
51
  * count itself (the role is "absent" from the column only with neither runs nor rejections). */
52
52
  function rejectionCell(prevRuns, prevRejections, runs, rejections) {
53
53
  const prev = prevRuns === 0 && prevRejections === 0 ? "—" : String(prevRejections);
@@ -86,7 +86,7 @@ function rate(errors, runs) {
86
86
  return "—";
87
87
  return percent(errors, runs);
88
88
  }
89
- /** One time-and-spend cell: `x.x h · $y.yy`, or "—" when the role ended no tick on that
89
+ /** One time-and-spend cell: `x.x h · $y.yy`, or "—" when the role ended no run on that
90
90
  * class — an absence is not a zero cost, for the same reason the Deltas cells say "—".
91
91
  * Hours carry one decimal: the digest prices agent-hours, and whole-hour rounding would
92
92
  * erase the difference between ten 200-ms errors and one 30-minute timeout. */
@@ -103,13 +103,13 @@ function hoursPhrase(ms) {
103
103
  }
104
104
  /** Render the digest as bounded Markdown. Byte bound: for a given team the tables grow only with
105
105
  * the number of configured roles (fixed by config), the RESULT_ORDER vocabulary (fixed by
106
- * src/tick/tick-outcome.ts), and the time-and-spend section's LOSS_TOP loss-cause lines (fixed by
106
+ * src/run/run-outcome.ts), and the time-and-spend section's LOSS_TOP loss-cause lines (fixed by
107
107
  * the cut), the merge-failure section's MERGE_FAILURE_TOP clusters (src/failure/failure-data.ts),
108
108
  * and every free string is capped — cluster examples at 120 chars (plus a `… (+N
109
109
  * chars)` cut marker when truncated, so a marked cut can never read as a complete message —
110
110
  * BUGS.md 2026-09-30), merged summaries at 100 under the same rule, a cluster's role list at 4
111
- * names plus a remainder count, and any loop id sliced to 32 chars (config validation already
112
- * refuses longer custom-loop ids, so the slice is a guard rather than the real bound). Cluster
111
+ * names plus a remainder count, and any role id sliced to 32 chars (config validation already
112
+ * refuses longer custom-role ids, so the slice is a guard rather than the real bound). Cluster
113
113
  * counts are capped at top-N, and the Team state changes section is capped at STATE_CHANGE_TOP
114
114
  * lines with each payload at STATE_CHANGE_MAX and each free field at STATE_CHANGE_FIELD_MAX; the
115
115
  * merged list at MERGED_TOP lines, each capped section closing with one remainder line when its cut
@@ -124,9 +124,12 @@ export function renderFailureMarkdown(data) {
124
124
  // The since shape prints the same sections as the day shape but labels its window as an
125
125
  // instant span (the wording renderSinceReportMarkdown uses); the day shape keeps its
126
126
  // day-window line. The empty branch follows the same split so neither claims the other's unit.
127
- lines.push(data.sinceMs !== undefined
128
- ? `${sinceWindowLine(durationLabel(data.sinceMs), data.fromIso)} · ${data.runs} runs`
129
- : `${reportWindow(data.from, data.to, data.days, eventsRotationLabel())} · ${data.runs} runs`);
127
+ const bounded = data.sinceMs !== undefined && data.untilMs !== undefined && data.toIso !== undefined;
128
+ lines.push(bounded
129
+ ? `${boundedWindowLine(durationLabel(data.sinceMs), durationLabel(data.untilMs), data.fromIso, data.toIso)} · ${data.runs} runs`
130
+ : data.sinceMs !== undefined
131
+ ? `${sinceWindowLine(durationLabel(data.sinceMs), data.fromIso)} · ${data.runs} runs`
132
+ : `${reportWindow(data.from, data.to, data.days, eventsRotationLabel())} · ${data.runs} runs`);
130
133
  if (data.role !== undefined)
131
134
  lines.push(`role: ${data.role}`);
132
135
  if (data.logState === "unreadable")
@@ -134,9 +137,11 @@ export function renderFailureMarkdown(data) {
134
137
  else if (data.emptyLog)
135
138
  lines.push("no events retained");
136
139
  else if (!data.hasEvents) {
137
- lines.push(data.sinceMs !== undefined
138
- ? `no events in the last ${durationLabel(data.sinceMs)}`
139
- : `no events in the last ${dayLabel(data.days)}`);
140
+ lines.push(bounded
141
+ ? `no events from ${durationLabel(data.sinceMs)} to ${durationLabel(data.untilMs)} ago`
142
+ : data.sinceMs !== undefined
143
+ ? `no events in the last ${durationLabel(data.sinceMs)}`
144
+ : `no events in the last ${dayLabel(data.days)}`);
140
145
  }
141
146
  else if (data.partial)
142
147
  lines.push(`partial: retained log starts ${data.oldestEventDate ?? "?"}`);
@@ -216,7 +221,7 @@ export function renderFailureMarkdown(data) {
216
221
  }
217
222
  }
218
223
  // Prompt-token telemetry: the per-role median prompt
219
- // send and how much of the prompt budget goes out before a tick's first edit. Omitted
224
+ // send and how much of the prompt budget goes out before a run's first edit. Omitted
220
225
  // entirely when no run_end in the window carries the fields, so old logs and their golden
221
226
  // digests render unchanged.
222
227
  if (data.promptStats.length > 0) {
@@ -228,7 +233,7 @@ export function renderFailureMarkdown(data) {
228
233
  }
229
234
  }
230
235
  lines.push("");
231
- lines.push(`## Deltas vs the preceding ${data.sinceMs !== undefined ? durationLabel(data.sinceMs) : dayLabel(data.days)}`);
236
+ lines.push(`## Deltas vs the preceding ${data.sinceMs !== undefined ? durationLabel(data.sinceMs - (data.untilMs ?? 0)) : dayLabel(data.days)}`);
232
237
  if (data.deltas.length === 0) {
233
238
  lines.push("_no runs in either window_");
234
239
  }
@@ -242,13 +247,16 @@ export function renderFailureMarkdown(data) {
242
247
  ])));
243
248
  }
244
249
  // The title restates the section's contract: its total now covers the Outcome table's error
245
- // column AND its main_red cells (the baseline gate's own cause rides the tick, BUGS.md
250
+ // column AND its main_red cells (the baseline gate's own cause rides the run, BUGS.md
246
251
  // 2026-09-28), so an operator cross-checking counts against the tables above is not misled
247
252
  // by a title that reads as the error column alone.
248
253
  renderClusters(lines, "Top error clusters (red-main causes included)", data.errors, "run errors");
249
254
  renderClusters(lines, "Top warning clusters", data.warnings, "warnings");
250
255
  renderClusters(lines, "Top review failure clusters", data.reviewFailures, "review failures");
251
256
  renderClusters(lines, "Top rejection clusters", data.rejections, "rejections");
257
+ if (data.postMergeRejections > 0) {
258
+ lines.push(`_${plural(data.postMergeRejections, "post-merge rejection", "post-merge rejections")} — the commit already reached main, so it stays there with the reviewer's recorded reasons (never re-reviewed — Forward, never back)_`);
259
+ }
252
260
  renderClusters(lines, "Merge failures", data.mergeFailures, "merge failures");
253
261
  lines.push("");
254
262
  lines.push("## Merged in the window");
@@ -297,7 +305,7 @@ function roleList(roles) {
297
305
  const rest = roles.length - ROLES_SHOWN;
298
306
  return rest > 0 ? `${shown} +${rest} more` : shown;
299
307
  }
300
- /** Config validation caps custom-loop ids at 32 chars; this slice keeps the render's byte bound
308
+ /** Config validation caps custom-role ids at 32 chars; this slice keeps the render's byte bound
301
309
  * true even for a hand-edited state file or a future catalog id. */
302
310
  function roleCell(role) {
303
311
  return role.slice(0, 32);
@@ -305,5 +313,5 @@ function roleCell(role) {
305
313
  /** A transition line's local `MM-DD HH:MM` stamp — the year is redundant inside the window and
306
314
  * the seconds add bytes without adding causality. */
307
315
  function stateChangeStamp(ts) {
308
- return `${monthDay(dayKey(ts))} ${formatTime(new Date(ts)).slice(0, 5)}`;
316
+ return `${monthDay(dayKey(ts))} ${clockHm(ts)}`;
309
317
  }
@@ -1,4 +1,3 @@
1
- // vocabulary: plain
2
1
  /** Wording half of the failure digest's Team state changes section: which transition event types
3
2
  * the digest replays and the bounded one-liner describing each. Split out of failure-data.ts (the
4
3
  * same seam build/build-check-events.ts split from build/build-check.ts) so the per-event-type
@@ -7,15 +6,15 @@
7
6
  * sliced here, so the digest's byte bound holds for any event shape; the render adds the timestamp
8
7
  * and a roleCell-sliced role, so no unbounded field reaches the page. */
9
8
  import { truncateExample } from "./failure-cluster.js";
10
- import { cutSplitsSurrogatePair } from "../text/text.js";
9
+ import { cutSplitsSurrogatePair, isNonBlankString } from "../text/text.js";
11
10
  import { finiteNumber, stringList } from "../files/json-object.js";
12
- import { ORCHESTRATOR_STOPPED, backendKindPhrase, budgetPhrase, teamPausePhrase, teamResumePhrase, holdPhrase, plural, rolePausePhrase, roleResumePhrase, rolesPhrase, shortSpanPhrase } from "../text/phrases.js";
11
+ import { orchestratorStopPhrase, backendKindPhrase, budgetPhrase, teamPausePhrase, teamResumePhrase, holdPhrase, plural, rolePausePhrase, roleResumePhrase, rolesPhrase, shortSpanPhrase, PAUSE_EXPIRED_SUFFIX } from "../text/phrases.js";
13
12
  import { shortSha } from "../text/format.js";
14
- import { deferredReasonText } from "../events/event-format.js";
13
+ import { deferredReasonText, untilClockSuffix } from "../events/event-format.js";
15
14
  /** The transition events the digest replays: the decisions the harness made about itself (the
16
15
  * cap/team gates and the team hold, the error-streak breaker's pause, the role's own daily
17
16
  * cap, model fallbacks, live-config edits, self-hosted redeploys, need-based deferrals,
18
- * orchestrator lifecycle). They are the evidence the telemetry role's load-bearing
17
+ * orchestrator lifecycle). They are the evidence the selfcheck role's load-bearing
19
18
  * rule asks it to judge — whether the harness's RESPONSE to a failure was wrong
20
19
  * (plans/telemetry-role.md) — so they sit beside the outcomes rather than being dropped. */
21
20
  export const STATE_CHANGE_TYPES = new Set([
@@ -101,20 +100,22 @@ export function describeStateChange(ev) {
101
100
  text = `budget resumed (${budgetPhrase(ev.spentUsd, ev.capUsd)} today)`;
102
101
  break;
103
102
  case "team_paused":
104
- text = teamPausePhrase();
103
+ text = teamPausePhrase() + untilClockSuffix(ev.until);
104
+ if (isNonBlankString(ev.reason))
105
+ text += `: ${field(ev.reason)}`;
105
106
  break;
106
107
  case "team_resumed":
107
- text = teamResumePhrase();
108
+ text = teamResumePhrase() + (ev.expired === true ? PAUSE_EXPIRED_SUFFIX : "");
108
109
  break;
109
110
  case "role_paused":
110
- text = rolePausePhrase(field(ev.role ?? "?"));
111
+ text = rolePausePhrase(field(ev.role ?? "?")) + untilClockSuffix(ev.until);
111
112
  break;
112
113
  case "role_resumed":
113
- text = roleResumePhrase(field(ev.role ?? "?"));
114
+ text = roleResumePhrase(field(ev.role ?? "?")) + (ev.expired === true ? PAUSE_EXPIRED_SUFFIX : "");
114
115
  break;
115
116
  case "role_streak_paused":
116
117
  // The error-streak breaker auto-paused the role; the digest names the depth so the
117
- // operator can connect the pause to the failing ticks the clusters above show.
118
+ // operator can connect the pause to the failing runs the clusters above show.
118
119
  text = `role ${field(ev.role ?? "?")} paused — ${field(ev.streak ?? "?")} runs failed in a row`;
119
120
  break;
120
121
  case "role_cap_paused":
@@ -178,7 +179,11 @@ export function describeStateChange(ev) {
178
179
  break;
179
180
  }
180
181
  case "build_stale":
181
- text = `update available (${plural(finiteNumber(ev.aheadCommits, 0), "commit")} behind) — restarts when idle`;
182
+ // The digest's 72-char line cap (§ STATE_CHANGE_MAX) cannot hold the full alert phrase
183
+ // "restarts after cooldown; work continues" beside the ahead-count prefix, so it says
184
+ // the same thing compactly: the restart waits while work continues (cooldown scope,
185
+ // PLANS.md 2026-10-23).
186
+ text = `update available (${plural(finiteNumber(ev.aheadCommits, 0), "commit")} behind) — restart waits; work continues`;
182
187
  break;
183
188
  case "restart_pending":
184
189
  text = `restart pending — main ${shortSha(ev.head)} green`;
@@ -199,7 +204,7 @@ export function describeStateChange(ev) {
199
204
  text = `tumwater started (pid ${field(ev.pid)}${ev.build ? `, build ${shortSha(ev.build)}` : ""})`;
200
205
  break;
201
206
  case "orchestrator_stop":
202
- text = ORCHESTRATOR_STOPPED;
207
+ text = orchestratorStopPhrase(ev.cause);
203
208
  break;
204
209
  case "supervisor_exit":
205
210
  text = `tumwater down — restart ${field(ev.generation)} ${ev.signal ? `killed by ${field(ev.signal)}` : `exited ${field(ev.code)}`}${ev.reason ? `: ${field(ev.reason)}` : ""}`;