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
@@ -0,0 +1,155 @@
1
+ import { textOr } from "../text/text.js";
2
+ import { groupBy } from "../collections.js";
3
+ /** The merge bookkeeping resolveQueuedResult joins over: per role, the `merge_queued` pins
4
+ * and the `merge_done`/`merge_failed` outcomes, each list oldest-first (the scan order). Exported
5
+ * as one helper with exactly three call sites — readRunRows' history-row join, the failure
6
+ * digest's time-and-spend fold (time-spend.ts), and run-detail-data.ts — so the collectors
7
+ * cannot disagree about which events feed the join or in what order they sit: a fourth
8
+ * merge-bookkeeping event
9
+ * type, or a changed order rule, is edited once here. Callers bucket only the events they
10
+ * need; the digest folds its `review_rejected` reasons with its own pass.
11
+ * Roles or cutoffs are the caller's concern: pass a role-scoped or wider-read event list and
12
+ * the maps carry exactly that list's evidence. */
13
+ export function bucketMergeEvents(events) {
14
+ const mergeQueuedByRunner = groupBy(events.filter((e) => e.type === "merge_queued"), (e) => e.source);
15
+ const outcomeByRunner = groupBy(events.filter((e) => e.type === "merge_done" || e.type === "merge_failed"), (e) => e.source);
16
+ return { mergeQueuedByRunner, outcomeByRunner };
17
+ }
18
+ /** Resolve one queued run row's eventual merge verdict: a "queued" run_end carries no
19
+ * result until its merge batch merges or fails, so the row joins its `end` run_end to the
20
+ * run's merge_queued pin and that pin's merge_done/merge_failed outcome (both from the
21
+ * bucketMergeEvents maps, oldest-first per role). The claim cursor `claimTop` (mutated in
22
+ * place) walks the pin list newest-first and hands each queued run the newest unclaimed pin
23
+ * at or before its end, so several queued runs cannot claim one pin.
24
+ *
25
+ * One change can take several merge attempts: a retriable result (RETRIABLE_MERGE_RESULTS:
26
+ * review_error, merge_conflict, merge_blocked) keeps the pin, and the role's next run — a
27
+ * leftover-recovery run that does no work of its own and ends in about a second — re-enqueues
28
+ * it (isRecoveryPin). The authoring run and the recovery runs resolve by different rules, so
29
+ * every attempt's verdict shows on exactly one row:
30
+ * - The run that authored the change (its pin is not a re-enqueue of a kept attempt) takes
31
+ * the change's FINAL verdict, not its first attempt's: a retriable failure that recovery
32
+ * later merges reads "changed" (BUGS.md 2026-10-10). While a re-enqueued attempt is still
33
+ * pending the row keeps the raw "queued" label — the change is waiting to merge again, and
34
+ * the failure before it already shows on the recovery run's row.
35
+ * - A recovery run takes the verdict of the attempt it re-enqueued — the retriable failure
36
+ * that kept the pin and made the run happen (recoveredAttemptVerdict). So a change that
37
+ * timed out in review seven times and was then rejected reads "rejected" once (the
38
+ * authoring row) and "review failed" seven times (the authoring attempt's and six
39
+ * recoveries' timeouts, each on the recovery row that followed it), not "rejected" on all
40
+ * eight rows.
41
+ * When main moved before a retry the verification's syncPinToMain rebased the ref
42
+ * (merge-core.ts), so the recovery pin carries the rebased sha while each attempt's own
43
+ * merge_done/merge_failed logs its entry's sha (merge-slot.ts's entry.sha): the attempts' shas
44
+ * differ, so the authoring row's walk follows the pin sequence (isRecoveryPin), not sha
45
+ * equality alone. It stops at a pin that is not a recovery re-enqueue — a fresh change after a
46
+ * conflict-cap discard — so the row never credits a different change's verdict (BUGS.md
47
+ * 2026-10-10). A recovery pin whose previous attempt shows no retriable verdict (a crash
48
+ * left the commit unpinned or the merge unfinished, or that attempt sits outside the read) is
49
+ * the change's first visible attempt, so its row takes the authoring rule.
50
+ *
51
+ * `joinStarts` maps role#run to its run_start's ts; when `floorTs` (a since-window's cutoff,
52
+ * null in the count view) sits after that start, the row's join evidence is not whole in the
53
+ * windowed read and the row keeps its raw "queued" label (null). A MISSING start needs no
54
+ * guard — it joins, because rotation drops a prefix, so every pin older than the lost start is
55
+ * lost with it and nothing can be mis-claimed (and a skipped run has no start at all but is
56
+ * never queued). A merge with no outcome event in the join set also keeps the raw label: the
57
+ * conservative fallback for a verdict simply not visible here. Returns the verdict's result
58
+ * text plus the pin and outcome of the attempt that produced it (and that pin's sha), or null
59
+ * to keep the raw label. Pure apart from the cursor it owns. */
60
+ export function resolveQueuedResult(end, mergeQueuedByRunner, outcomeByRunner, claimTop, joinStarts, floorTs) {
61
+ // Interlock relief (plans/interlock-relief.md): a held run's commit stayed on the branch and
62
+ // enqueued nothing, so its queued label names the held commit, not a merge this run made.
63
+ // Returning null keeps that row's raw queued label and, crucially, leaves the earlier change's
64
+ // merge pins unclaimed, so the verdict that belongs to the queued entry is not credited to the
65
+ // held run. One guard covers all three call sites (history-data.ts, time-spend.ts,
66
+ // run-detail-data.ts).
67
+ if (end.heldOnBranch === true)
68
+ return null;
69
+ if (floorTs !== null) {
70
+ const startTs = joinStarts.get(`${end.source}#${end.run}`);
71
+ if (startTs !== undefined && startTs < floorTs)
72
+ return null;
73
+ }
74
+ const queuedList = mergeQueuedByRunner.get(end.source);
75
+ if (!queuedList || queuedList.length === 0)
76
+ return null;
77
+ let top = claimTop.get(end.source) ?? queuedList.length - 1;
78
+ while (top >= 0 && queuedList[top].ts > end.ts)
79
+ top--;
80
+ if (top < 0)
81
+ return null;
82
+ claimTop.set(end.source, top - 1);
83
+ const outcomes = outcomeByRunner.get(end.source) ?? [];
84
+ // A recovery run's row: the attempt it re-enqueued, not the change's final verdict, so the
85
+ // final verdict shows once (on the authoring row) and each retriable failure once (here).
86
+ const recovered = recoveredAttemptVerdict(queuedList, top, outcomes);
87
+ if (recovered !== null)
88
+ return recovered;
89
+ // The authoring row: walk forward while the next pin is a recovery re-enqueue and resolve to
90
+ // the last attempt's verdict. `findLast` picks the newest same-sha outcome so an un-rebased
91
+ // retry (recovery re-enqueues the identical sha, logged by both pins) does not re-match the
92
+ // first attempt's outcome inside the recovery pin's walk. The returned pin/outcome ride the
93
+ // result so a caller correlating sibling evidence (the failure digest's review-failure
94
+ // message) can key on THIS attempt's position, not a sha a pre-review rebase may have moved
95
+ // (BUGS.md 2026-10-10).
96
+ let idx = top;
97
+ let resolved = null;
98
+ for (;;) {
99
+ const pin = queuedList[idx];
100
+ const outcome = outcomes.findLast((o) => o.ts >= pin.ts && textOr(o.commit, "") === textOr(pin.commit, ""));
101
+ // No outcome on the run's own pin, or on a recovery pin the walk reached: that attempt is
102
+ // still pending, so the change is waiting to merge — keep the raw label. The previous
103
+ // attempt's retriable verdict already shows on the recovery run's row.
104
+ if (!outcome)
105
+ return null;
106
+ const verdict = attemptVerdict(pin, outcome);
107
+ if (verdict === null)
108
+ return resolved;
109
+ // A merge or a rejection ends the change (the pin is discarded, so no recovery pin can
110
+ // follow); any other verdict kept the pin, and the walk continues only if the next pin is
111
+ // the recovery re-enqueue that kept it. Otherwise this attempt's verdict is the final one
112
+ // visible.
113
+ if (endsChange(verdict.result) || !isRecoveryPin(queuedList[idx + 1]))
114
+ return verdict;
115
+ resolved = verdict;
116
+ idx += 1;
117
+ }
118
+ }
119
+ /** The verdict one attempt's outcome carries, or null for a torn outcome with no result (a
120
+ * merge_failed whose result is missing or not text). A merge_done with no result is a merge. */
121
+ function attemptVerdict(pin, outcome) {
122
+ const result = textOr(outcome.result, outcome.type === "merge_done" ? "changed" : "");
123
+ return result ? { result, sha: textOr(pin.commit, ""), pin, outcome } : null;
124
+ }
125
+ /** True when a verdict ends its change: a merge or a rejection discards the pin, so no recovery
126
+ * re-enqueue can follow it. Every other verdict (the retriable ones) kept the pin. The one rule
127
+ * both directions of resolveQueuedResult's chain read — the authoring row's walk forward and a
128
+ * recovery row's step back — so the two cannot disagree on where a change ends. */
129
+ function endsChange(result) {
130
+ return result === "changed" || result === "rejected";
131
+ }
132
+ /** A recovery run's own row verdict: when the pin at `idx` is a recovery re-enqueue and the
133
+ * previous pin's attempt ended in a retriable verdict (the kept pin this run re-enqueued), that
134
+ * verdict, carrying the previous attempt's pin and outcome. The outcome is the newest one for
135
+ * the previous pin's sha logged between the two pins: recovery runs only after the previous
136
+ * attempt's outcome is written, and bounding at this pin keeps an un-rebased retry (both pins
137
+ * share a sha) from matching a LATER attempt's outcome. Null when the pin is not a recovery
138
+ * re-enqueue, or is one with no retriable attempt visible before it — a crash's unpinned or
139
+ * unfinished commit, or a previous pin outside the read — so the row takes the authoring rule. */
140
+ function recoveredAttemptVerdict(queuedList, idx, outcomes) {
141
+ const pin = queuedList[idx];
142
+ const prev = queuedList[idx - 1];
143
+ if (prev === undefined || !isRecoveryPin(pin))
144
+ return null;
145
+ const outcome = outcomes.findLast((o) => o.ts >= prev.ts && o.ts <= pin.ts && textOr(o.commit, "") === textOr(prev.commit, ""));
146
+ const verdict = outcome === undefined ? null : attemptVerdict(prev, outcome);
147
+ return verdict === null || endsChange(verdict.result) ? null : verdict;
148
+ }
149
+ /** True when a pin re-enqueues the previous attempt's kept pin rather than starting a fresh
150
+ * change. `recoverLeftover` (src/runner/leftover.ts) is the one `merge_queued` producer whose
151
+ * summary starts with this label, so a fresh change's pin after a conflict-cap discard ends
152
+ * `resolveQueuedResult`'s walk. */
153
+ function isRecoveryPin(pin) {
154
+ return pin !== undefined && textOr(pin.summary, "").startsWith("recovered leftover work from ");
155
+ }
@@ -1,5 +1,4 @@
1
- // vocabulary: plain
2
- /** Rendering half of `tumwater history`: format a `TickRow[]`/summary — collected by
1
+ /** Rendering half of `tumwater history`: format a `RunRow[]`/summary — collected by
3
2
  * history-data.ts — into the aligned table or `--summary` text `cmdHistory` (history.ts)
4
3
  * prints. The sibling observing view report does the same (src/report/report.ts prints
5
4
  * src/report/report-render.ts's Markdown), and this split keeps the same separation
@@ -10,16 +9,16 @@ import { formatTimestamp, humanSeconds } from "../text/datetime.js";
10
9
  import { roleLabel } from "../roles/role-labels.js";
11
10
  import { displayWidth, padToWidth } from "../text/text-width.js";
12
11
  import { compactTokens, usd } from "../text/format.js";
13
- import { tickRowCells, tickRowLine } from "./history-data.js";
12
+ import { runRowCells, runRowLine } from "./history-data.js";
14
13
  /** Each column's width from its unpadded cell. Text cells measure terminal display columns
15
- * (displayWidth), not UTF-16 code units: a loop name holding CJK or emoji renders two columns
14
+ * (displayWidth), not UTF-16 code units: a role name holding CJK or emoji renders two columns
16
15
  * per code point, and a code-unit padEnd lets that row's later columns drift right of its
17
- * ASCII neighbors. The tick cell measures before its "#" prefix is added (the prefix rides
16
+ * ASCII neighbors. The run cell measures before its "#" prefix is added (the prefix rides
18
17
  * outside the pad), and duration is ASCII, so both take plain length. */
19
18
  function widthsOf(cells) {
20
19
  return {
21
- loop: displayWidth(cells.loop),
22
- tick: cells.tick.length,
20
+ role: displayWidth(cells.role),
21
+ run: cells.run.length,
23
22
  result: displayWidth(cells.result),
24
23
  model: displayWidth(cells.model),
25
24
  duration: cells.duration.length,
@@ -29,11 +28,11 @@ function widthsOf(cells) {
29
28
  /** The per-column maxima over every row the table shows — the widths derive from the rows
30
29
  * actually shown, so a single-row table has no padding gap. */
31
30
  export function widestWidths(rows) {
32
- const widths = { loop: 0, tick: 0, result: 0, model: 0, duration: 0, usage: 0 };
31
+ const widths = { role: 0, run: 0, result: 0, model: 0, duration: 0, usage: 0 };
33
32
  for (const row of rows) {
34
- const w = widthsOf(tickRowCells(row));
35
- widths.loop = Math.max(widths.loop, w.loop);
36
- widths.tick = Math.max(widths.tick, w.tick);
33
+ const w = widthsOf(runRowCells(row));
34
+ widths.role = Math.max(widths.role, w.role);
35
+ widths.run = Math.max(widths.run, w.run);
37
36
  widths.result = Math.max(widths.result, w.result);
38
37
  widths.model = Math.max(widths.model, w.model);
39
38
  widths.duration = Math.max(widths.duration, w.duration);
@@ -41,22 +40,43 @@ export function widestWidths(rows) {
41
40
  }
42
41
  return widths;
43
42
  }
43
+ /** The column-wise maximum of two width sets: the running widths of a follow, which only grow. */
44
+ export function widenWidths(a, b) {
45
+ return {
46
+ role: Math.max(a.role, b.role),
47
+ run: Math.max(a.run, b.run),
48
+ result: Math.max(a.result, b.result),
49
+ model: Math.max(a.model, b.model),
50
+ duration: Math.max(a.duration, b.duration),
51
+ usage: Math.max(a.usage, b.usage),
52
+ };
53
+ }
44
54
  /** One aligned table line: fixed-width columns over the row set, then the free-text detail.
45
55
  * Every cell is padded to its column's width even when empty — usage is the one cell that can
46
- * be empty (a tick with neither tokens nor cost), and dropping it would pull the detail left,
47
- * misaligning that row against its neighbors; tickRowLine strips only the trailing pad. The
48
- * row's column order rides history-data.ts's tickRowLine, shared with the grep haystack so the
56
+ * be empty (a run with neither tokens nor cost), and dropping it would pull the detail left,
57
+ * misaligning that row against its neighbors; runRowLine strips only the trailing pad. The
58
+ * row's column order rides history-data.ts's runRowLine, shared with the grep haystack so the
49
59
  * two cannot disagree about which field sits where. */
50
60
  export function renderRow(row, widths) {
51
- return tickRowLine(row, (column, value) => padToWidth(value, widths[column]));
61
+ return runRowLine(row, (column, value) => padToWidth(value, widths[column]));
52
62
  }
53
- /** The `--summary` text: a header, one line per loop, then the total line. The loop column pads to
63
+ /** The `--summary` text: a header, one line per role, then the total line. The role column pads to
54
64
  * display width; each number column right-aligns under its header; the `time` column is the sum of
55
65
  * each role's run spans, `—` when none of the role's rows carried a span; the last column,
56
66
  * `last change`, is the newest change's timestamp (`—` when none) and is left-aligned, unpadded. */
57
67
  export function renderSummary(summary) {
58
- const all = [...summary.roles, summary.total];
59
- const loopWidth = Math.max(...["role", ...all.map((r) => roleLabel(r.role))].map(displayWidth));
68
+ return renderSummaryTable("role", [...summary.roles, summary.total].map((row) => ({ label: roleLabel(row.role), row })));
69
+ }
70
+ /** The `--summary --by model` text: renderSummary's table with one line per model (`(unknown)` for
71
+ * the null bucket), then the total line. */
72
+ export function renderModelSummary(summary) {
73
+ return renderSummaryTable("model", [
74
+ ...summary.models.map((row) => ({ label: row.model ?? "(unknown)", row })),
75
+ { label: "total", row: summary.total },
76
+ ]);
77
+ }
78
+ function renderSummaryTable(header, all) {
79
+ const roleWidth = Math.max(...[header, ...all.map((r) => r.label)].map(displayWidth));
60
80
  const cols = ["runs", "changes", "none", "refused", "problems", "tokens", "cost", "time"];
61
81
  const cell = (r, c) => c === "tokens"
62
82
  ? compactTokens(r.tokens)
@@ -67,10 +87,10 @@ export function renderSummary(summary) {
67
87
  ? "—"
68
88
  : humanSeconds(r.durationMs / 1000)
69
89
  : String(c === "runs" ? r.runs : r[c]);
70
- const widths = cols.map((c) => Math.max(c.length, ...all.map((r) => cell(r, c).length)));
71
- const line = (loop, cells, lastChange) => [padToWidth(loop, loopWidth), ...cells.map((c, i) => c.padStart(widths[i] ?? 0)), lastChange].join(" ").trimEnd();
90
+ const widths = cols.map((c) => Math.max(c.length, ...all.map((r) => cell(r.row, c).length)));
91
+ const line = (role, cells, lastChange) => [padToWidth(role, roleWidth), ...cells.map((c, i) => c.padStart(widths[i] ?? 0)), lastChange].join(" ").trimEnd();
72
92
  return [
73
- line("role", [...cols], "last change"),
74
- ...all.map((r) => line(roleLabel(r.role), cols.map((c) => cell(r, c)), r.lastChangeTs === null ? "—" : formatTimestamp(r.lastChangeTs))),
93
+ line(header, [...cols], "last change"),
94
+ ...all.map(({ label, row }) => line(label, cols.map((c) => cell(row, c)), row.lastChangeTs === null ? "—" : formatTimestamp(row.lastChangeTs))),
75
95
  ].join("\n");
76
96
  }
@@ -1,14 +1,30 @@
1
- // vocabulary: plain
2
- import { fail, say, sayJson } from "../cli/cli-output.js";
3
- import { durationLabel, failRivalShapes, flagValue, parseCountFlag, parseGrepFlag, parseRoleScope, parseSinceFlag } from "../cli/cli-args.js";
4
- import { HISTORY_DEFAULT_TICKS, HISTORY_MAX_TICKS, HISTORY_RESULT_FLAG_NAMES, historyClass, parseHistoryResultClass, readTickRows, readTickRowsSince, summarizeTickRows, tickRowsMatchingGrep } from "./history-data.js";
5
- import { renderRow, renderSummary, widestWidths } from "./history-render.js";
1
+ import { fail, say, sayJson, sayJsonLine } from "../cli/cli-output.js";
2
+ import { durationLabel, failRivalShapes, failUntilShorterThanSince, flagValue, parseCountFlag, parseGrepFlag, parseRoleListScope, parseSinceFlag, rejectFlagLookingValue } from "../cli/cli-args.js";
3
+ import { HISTORY_DEFAULT_RUNS, HISTORY_MAX_RUNS, HISTORY_RESULT_FLAG_NAMES, HISTORY_SUMMARY_GROUPS, historyClass, parseHistoryResultClass, readRunRows, readRunRowsSince, runRows, summarizeRunRows, summarizeRunRowsByModel, runRowsMatchingGrep, runRowsOnModels } from "./history-data.js";
4
+ import { renderModelSummary, renderRow, renderSummary, widenWidths, widestWidths } from "./history-render.js";
6
5
  import { LOGS_SINCE_MAX_MS, SPARSE_WINDOW_NOTE } from "../events/event-window.js";
7
- import { gotSuffix } from "../text/text.js";
6
+ import { followUntilParentGone } from "../cli/log-commands.js";
7
+ import { hasFollowFlag } from "../cli/cli-flag-specs.js";
8
+ import { parseEventLine, readEventsTailWithEnd } from "../events/event-read.js";
9
+ import { eventsLogPath } from "../paths.js";
10
+ import { gotSuffix, splitCommaList, splitCommaListUnique } from "../text/text.js";
11
+ import { trimToLast } from "../collections.js";
8
12
  import { typoSuffix } from "../text/suggest.js";
9
- /** `tumwater history [--role <id>] [-n N]`: one row per completed tick, newest first. The
13
+ /** Parse a `--model` value: a comma list of model selectors, deduplicated in first-seen order.
14
+ * A missing, empty or all-commas value fails with HISTORY_MODEL_VALUE_ERROR; selectors have no
15
+ * closed vocabulary, so none is rejected as unknown. Shared by the listing and the one-run
16
+ * trail's `--last --model`. */
17
+ export function parseHistoryModelList(raw) {
18
+ if (raw === undefined || raw === "")
19
+ fail(HISTORY_MODEL_VALUE_ERROR);
20
+ const pieces = splitCommaListUnique(raw);
21
+ if (pieces.length === 0)
22
+ fail(HISTORY_MODEL_VALUE_ERROR);
23
+ return pieces;
24
+ }
25
+ /** `tumwater history [--role <id>[,<id>...]] [-n N]`: one row per completed run, newest first. The
10
26
  * observing half beside cmdLogs (log-commands.ts): read-only over the event log, stdout only —
11
- * every datum rides the `run_end` events src/tick/tick-finalize.ts already writes, so this
27
+ * every datum rides the `run_end` events src/run/run-finalize.ts already writes, so this
12
28
  * is a rendering of the existing record, not a new one. The rows themselves are collected
13
29
  * by history-data.ts (beside src/report/report-data.ts and src/failure/failure-data.ts), and
14
30
  * the aligned table/summary text they print is rendered by history-render.ts (beside
@@ -23,14 +39,46 @@ export const HISTORY_GREP_VALUE_ERROR = "history --grep needs a pattern";
23
39
  * same wording. Its sibling of HISTORY_GREP_VALUE_ERROR: each names the command that printed
24
40
  * it. */
25
41
  export const HISTORY_RESULT_VALUE_ERROR = "history --result needs a value";
26
- /** `tumwater history [--role <id>] [-n N] [--since <duration>] [--grep <text>] [--result
27
- * <class>]`: print the last N completed ticks, newest first — or, with `--since`, the ticks of a
28
- * bounded past window over the same record, the same shape `logs --since` and `report --since`
29
- * speak. `--grep` keeps rows whose rendered line matches, and `--result` narrows to one outcome
30
- * class (changes/none/problems, the dashboard History filter's buckets); both compose with
31
- * --role, -n, and --since. Read-only: stdout only, no state file created — a missing or empty
42
+ /** The missing-value error cmdHistory prints for a valueless, empty or comma-only `--model`,
43
+ * exported so cli.ts's rejectUnknownArgs spec for --model fails a trailing `history --model`
44
+ * with the same wording. Its sibling of HISTORY_RESULT_VALUE_ERROR. */
45
+ export const HISTORY_MODEL_VALUE_ERROR = "history --model needs a model selector";
46
+ /** The missing-value error for a valueless or empty `--by`, exported for cli.ts's flag spec. */
47
+ export const HISTORY_BY_VALUE_ERROR = "history --by needs role or model";
48
+ /** How many trailing events `history -f` keeps: the seed read and the rolling buffer a finished
49
+ * run's start is paired from. */
50
+ const HISTORY_FOLLOW_BUFFER_EVENTS = 2000;
51
+ /** Parse a `--result` value: a comma list of outcome classes (the spellings
52
+ * HISTORY_RESULT_FLAG_NAMES prints, plus the hidden `changes` alias), deduplicated in first-seen
53
+ * order. A missing, empty or all-commas value fails with HISTORY_RESULT_VALUE_ERROR; the first
54
+ * unknown piece fails naming the accepted classes (gotSuffix plus a did-you-mean hint). Shared by
55
+ * the listing and the one-run trail's `--last --result`. */
56
+ export function parseHistoryResultList(raw) {
57
+ if (raw === undefined || raw === "")
58
+ fail(HISTORY_RESULT_VALUE_ERROR);
59
+ const pieces = splitCommaList(raw);
60
+ if (pieces.length === 0)
61
+ fail(HISTORY_RESULT_VALUE_ERROR);
62
+ const parsed = [];
63
+ for (const piece of pieces) {
64
+ const cls = parseHistoryResultClass(piece);
65
+ if (cls === null)
66
+ fail(`history --result must be one of ${Object.values(HISTORY_RESULT_FLAG_NAMES).join(", ")}${gotSuffix(piece)}${typoSuffix(piece, Object.values(HISTORY_RESULT_FLAG_NAMES))}`);
67
+ if (!parsed.includes(cls))
68
+ parsed.push(cls);
69
+ }
70
+ return parsed;
71
+ }
72
+ /** `tumwater history [--role <id>[,<id>...]] [-n N] [-f] [--since <duration>]
73
+ * [--until <duration>] [--grep <text>] [--result <class>] [--model <selector>[,...]]`: print the
74
+ * last N completed runs, newest first — or,
75
+ * with `--since`, the runs of a bounded past window over the same record, the same shape
76
+ * `logs --since` and `report --since` speak. `--grep` keeps rows whose rendered line matches,
77
+ * `--result` narrows to the listed outcome classes (changes/none/problems, the dashboard History
78
+ * filter's buckets) and `--model` keeps runs that started on the listed models; all compose
79
+ * with --role, -n, and --since. Read-only: stdout only, no state file created — a missing or empty
32
80
  * event log prints `no runs yet` and exits 0. */
33
- export async function cmdHistory(root, args) {
81
+ export async function cmdHistory(root, args, seams = {}) {
34
82
  // `--grep <text>` filters the row set case-insensitively against the WYSIWYG row line (plus a
35
83
  // `run_end` prefix, the same haystack rule logs --grep applies to its rendered lines), so
36
84
  // both observing views answer substring questions the same way. The flag scan itself — the
@@ -40,7 +88,7 @@ export async function cmdHistory(root, args) {
40
88
  // --grep, rest is args.
41
89
  const { rest, pattern: grepPattern } = parseGrepFlag(args, HISTORY_GREP_VALUE_ERROR);
42
90
  const nRaw = flagValue(rest, "-n");
43
- // `--since <duration>` is the window-shaped view over the same tick record the -n view
91
+ // `--since <duration>` is the window-shaped view over the same run record the -n view
44
92
  // dumps: a rival shape to -n, so they fail together naming both (the same rule and wording
45
93
  // shape logs uses). It reuses the logs --since cap (same log, same cap — no third cap
46
94
  // value) through parseSinceFlag, so the error wordings cannot drift. `--role` composes with
@@ -48,66 +96,108 @@ export async function cmdHistory(root, args) {
48
96
  // against.
49
97
  let rows;
50
98
  let covered = true;
99
+ const follow = hasFollowFlag(rest);
100
+ if (follow) {
101
+ if (rest.includes("--since"))
102
+ failRivalShapes("history --since", "-f/--follow", 'follow means "from now on"; --since shows a bounded past window');
103
+ if (rest.includes("--summary"))
104
+ failRivalShapes("history --summary", "-f/--follow", "a summary tallies a bounded set; -f follows from now on");
105
+ }
106
+ const followTail = follow ? readEventsTailWithEnd(root, HISTORY_FOLLOW_BUFFER_EVENTS) : null;
51
107
  const sinceMs = parseSinceFlag(rest, "history --since", LOGS_SINCE_MAX_MS);
52
- if (sinceMs !== null) {
108
+ const untilMs = parseSinceFlag(rest, "history --until", LOGS_SINCE_MAX_MS, "--until");
109
+ failUntilShorterThanSince("history", untilMs, sinceMs);
110
+ if (followTail !== null) {
111
+ const limit = nRaw !== null ? parseCountFlag("-n", nRaw, HISTORY_MAX_RUNS) : HISTORY_DEFAULT_RUNS;
112
+ // The seed goes through readRunRows, which widens its window until it finds `limit` rows;
113
+ // the tail buffer only feeds the live follow.
114
+ rows = readRunRows(root, limit, parseRoleListScope(root, rest));
115
+ }
116
+ else if (sinceMs !== null) {
53
117
  if (nRaw !== null)
54
118
  failRivalShapes("history --since", "-n", "a count and a window are rival shapes");
55
- const role = parseRoleScope(root, rest);
56
- const windowed = readTickRowsSince(root, sinceMs, role);
119
+ const role = parseRoleListScope(root, rest);
120
+ const windowed = readRunRowsSince(root, sinceMs, role, untilMs);
57
121
  rows = windowed.rows;
58
122
  covered = windowed.covered;
59
123
  }
60
124
  else {
61
125
  // parseCountFlag's max caps the scan window: a typo'd "999999" must not build a
62
- // HISTORY_MAX_TICKS-sized slice scan from a huge limit.
63
- const limit = nRaw !== null ? parseCountFlag("-n", nRaw, HISTORY_MAX_TICKS) : HISTORY_DEFAULT_TICKS;
126
+ // HISTORY_MAX_RUNS-sized slice scan from a huge limit.
127
+ const limit = nRaw !== null ? parseCountFlag("-n", nRaw, HISTORY_MAX_RUNS) : HISTORY_DEFAULT_RUNS;
64
128
  // The config is read (through loadConfigCached, never throwing) only when --role is present:
65
129
  // a read-only view must not refuse a transiently broken tumwater.json.
66
- const role = parseRoleScope(root, rest);
67
- rows = readTickRows(root, limit, role);
130
+ const role = parseRoleListScope(root, rest);
131
+ rows = readRunRows(root, limit, role);
68
132
  }
69
133
  // The filter runs on the collected rows, before rendering and before --json serialization:
70
134
  // the same rows the table would print, filtered the same way in both forms (the logs --grep
71
135
  // rule: -n bounds the scanned window, not the printed rows).
72
136
  if (grepPattern !== null)
73
- rows = tickRowsMatchingGrep(rows, grepPattern);
137
+ rows = runRowsMatchingGrep(rows, grepPattern);
74
138
  // The set --grep admitted, saved so the empty-state wording blames the filter that actually
75
139
  // emptied it: if the result filter below removes rows grep matched, naming the pattern would
76
140
  // be false (`no runs matching "widget"` when feature #7 does match). The GUI draws the same
77
141
  // line between a search and its outcome filter.
78
142
  const afterGrep = rows.length;
79
- // `--result <class>` narrows to one outcome bucket, reading the same canonical historyClass
80
- // rule the dashboard's History filter applies (shared from history-data.ts, not copied). It
81
- // composes with --role, -n, --since, and --grep, so no rival-shape rule: a missing or empty
82
- // value fails with its own wording, and an unknown class names the accepted ones (the gotSuffix
83
- // shape the other flag validators use, plus a did-you-mean hint for a near-miss of a real
84
- // result — the same typoSuffix treatment `logs --type` gives its codes). Applied after --grep,
85
- // over the same rows both the table
143
+ // `--result <class>[,<class>...]` narrows to the listed outcome buckets, reading the same
144
+ // canonical historyClass rule the dashboard's History filter applies (shared from
145
+ // history-data.ts, not copied). It composes with --role, -n, --since, and --grep, so no
146
+ // rival-shape rule: a missing or empty value fails with its own wording, and an unknown
147
+ // class names the accepted ones (the gotSuffix shape the other flag validators use, plus a
148
+ // did-you-mean hint for a near-miss of a real result — the same typoSuffix treatment
149
+ // `logs --type` gives its codes). Applied after --grep, over the same rows both the table
86
150
  // and --json serialize.
87
- const resultRaw = flagValue(rest, "--result");
88
- let resultClass = null;
151
+ const resultRaw = rejectFlagLookingValue("history --result", flagValue(rest, "--result"));
152
+ let resultClasses = null;
89
153
  if (resultRaw !== null) {
90
- if (resultRaw === undefined || resultRaw === "")
91
- fail(HISTORY_RESULT_VALUE_ERROR);
92
- resultClass = parseHistoryResultClass(resultRaw);
93
- if (resultClass === null)
94
- fail(`history --result must be one of ${Object.values(HISTORY_RESULT_FLAG_NAMES).join(", ")}${gotSuffix(resultRaw)}${typoSuffix(resultRaw, Object.values(HISTORY_RESULT_FLAG_NAMES))}`);
95
- rows = rows.filter((r) => historyClass(r.result) === resultClass);
154
+ const wanted = parseHistoryResultList(resultRaw);
155
+ resultClasses = wanted;
156
+ rows = rows.filter((r) => wanted.includes(historyClass(r.result)));
157
+ }
158
+ // `--model <selector>[,...]` keeps the rows whose run_start named exactly one of the listed
159
+ // selectors (case-sensitive, the strings `--summary --by model` prints); a null-model row never
160
+ // matches. An unknown selector is not an error: models have no closed vocabulary and an old log
161
+ // holds models the config has since dropped, so it simply keeps no rows.
162
+ const afterResult = rows.length;
163
+ const modelRaw = rejectFlagLookingValue("history --model", flagValue(rest, "--model"));
164
+ let models = null;
165
+ if (modelRaw !== null) {
166
+ const pieces = parseHistoryModelList(modelRaw);
167
+ models = pieces;
168
+ rows = runRowsOnModels(rows, pieces);
169
+ }
170
+ if (followTail !== null) {
171
+ await followHistory(root, rest, rows, followTail, (r) => rowKept(r, grepPattern, resultClasses, models), seams, rest.includes("--json"));
172
+ return;
96
173
  }
97
174
  // --json swaps the renderer for the collector's own payload, exactly as status --json and
98
175
  // report --json: the same rows the table prints, each with ts/tokens/costUsd kept raw. A
99
176
  // JSON document even when the log is empty ({"rows":[]} — never the prose `no runs yet`,
100
177
  // the report --json precedent: the flag's output must be parseable in every exit-0 case).
178
+ // `--by role|model` picks how --summary groups its lines (role by default).
179
+ const byRaw = flagValue(rest, "--by");
180
+ let byModel = false;
181
+ if (rest.includes("--by")) {
182
+ if (!byRaw)
183
+ return fail(HISTORY_BY_VALUE_ERROR);
184
+ if (!HISTORY_SUMMARY_GROUPS.includes(byRaw)) {
185
+ fail(`history --by must be one of ${HISTORY_SUMMARY_GROUPS.join(", ")}${gotSuffix(byRaw)}${typoSuffix(byRaw, [...HISTORY_SUMMARY_GROUPS])}`);
186
+ }
187
+ if (!rest.includes("--summary"))
188
+ fail("history --by needs --summary (it picks how the summary groups runs)");
189
+ byModel = byRaw === "model";
190
+ }
101
191
  const summary = rest.includes("--summary") && rows.length > 0;
102
192
  if (rest.includes("--json")) {
103
- sayJson(summary ? summarizeTickRows(rows) : { rows });
193
+ sayJson(summary ? (byModel ? summarizeRunRowsByModel(rows) : summarizeRunRows(rows)) : { rows });
104
194
  return;
105
195
  }
106
196
  if (rows.length === 0) {
107
197
  // A grep that matched nothing must not borrow the empty-log prose: `no runs yet` and
108
198
  // `no runs in 2h` are statements about the LOG and the WINDOW, and both are false when
109
199
  // rows were scanned and filtered out — the operator would read a quiet team where the
110
- // truth is `the ticks are there, none match`. The logs --grep rule (its `no events
200
+ // truth is `the runs are there, none match`. The logs --grep rule (its `no events
111
201
  // matching "<pattern>"`, src/cli/log-commands.ts) names the pattern instead; --json stays
112
202
  // a bare {"rows":[]} document above, never prose in either form.
113
203
  //
@@ -115,17 +205,21 @@ export async function cmdHistory(root, args) {
115
205
  // empty log or window scanned no rows, so the result filter removed nothing and the
116
206
  // empty-log/window prose is the true one — the dashboard's History view draws the same
117
207
  // line. A grep that admitted nothing keeps its own wording regardless of --result.
118
- say(resultClass !== null && afterGrep > 0
119
- ? `no runs with result "${resultClass}"`
120
- : grepPattern !== null
121
- ? `no runs matching "${grepPattern}"`
122
- : sinceMs === null
123
- ? "no runs yet"
124
- : `no runs in ${durationLabel(sinceMs)}`);
208
+ say(models !== null && afterResult > 0
209
+ ? `no runs on model "${models.join(",")}"`
210
+ : resultClasses !== null && afterGrep > 0
211
+ ? `no runs with result "${resultClasses.join(",")}"`
212
+ : grepPattern !== null
213
+ ? `no runs matching "${grepPattern}"`
214
+ : sinceMs === null
215
+ ? "no runs yet"
216
+ : untilMs === null
217
+ ? `no runs in ${durationLabel(sinceMs)}`
218
+ : `no runs between ${durationLabel(sinceMs)} and ${durationLabel(untilMs)} ago`);
125
219
  return;
126
220
  }
127
221
  if (summary)
128
- say(renderSummary(summarizeTickRows(rows)));
222
+ say(byModel ? renderModelSummary(summarizeRunRowsByModel(rows)) : renderSummary(summarizeRunRows(rows)));
129
223
  else {
130
224
  const widths = widestWidths(rows);
131
225
  say(rows.map((r) => renderRow(r, widths)).join("\n"));
@@ -137,3 +231,50 @@ export async function cmdHistory(root, args) {
137
231
  if (!covered)
138
232
  say(SPARSE_WINDOW_NOTE);
139
233
  }
234
+ /** The row filters `--grep`, `--result` and `--model` apply, as one predicate for followed rows. */
235
+ function rowKept(row, grep, classes, models) {
236
+ if (grep !== null && runRowsMatchingGrep([row], grep).length === 0)
237
+ return false;
238
+ if (classes !== null && !classes.includes(historyClass(row.result)))
239
+ return false;
240
+ return models === null || runRowsOnModels([row], models).length > 0;
241
+ }
242
+ /** `history -f`: print the seed rows oldest first, then one row per `run_end` as it lands. A run
243
+ * whose change waits to merge prints `queued` and no second line when the merge resolves. With
244
+ * `json`, each row is one `sayJsonLine` object (the `history --json` row shape), nothing else. */
245
+ async function followHistory(root, rest, seed, tail, keep, seams, json) {
246
+ const ordered = [...seed].reverse();
247
+ let widths = widestWidths(ordered);
248
+ if (json)
249
+ for (const r of ordered)
250
+ sayJsonLine(r);
251
+ else if (ordered.length > 0)
252
+ say(ordered.map((r) => renderRow(r, widths)).join("\n"));
253
+ const seen = new Set(seed.map((r) => `${r.role}#${r.run}`));
254
+ const buffer = [...tail.events];
255
+ const role = parseRoleListScope(root, rest);
256
+ await followUntilParentGone(eventsLogPath(root), tail.coveredEnd, (lines) => {
257
+ for (const line of lines.filter(Boolean)) {
258
+ const e = parseEventLine(line);
259
+ if (!e)
260
+ continue;
261
+ buffer.push(e);
262
+ trimToLast(buffer, HISTORY_FOLLOW_BUFFER_EVENTS);
263
+ if (e.type !== "run_end")
264
+ continue;
265
+ const row = runRows(buffer, 1, role)[0];
266
+ if (!row)
267
+ continue;
268
+ const key = `${row.role}#${row.run}`;
269
+ if (seen.has(key) || !keep(row))
270
+ continue;
271
+ seen.add(key);
272
+ if (json) {
273
+ sayJsonLine(row);
274
+ continue;
275
+ }
276
+ widths = widenWidths(widths, widestWidths([row]));
277
+ say(renderRow(row, widths));
278
+ }
279
+ }, seams);
280
+ }