tumwater 0.4.2 → 0.4.4

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 (354) hide show
  1. package/README.md +6 -6
  2. package/dist/build-info.json +2 -2
  3. package/dist/src/agent/agent-backend.js +6 -6
  4. package/dist/src/agent/{run-agent.js → agent-run.js} +27 -13
  5. package/dist/src/agent/{run-settle.js → agent-settle.js} +48 -24
  6. package/dist/src/agent/{run-spawn.js → agent-spawn.js} +21 -12
  7. package/dist/src/agent/{run-stream.js → agent-stream.js} +4 -4
  8. package/dist/src/agent/backend-traits.js +2 -2
  9. package/dist/src/agent/claude/claude-account.js +6 -27
  10. package/dist/src/agent/claude/claude-backend.js +3 -3
  11. package/dist/src/agent/claude/claude-env.js +27 -31
  12. package/dist/src/agent/claude/claude-stream.js +111 -54
  13. package/dist/src/agent/codex/codex-account.js +8 -0
  14. package/dist/src/agent/codex/codex-args.js +5 -5
  15. package/dist/src/agent/codex/codex-backend.js +2 -2
  16. package/dist/src/agent/codex/codex-env.js +28 -15
  17. package/dist/src/agent/codex/codex-hook-trust.js +39 -20
  18. package/dist/src/agent/codex/codex-rollout.js +93 -56
  19. package/dist/src/agent/codex/codex-stream.js +1 -1
  20. package/dist/src/agent/env-scrub.js +18 -0
  21. package/dist/src/agent/image-input.js +68 -11
  22. package/dist/src/agent/notes-shim.js +2 -2
  23. package/dist/src/agent/opencode/opencode-backend.js +26 -24
  24. package/dist/src/agent/opencode/opencode-catalog.js +85 -36
  25. package/dist/src/agent/opencode/opencode-config.js +32 -14
  26. package/dist/src/agent/opencode/opencode-env.js +12 -13
  27. package/dist/src/agent/opencode/opencode-stream.js +47 -25
  28. package/dist/src/agent/session-ledger.js +23 -27
  29. package/dist/src/agent/subscription-account.js +2 -83
  30. package/dist/src/backlog/backlog-archive.js +3 -4
  31. package/dist/src/backlog/backlog-cli-view.js +81 -41
  32. package/dist/src/backlog/backlog-cli.js +57 -26
  33. package/dist/src/backlog/backlog-eligibility.js +56 -122
  34. package/dist/src/backlog/backlog-md.js +5 -5
  35. package/dist/src/backlog/backlog-parts.js +65 -27
  36. package/dist/src/backlog/{question-answer.js → backlog-question-answer.js} +1 -1
  37. package/dist/src/backlog/backlog-release.js +12 -12
  38. package/dist/src/backlog/backlog-structure.js +114 -61
  39. package/dist/src/backlog/backlog.js +3 -2
  40. package/dist/src/baseline/main-baseline.js +148 -84
  41. package/dist/src/baseline/main-guardrails.js +2 -2
  42. package/dist/src/baseline/main-watch.js +84 -50
  43. package/dist/src/brief/brief.js +26 -13
  44. package/dist/src/budget/budget-fallback-view.js +100 -243
  45. package/dist/src/budget/budget-wiring.js +29 -65
  46. package/dist/src/budget/budget.js +4 -5
  47. package/dist/src/budget/fallback-breaker.js +2 -2
  48. package/dist/src/build/build-check-detect.js +43 -14
  49. package/dist/src/build/build-check-events.js +2 -2
  50. package/dist/src/build/build-check-report.js +10 -0
  51. package/dist/src/build/build-check-scoped.js +40 -18
  52. package/dist/src/build/build-check.js +13 -5
  53. package/dist/src/build/build-stage.js +6 -4
  54. package/dist/src/change/change-data.js +1 -4
  55. package/dist/src/cli/cli-command-args.js +89 -52
  56. package/dist/src/cli/cli-completion-command.js +75 -0
  57. package/dist/src/cli/{config-commands.js → cli-config-commands.js} +82 -71
  58. package/dist/src/cli/cli-doctor-command.js +103 -0
  59. package/dist/src/cli/cli-flag-specs.js +42 -22
  60. package/dist/src/cli/cli-init.js +36 -14
  61. package/dist/src/cli/{log-commands.js → cli-log-commands.js} +43 -23
  62. package/dist/src/cli/{notify-command.js → cli-notify-command.js} +2 -2
  63. package/dist/src/cli/cli-query-commands.js +2 -2
  64. package/dist/src/cli/{question-commands.js → cli-question-commands.js} +86 -60
  65. package/dist/src/cli/cli-repo-gate.js +2 -2
  66. package/dist/src/cli/cli-role-command.js +2 -41
  67. package/dist/src/cli/cli-start-summary.js +4 -3
  68. package/dist/src/cli/cli-start-supervisor.js +190 -0
  69. package/dist/src/cli/cli-start.js +94 -244
  70. package/dist/src/cli/completion.js +85 -49
  71. package/dist/src/cli/follow-view.js +20 -2
  72. package/dist/src/cli/help.js +6 -13
  73. package/dist/src/cli.js +70 -176
  74. package/dist/src/collections.js +39 -27
  75. package/dist/src/concurrency/check-slots.js +70 -0
  76. package/dist/src/concurrency/lock.js +23 -1
  77. package/dist/src/concurrency/run-permit.js +49 -0
  78. package/dist/src/concurrency/semaphore.js +46 -250
  79. package/dist/src/config/config-editable-keys.js +4 -7
  80. package/dist/src/config/config-fallbacks.js +81 -101
  81. package/dist/src/config/config-field-checks.js +66 -37
  82. package/dist/src/config/config-keys.js +66 -40
  83. package/dist/src/config/config-live.js +4 -28
  84. package/dist/src/config/{model-selector.js → config-model-selector.js} +4 -4
  85. package/dist/src/config/config-request.js +163 -102
  86. package/dist/src/config/config-schema.js +10 -12
  87. package/dist/src/config/config-selector-checks.js +1 -1
  88. package/dist/src/config/config-validation-agents.js +2 -2
  89. package/dist/src/config/config-validation-roles.js +8 -72
  90. package/dist/src/config/config-validation.js +4 -39
  91. package/dist/src/config/config-views.js +26 -50
  92. package/dist/src/config/config-write.js +128 -102
  93. package/dist/src/config/config.js +5 -14
  94. package/dist/src/doctor/doctor-backlog-bugs.js +7 -6
  95. package/dist/src/doctor/doctor-backlog-plans.js +53 -120
  96. package/dist/src/doctor/doctor-backlog.js +16 -14
  97. package/dist/src/doctor/doctor-check-names.js +3 -6
  98. package/dist/src/doctor/doctor-checks.js +8 -8
  99. package/dist/src/doctor/doctor-claude-checks.js +70 -36
  100. package/dist/src/doctor/doctor-codex-checks.js +6 -6
  101. package/dist/src/doctor/doctor-contention-checks.js +57 -117
  102. package/dist/src/doctor/doctor-fallback-checks.js +48 -26
  103. package/dist/src/doctor/doctor-holds.js +8 -47
  104. package/dist/src/doctor/doctor-install.js +1 -1
  105. package/dist/src/doctor/doctor-launch-services.js +1 -1
  106. package/dist/src/doctor/doctor-model-checks.js +178 -94
  107. package/dist/src/doctor/doctor-notify.js +2 -2
  108. package/dist/src/doctor/doctor-opencode-checks.js +5 -4
  109. package/dist/src/doctor/doctor-orphans.js +122 -21
  110. package/dist/src/doctor/{outcome.js → doctor-outcome.js} +7 -0
  111. package/dist/src/doctor/doctor-pi-checks.js +2 -2
  112. package/dist/src/doctor/doctor-principles.js +3 -3
  113. package/dist/src/doctor/doctor-project-checks.js +24 -11
  114. package/dist/src/doctor/doctor-role-notes.js +1 -1
  115. package/dist/src/doctor/doctor.js +8 -11
  116. package/dist/src/events/event-format.js +44 -64
  117. package/dist/src/events/event-labels.js +1 -5
  118. package/dist/src/events/event-legacy.js +3 -5
  119. package/dist/src/events/{notify.js → event-notify.js} +6 -6
  120. package/dist/src/events/event-read.js +1 -1
  121. package/dist/src/events/event-window.js +1 -1
  122. package/dist/src/events/events.js +8 -0
  123. package/dist/src/failure/failure-cluster.js +9 -18
  124. package/dist/src/failure/failure-data.js +12 -8
  125. package/dist/src/failure/failure-render.js +46 -36
  126. package/dist/src/failure/failure-state-change.js +54 -120
  127. package/dist/src/failure/{time-spend.js → failure-time-spend.js} +117 -84
  128. package/dist/src/files/files.js +30 -0
  129. package/dist/src/files/json-object.js +8 -8
  130. package/dist/src/files/stat-cache.js +12 -8
  131. package/dist/src/gates/budget-gates.js +85 -84
  132. package/dist/src/gates/gate-polls.js +32 -74
  133. package/dist/src/gates/pause-gates.js +16 -2
  134. package/dist/src/gates/readiness.js +1 -1
  135. package/dist/src/gates/role-cap-gates.js +44 -35
  136. package/dist/src/gates/{startup-gate.js → startup-gates.js} +9 -8
  137. package/dist/src/gates/{streak-gate.js → streak-gates.js} +75 -52
  138. package/dist/src/git/git-diff.js +6 -2
  139. package/dist/src/git/git.js +75 -45
  140. package/dist/src/git/worktree-pool.js +3 -6
  141. package/dist/src/gui/gui-admin-endpoints.js +3 -3
  142. package/dist/src/gui/gui-args.js +12 -0
  143. package/dist/src/gui/gui-backlog-commands.js +4 -4
  144. package/dist/src/gui/gui-backlog-endpoints.js +61 -41
  145. package/dist/src/gui/gui-command.js +43 -24
  146. package/dist/src/gui/gui-config-commands.js +2 -64
  147. package/dist/src/gui/gui-endpoint-commands.js +1 -1
  148. package/dist/src/gui/gui-endpoint-common.js +19 -1
  149. package/dist/src/gui/gui-history-endpoints.js +100 -52
  150. package/dist/src/gui/gui-operator-commands.js +43 -48
  151. package/dist/src/gui/gui-prompt-commands.js +25 -25
  152. package/dist/src/gui/gui-report-endpoints.js +1 -1
  153. package/dist/src/gui/gui-server.js +1 -2
  154. package/dist/src/gui/gui-team-commands.js +2 -1
  155. package/dist/src/history/history-data.js +71 -44
  156. package/dist/src/history/history-queued.js +49 -27
  157. package/dist/src/history/history.js +54 -36
  158. package/dist/src/inbox/prompt-commands.js +59 -42
  159. package/dist/src/init/init.js +88 -40
  160. package/dist/src/merge/merge-backlog-conflicts.js +1 -1
  161. package/dist/src/merge/merge-check-failures.js +12 -9
  162. package/dist/src/merge/merge-core.js +7 -32
  163. package/dist/src/merge/merge-drain.js +11 -13
  164. package/dist/src/merge/merge-pipeline.js +6 -7
  165. package/dist/src/merge/merge-post-review-drain.js +11 -14
  166. package/dist/src/merge/merge-queue-render.js +3 -3
  167. package/dist/src/merge/merge-requeue.js +1 -1
  168. package/dist/src/merge/merge-slot.js +0 -8
  169. package/dist/src/merge/merge-stack.js +17 -18
  170. package/dist/src/merge/merge-to-main.js +13 -58
  171. package/dist/src/merge/merge-verify.js +12 -21
  172. package/dist/src/notes-cli.js +52 -42
  173. package/dist/src/operator/operator-commands.js +80 -47
  174. package/dist/src/operator/operator-intent.js +3 -3
  175. package/dist/src/orchestrator/orchestrator-launch.js +31 -63
  176. package/dist/src/orchestrator/orchestrator-poll.js +44 -69
  177. package/dist/src/orchestrator/orchestrator-scheduling.js +5 -9
  178. package/dist/src/orchestrator/orchestrator.js +8 -21
  179. package/dist/src/orchestrator/scheduling-admit.js +101 -68
  180. package/dist/src/orchestrator/scheduling-scope.js +18 -27
  181. package/dist/src/orchestrator/scheduling-skip.js +10 -14
  182. package/dist/src/orchestrator/startup-migrations.js +1 -1
  183. package/dist/src/paths.js +10 -9
  184. package/dist/src/pi/pi-args.js +5 -2
  185. package/dist/src/pi/pi-backend.js +2 -2
  186. package/dist/src/pi/pi-bin.js +3 -3
  187. package/dist/src/pi/pi-event-line.js +28 -1
  188. package/dist/src/pi/pi-listed-models.js +13 -0
  189. package/dist/src/pi/pi-models.js +43 -29
  190. package/dist/src/pi/pi-settings.js +43 -20
  191. package/dist/src/pi/pi-stream.js +41 -8
  192. package/dist/src/pi/pi-watchdogs.js +43 -15
  193. package/dist/src/pi/pi.js +2 -2
  194. package/dist/src/pi-extension/context-shake.js +32 -20
  195. package/dist/src/pi-extension/kill-guard.js +69 -0
  196. package/dist/src/process/process-group.js +51 -26
  197. package/dist/src/process/process.js +20 -12
  198. package/dist/src/process/run-marker.js +33 -15
  199. package/dist/src/process/supervisor.js +40 -28
  200. package/dist/src/prompt/dialect.js +1 -1
  201. package/dist/src/prompt/gate-notes.js +2 -2
  202. package/dist/src/prompt/gate-prompts.js +1 -44
  203. package/dist/src/prompt/notes-backlog.js +66 -37
  204. package/dist/src/prompt/principles.js +6 -4
  205. package/dist/src/prompt/prompt-followup.js +12 -4
  206. package/dist/src/prompt/prompt.js +88 -38
  207. package/dist/src/redeploy/redeployer.js +21 -1
  208. package/dist/src/report/report-data.js +53 -37
  209. package/dist/src/report/report-fold.js +16 -9
  210. package/dist/src/report/report-render.js +21 -194
  211. package/dist/src/report/report-since-data.js +4 -3
  212. package/dist/src/report/report-since-render.js +173 -0
  213. package/dist/src/report/report-usage-fold.js +8 -8
  214. package/dist/src/report/report.js +46 -22
  215. package/dist/src/review/diff-rules.js +35 -21
  216. package/dist/src/review/post-merge-review-render.js +9 -3
  217. package/dist/src/review/post-merge-review.js +48 -41
  218. package/dist/src/review/review-precheck.js +14 -4
  219. package/dist/src/review/review.js +48 -170
  220. package/dist/src/review/secret-leaks.js +26 -16
  221. package/dist/src/roles/legacy-role-migration.js +6 -3
  222. package/dist/src/roles/role-catalog.js +11 -3
  223. package/dist/src/roles/role-guidance.js +16 -12
  224. package/dist/src/roles/role-render.js +7 -46
  225. package/dist/src/roles/role-view.js +10 -33
  226. package/dist/src/run/acceptance-greps.js +88 -45
  227. package/dist/src/run/ci-parity.js +19 -8
  228. package/dist/src/run/run-apply.js +12 -15
  229. package/dist/src/run/run-detail-data.js +121 -68
  230. package/dist/src/run/run-detail-render.js +92 -35
  231. package/dist/src/run/run-detail.js +72 -41
  232. package/dist/src/run/run-prompt.js +63 -50
  233. package/dist/src/run/run-resume.js +76 -29
  234. package/dist/src/run/run-stage.js +141 -90
  235. package/dist/src/run/run-timing.js +8 -2
  236. package/dist/src/run/run-usage.js +31 -1
  237. package/dist/src/run/run-verdict.js +128 -50
  238. package/dist/src/run/stage-check.js +60 -50
  239. package/dist/src/run/stage-checks-shared.js +28 -0
  240. package/dist/src/run/stage-name-checks.js +111 -92
  241. package/dist/src/run/stage-path-checks.js +69 -42
  242. package/dist/src/run/stage-prose-checks.js +55 -40
  243. package/dist/src/runner/{leftover.js → runner-leftover.js} +9 -26
  244. package/dist/src/runner/runner-outcome.js +7 -2
  245. package/dist/src/runner/runner-pi.js +9 -22
  246. package/dist/src/runner/runner-recovery.js +5 -13
  247. package/dist/src/runner/{revision.js → runner-revision.js} +1 -2
  248. package/dist/src/runner/runner-run.js +103 -50
  249. package/dist/src/runner/runner-state-load.js +18 -28
  250. package/dist/src/runner/runner-state.js +0 -14
  251. package/dist/src/runner/runner.js +41 -25
  252. package/dist/src/scheduling/backoff.js +8 -3
  253. package/dist/src/scheduling/claim-entries.js +2 -1
  254. package/dist/src/scheduling/claims.js +0 -4
  255. package/dist/src/scheduling/scheduling.js +53 -66
  256. package/dist/src/status/status-budget.js +4 -43
  257. package/dist/src/status/status-data.js +35 -39
  258. package/dist/src/status/status-role-notes.js +2 -2
  259. package/dist/src/status/status-roles.js +9 -18
  260. package/dist/src/team/{error-storm.js → team-error-storm.js} +1 -1
  261. package/dist/src/team/{failure-spread.js → team-failure-spread.js} +5 -5
  262. package/dist/src/team/team-hold.js +105 -70
  263. package/dist/src/team/{orchestrator-info.js → team-orchestrator-info.js} +55 -36
  264. package/dist/src/team/team-polls.js +53 -201
  265. package/dist/src/team/{reclaim.js → team-reclaim.js} +61 -31
  266. package/dist/src/team/team-state.js +4 -13
  267. package/dist/src/text/datetime.js +16 -7
  268. package/dist/src/text/markdown.js +1 -1
  269. package/dist/src/text/phrases.js +29 -15
  270. package/dist/src/text/suggest.js +1 -1
  271. package/dist/src/text/text.js +6 -5
  272. package/dist/src/ui/alert-feeds.js +46 -111
  273. package/dist/src/ui/badges.js +17 -88
  274. package/dist/src/ui/gui/gui-client-boot.js +2 -8
  275. package/dist/src/ui/gui/gui-client-composer.js +1 -1
  276. package/dist/src/ui/gui/gui-client-drawer-config.js +8 -33
  277. package/dist/src/ui/gui/gui-client-drawer.js +1 -1
  278. package/dist/src/ui/gui/gui-client-feed.js +1 -1
  279. package/dist/src/ui/gui/gui-client-history.js +28 -14
  280. package/dist/src/ui/gui/gui-client-model.js +0 -12
  281. package/dist/src/ui/gui/gui-client-operator.js +7 -5
  282. package/dist/src/ui/gui/gui-client-pending.js +7 -6
  283. package/dist/src/ui/gui/gui-client-report-chart.js +5 -5
  284. package/dist/src/ui/gui/gui-client-report.js +1 -1
  285. package/dist/src/ui/gui/gui-client-roles.js +0 -1
  286. package/dist/src/ui/gui/gui-client-settings.js +43 -84
  287. package/dist/src/ui/gui/gui-client-team.js +12 -6
  288. package/dist/src/ui/gui/gui-client.js +7 -1
  289. package/dist/src/ui/progress-data.js +107 -44
  290. package/dist/src/ui/run-progress-model.js +20 -4
  291. package/dist/src/ui/status-model.js +2 -24
  292. package/dist/src/ui/status-payload.js +11 -30
  293. package/dist/src/ui/status-render.js +13 -24
  294. package/dist/src/ui/team-alert-backlog.js +14 -204
  295. package/dist/src/ui/team-alert-shared.js +6 -29
  296. package/dist/src/ui/team-alert-status.js +9 -170
  297. package/dist/src/ui/team-alert-tail.js +19 -215
  298. package/dist/src/ui/team-alerts.js +8 -15
  299. package/dist/src/ui/transcript-run.js +27 -16
  300. package/dist/src/ui/tui/tui-backlog.js +2 -2
  301. package/dist/src/ui/tui/tui-editor-answer.js +12 -10
  302. package/dist/src/ui/tui/tui-editor-attach.js +4 -3
  303. package/dist/src/ui/tui/tui-editor-backends.js +4 -3
  304. package/dist/src/ui/tui/tui-editor-brief.js +6 -9
  305. package/dist/src/ui/tui/tui-editor-budget.js +4 -3
  306. package/dist/src/ui/tui/tui-editor-config.js +80 -68
  307. package/dist/src/ui/tui/tui-editor-entry.js +12 -20
  308. package/dist/src/ui/tui/tui-editor-pause.js +51 -57
  309. package/dist/src/ui/tui/tui-editor-prompt.js +9 -13
  310. package/dist/src/ui/tui/tui-editor-queued.js +11 -14
  311. package/dist/src/ui/tui/tui-editor-quiet.js +4 -3
  312. package/dist/src/ui/tui/tui-editor-release.js +4 -3
  313. package/dist/src/ui/tui/tui-editor-retire.js +7 -7
  314. package/dist/src/ui/tui/tui-editor-role-enabled.js +4 -3
  315. package/dist/src/ui/tui/tui-editor-role-file.js +4 -4
  316. package/dist/src/ui/tui/tui-editor-role-key.js +4 -3
  317. package/dist/src/ui/tui/tui-editor-role-model.js +97 -15
  318. package/dist/src/ui/tui/tui-editor-search.js +4 -3
  319. package/dist/src/ui/tui/tui-editor-session.js +17 -4
  320. package/dist/src/ui/tui/tui-editors.js +5 -10
  321. package/dist/src/ui/tui/tui-frame.js +182 -134
  322. package/dist/src/ui/tui/tui-guard.js +20 -0
  323. package/dist/src/ui/tui/tui-input.js +167 -98
  324. package/dist/src/ui/tui/tui-keymap.js +51 -29
  325. package/dist/src/ui/tui/tui-keys.js +1 -1
  326. package/dist/src/ui/tui/tui-pane-activity.js +5 -4
  327. package/dist/src/ui/tui/tui-pane-backlog.js +74 -36
  328. package/dist/src/ui/tui/tui-pane-ctx.js +1 -1
  329. package/dist/src/ui/tui/tui-pane-health.js +252 -230
  330. package/dist/src/ui/tui/tui-pane-history.js +67 -74
  331. package/dist/src/ui/tui/tui-pane-pending.js +288 -247
  332. package/dist/src/ui/tui/tui-pane-queued.js +25 -27
  333. package/dist/src/ui/tui/tui-pane-report.js +2 -1
  334. package/dist/src/ui/tui/tui-pane-session.js +15 -10
  335. package/dist/src/ui/tui/tui-pane-transcript.js +19 -31
  336. package/dist/src/ui/tui/tui-pane.js +14 -11
  337. package/dist/src/ui/tui/tui-panes.js +38 -33
  338. package/dist/src/ui/tui/tui.js +4 -7
  339. package/dist/src/verdict/fix-claim.js +107 -56
  340. package/dist/src/verdict/refusal.js +1 -1
  341. package/dist/src/verdict/reply-contract.js +3 -4
  342. package/package.json +5 -5
  343. package/dist/src/concurrency/provider-lease.js +0 -105
  344. package/dist/src/review/review-followup.js +0 -87
  345. package/dist/src/review/review-run.js +0 -395
  346. package/dist/src/runner/model-fallback.js +0 -76
  347. package/dist/src/runner/runner-fallback.js +0 -66
  348. package/dist/src/ui/tui/tui-editor-claude-reserve.js +0 -24
  349. package/dist/src/ui/tui/tui-editor-provider-caps.js +0 -27
  350. /package/dist/src/cli/{brief-command.js → cli-brief-command.js} +0 -0
  351. /package/dist/src/doctor/{probe.js → doctor-probe.js} +0 -0
  352. /package/dist/src/gates/{disk-gate.js → disk-gates.js} +0 -0
  353. /package/dist/src/operator/{retire.js → operator-retire.js} +0 -0
  354. /package/dist/src/operator/{revert.js → operator-revert.js} +0 -0
@@ -87,6 +87,62 @@ export function handleBacklogMatch(q, res, root) {
87
87
  .map((row) => ({ role: row.role, file: row.file }));
88
88
  sendJson(res, 200, out);
89
89
  }
90
+ /** Normalise an optional `?path=` query arg to `{ path }` (absent → `{ path: undefined }`),
91
+ * answering the shared 400 when the raw value does not name a repo-relative path. Shared by
92
+ * the `?overlaps` and `?role` arms; `null` means the 400 was sent, so the caller returns. */
93
+ function diffPathParam(res, rawPath) {
94
+ if (rawPath === null)
95
+ return { path: undefined };
96
+ const path = normalizeDiffPath(rawPath);
97
+ if (path === null) {
98
+ badRequest(res, "path takes a repo-relative path such as src/cli.ts (as overlaps lists it)");
99
+ return null;
100
+ }
101
+ return { path };
102
+ }
103
+ /** The `?overlaps` view: refuse stat, normalise the path, resolve the comma-separated role
104
+ * list, collect the overlaps and scope them by roles then by path, then send. */
105
+ async function handleDiffOverlaps(q, res, root, rawPath, stat) {
106
+ if (stat) {
107
+ badRequest(res, "overlaps lists paths, not patches; drop stat");
108
+ return;
109
+ }
110
+ const opath = diffPathParam(res, rawPath);
111
+ if (opath === null)
112
+ return;
113
+ const roles = roleListQuery(q, res, root);
114
+ if (roles === null)
115
+ return;
116
+ let view = await collectChangeOverlaps(root);
117
+ if (roles !== undefined)
118
+ view = scopeChangeOverlaps(view, roles);
119
+ if (opath.path !== undefined)
120
+ view = scopeChangeOverlapsToPath(view, opath.path);
121
+ sendJson(res, 200, view);
122
+ }
123
+ /** The role-less team roster: refuse stat and path with their own 400s, then send
124
+ * collectTeamChanges's document. */
125
+ async function handleDiffTeam(res, root, rawPath, stat) {
126
+ if (stat) {
127
+ badRequest(res, "stat needs role (the team roster carries no patch)");
128
+ return;
129
+ }
130
+ if (rawPath !== null) {
131
+ badRequest(res, "path needs role (the team roster carries no patch)");
132
+ return;
133
+ }
134
+ sendJson(res, 200, await collectTeamChanges(root));
135
+ }
136
+ /** The `?role=<id>` per-role view: validate through rejectBadRole, normalise the shared path,
137
+ * then send collectRoleChange with its path and stat options. */
138
+ async function handleDiffRole(res, root, role, rawPath, stat) {
139
+ if (rejectBadRole(root, res, role))
140
+ return;
141
+ const path = diffPathParam(res, rawPath);
142
+ if (path === null)
143
+ return;
144
+ sendJson(res, 200, await collectRoleChange(root, role, { ...(path.path === undefined ? {} : { path: path.path }), ...(stat ? { stat: true } : {}) }));
145
+ }
90
146
  /** Handle GET /api/diff[?role=<id>]: the unmerged-change view the CLI prints from the same
91
147
  * collectors — no `role` → collectTeamChanges (exactly `tumwater diff --json`'s document:
92
148
  * `{ mainBranch, roles }`, no per-role patch halves); `?role=<id>` → collectRoleChange (the
@@ -105,46 +161,10 @@ export function handleBacklogMatch(q, res, root) {
105
161
  export async function handleDiff(q, res, root) {
106
162
  const rawPath = q.get("path");
107
163
  const stat = q.get("stat") !== null;
108
- if (q.get("overlaps") !== null) {
109
- if (stat) {
110
- badRequest(res, "overlaps lists paths, not patches; drop stat");
111
- return;
112
- }
113
- const opath = rawPath === null ? undefined : normalizeDiffPath(rawPath);
114
- if (opath === null) {
115
- badRequest(res, "path takes a repo-relative path such as src/cli.ts (as overlaps lists it)");
116
- return;
117
- }
118
- const roles = roleListQuery(q, res, root);
119
- if (roles === null)
120
- return;
121
- let view = await collectChangeOverlaps(root);
122
- if (roles !== undefined)
123
- view = scopeChangeOverlaps(view, roles);
124
- if (opath !== undefined)
125
- view = scopeChangeOverlapsToPath(view, opath);
126
- sendJson(res, 200, view);
127
- return;
128
- }
164
+ if (q.get("overlaps") !== null)
165
+ return handleDiffOverlaps(q, res, root, rawPath, stat);
129
166
  const role = q.get("role");
130
- if (role === null) {
131
- if (stat) {
132
- badRequest(res, "stat needs role (the team roster carries no patch)");
133
- return;
134
- }
135
- if (rawPath !== null) {
136
- badRequest(res, "path needs role (the team roster carries no patch)");
137
- return;
138
- }
139
- sendJson(res, 200, await collectTeamChanges(root));
140
- return;
141
- }
142
- if (rejectBadRole(root, res, role))
143
- return;
144
- const path = rawPath === null ? undefined : normalizeDiffPath(rawPath);
145
- if (path === null) {
146
- badRequest(res, "path takes a repo-relative path such as src/cli.ts (as overlaps lists it)");
147
- return;
148
- }
149
- sendJson(res, 200, await collectRoleChange(root, role, { ...(path === undefined ? {} : { path }), ...(stat ? { stat: true } : {}) }));
167
+ if (role === null)
168
+ return handleDiffTeam(res, root, rawPath, stat);
169
+ return handleDiffRole(res, root, role, rawPath, stat);
150
170
  }
@@ -32,29 +32,19 @@ export function lanAddresses(interfaces = os.networkInterfaces()) {
32
32
  * /proc/<pid>/cmdline exposes to every local user and what a reload re-runs verbatim — so the
33
33
  * secret must travel by env, never argv. */
34
34
  export const GUI_TOKEN_ENV = "TUMWATER_GUI_TOKEN";
35
- /** The `tumwater gui` command: parse the --port/--all-interfaces/--token flags, start the
36
- * server, print the banner, and serve until Ctrl+C. Lives beside startGui (gui/gui-server.ts) so
37
- * the CLI's gui-specific policy (token validation, the listen-failure messages, the LAN
38
- * announcement) cannot drift from the server it drives. Flag-vocabulary rejection stays in
39
- * cli.ts with the other cases; everything gui-specific after that gate is this function's job.
40
- * A failed listen throws (cli.ts's main catch renders it as the same `tumwater: …` line and
41
- * exit 1 fail() would) so the listen-failure wordings stay assertable in-process. */
42
- export async function cmdGui(root, args, seams = {}) {
43
- const start = seams.startGui ?? startGui;
44
- const lan = seams.lanAddresses ?? lanAddresses;
45
- const print = seams.say ?? say;
46
- const serve = seams.serve ?? (() => new Promise(() => { }));
47
- const portRaw = flagValue(args, "--port");
48
- const port = portRaw !== null ? parsePortFlag(portRaw) : 7180;
49
- const allInterfaces = args.includes("--all-interfaces");
50
- // A valueless or empty --token is a CLI error, not an open server: an operator who
51
- // asked for protection must never silently get none. The validation and its wording
52
- // live in tokenValueProblem, shared with `start --gui` so the two callers cannot
53
- // drift; its flag-looking-value case names `tumwater gui` here.
54
- const problem = tokenValueProblem(args, "tumwater gui");
55
- if (problem)
56
- fail(problem);
57
- const flagToken = flagValue(args, "--token");
35
+ /** Resolve cmdGui's four injectable seams to their runnable forms: the server it starts, the
36
+ * LAN address source the banner names, the printer for the banner and warnings, and the
37
+ * serve-until-Ctrl+C wait. Production callers omit every seam and get the real server,
38
+ * interfaces, stdout, and wait. */
39
+ function resolveGuiSeams(seams) {
40
+ return {
41
+ start: seams.startGui ?? startGui,
42
+ lan: seams.lanAddresses ?? lanAddresses,
43
+ print: seams.say ?? say,
44
+ serve: seams.serve ?? (() => new Promise(() => { })),
45
+ };
46
+ }
47
+ function adoptGuiToken(flagToken, print) {
58
48
  // A direct `tumwater gui --token` puts the secret on its own argv — readable by any local
59
49
  // user and re-run verbatim by reexecSelf onto every reloaded generation — so fold it into
60
50
  // the env and take it off the command line before a redeploy can re-expose it. A child
@@ -66,7 +56,10 @@ export async function cmdGui(root, args, seams = {}) {
66
56
  process.argv.splice(argvToken, 2);
67
57
  print(`--token is visible to every local user in this process's command line (\`ps\`, /proc/<pid>/cmdline) and is re-run onto each reloaded generation — prefer the ${GUI_TOKEN_ENV} env var`);
68
58
  }
69
- const token = flagToken ?? process.env[GUI_TOKEN_ENV] ?? "";
59
+ return flagToken ?? process.env[GUI_TOKEN_ENV] ?? "";
60
+ }
61
+ /** Start the server and translate the two common listen failures into hints naming their fix. */
62
+ async function startGuiOrExplain(start, root, port, allInterfaces, token) {
70
63
  try {
71
64
  await start(root, port, allInterfaces, token);
72
65
  }
@@ -83,6 +76,9 @@ export async function cmdGui(root, args, seams = {}) {
83
76
  throw new Error(`port ${port} could not be opened (permission denied) — ports below 1024 need root; pick an unprivileged port with \`tumwater gui --port <n>\``);
84
77
  throw err;
85
78
  }
79
+ }
80
+ /** Print the localhost banner and, under --all-interfaces, the LAN URLs and exposure wording. */
81
+ function printGuiBanner(print, lan, port, allInterfaces, token) {
86
82
  const tokenSuffix = token ? `/?token=${encodeURIComponent(token)}` : "";
87
83
  print(`tumwater gui at http://127.0.0.1:${port}${tokenSuffix} — Ctrl+C to stop`);
88
84
  if (allInterfaces) {
@@ -95,5 +91,28 @@ export async function cmdGui(root, args, seams = {}) {
95
91
  ? "listening on ALL interfaces — token-protected; prompting the director requires the token"
96
92
  : "listening on ALL interfaces — no auth; anyone reaching it can prompt the director");
97
93
  }
94
+ }
95
+ /** The `tumwater gui` command: parse the --port/--all-interfaces/--token flags, start the
96
+ * server, print the banner, and serve until Ctrl+C. Lives beside startGui (gui/gui-server.ts) so
97
+ * the CLI's gui-specific policy (token validation, the listen-failure messages, the LAN
98
+ * announcement) cannot drift from the server it drives. Flag-vocabulary rejection stays in
99
+ * cli.ts with the other cases; everything gui-specific after that gate is this function's job.
100
+ * A failed listen throws (cli.ts's main catch renders it as the same `tumwater: …` line and
101
+ * exit 1 fail() would) so the listen-failure wordings stay assertable in-process. */
102
+ export async function cmdGui(root, args, seams = {}) {
103
+ const { start, lan, print, serve } = resolveGuiSeams(seams);
104
+ const portRaw = flagValue(args, "--port");
105
+ const port = portRaw !== null ? parsePortFlag(portRaw) : 7180;
106
+ const allInterfaces = args.includes("--all-interfaces");
107
+ // A valueless or empty --token is a CLI error, not an open server: an operator who
108
+ // asked for protection must never silently get none. The validation and its wording
109
+ // live in tokenValueProblem, shared with `start --gui` so the two callers cannot
110
+ // drift; its flag-looking-value case names `tumwater gui` here.
111
+ const problem = tokenValueProblem(args, "tumwater gui");
112
+ if (problem)
113
+ fail(problem);
114
+ const token = adoptGuiToken(flagValue(args, "--token") ?? null, print);
115
+ await startGuiOrExplain(start, root, port, allInterfaces, token);
116
+ printGuiBanner(print, lan, port, allInterfaces, token);
98
117
  await serve(); // Serve until Ctrl+C.
99
118
  }
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * The dashboard's POST config/budget command handlers (barrelled into
3
3
  * gui/gui-endpoint-commands.ts, which routes to them from src/gui/gui-server.ts):
4
- * config-set, config-unset, budget, role-cap, role-review-after-merge, role-enable,
4
+ * config-set, config-unset, budget, role-cap, role-enable,
5
5
  * role-disable, and
6
6
  * notify-test — the
7
7
  * post-handler layer onto
@@ -9,9 +9,8 @@
9
9
  * own `res`.
10
10
  */
11
11
  import { setConfigKey, setDailyBudgetUsd, setRoleModelFields, unsetConfigKey } from "../config/config-write.js";
12
- import { loadConfigSafe } from "../config/config.js";
13
12
  import { checkDailyBudgetUsd, checkMinRunInterval } from "../config/config-keys.js";
14
- import { testNotifyHook } from "../cli/notify-command.js";
13
+ import { testNotifyHook } from "../cli/cli-notify-command.js";
15
14
  import { rejectBadRole, requireConfigKey, requireRoleField, requireStringField } from "./gui-args.js";
16
15
  import { baseRoleOf } from "../roles/runner-ids.js";
17
16
  import { DIRECTOR_ROLE } from "../roles/roles.js";
@@ -88,67 +87,6 @@ export async function handleRoleCap(req, res, root) {
88
87
  return configKeyError(res, "maxDailyCostUsdPerRole." + baseRole, result);
89
88
  sendJson(res, 200, { ok: true, role: baseRole, maxDailyCostUsd: value, ...unchangedFlag(result) });
90
89
  }
91
- /** Handle POST /api/role-review-after-merge: the role drawer's Review row button moves one role
92
- * in or out of review.afterMerge — the browser twin of `tumwater role <id>
93
- * --review-after-merge`/`--clear-review-after-merge` (the CLI's just-landed write, the amber
94
- * role-cap alert's remedy on the surface the alert appears on). Same body discipline as
95
- * /api/role-cap (readPostBody → 400 malformed/non-object, 413 oversized); the body carries the
96
- * desired membership `afterMerge: true|false` (400 `afterMerge must be true or false`
97
- * otherwise), and the role id maps to its base role's membership (baseRoleOf) exactly as the
98
- * CLI's `--review-after-merge` branch does. An unknown id is refused up front (rejectBadRole
99
- * on the base id, the mirror of the CLI's requireKnownRole) because a no-op or clear write
100
- * skips validateConfig's known-role refusal — setConfigKey's section branch returns unchanged
101
- * and writes nothing when the candidate equals the resolved list, so without it a bogus id
102
- * would answer 200 on a clear. The whole-list value rides the same setConfigKey core `config
103
- * set review.afterMerge <json>` uses, so validateConfig owns the string-array shape and the
104
- * `review.afterMerge must not be "director"` refusal — no explicit director branch, matching
105
- * the CLI. A no-op write (already in the asked-for state) answers unchanged and leaves
106
- * tumwater.json byte-identical. */
107
- export async function handleRoleReviewAfterMerge(req, res, root) {
108
- const body = await readPostBody(req, res, '{"role": "feature", "afterMerge": true}');
109
- if (!body)
110
- return;
111
- const role = requireRoleField(res, body);
112
- if (role === null)
113
- return;
114
- const want = body.afterMerge;
115
- if (typeof want !== "boolean") {
116
- badRequest(res, "afterMerge must be true or false");
117
- return;
118
- }
119
- const base = baseRoleOf(role);
120
- // An unknown base id is refused up front — the mirror of the CLI's requireKnownRole. A
121
- // no-op or clear write returns the list unchanged and setConfigKey skips validateConfig
122
- // (the known-role check a real write gets for free), so without this guard a bogus id
123
- // would answer 200 on the clear path (same reason handleRoleQuietHours's clear arm carries
124
- // rejectBadRole). The stored list holds base ids, so the base id is screened — as
125
- // validateConfig's checkKnownRoleId screens the written list — letting an instance id
126
- // like feature-2 map to its known base and pass.
127
- if (rejectBadRole(root, res, base))
128
- return;
129
- // The CLI's own computation, list-for-list: the whole value lands through the same
130
- // setConfigKey core so validateConfig owns the string-array shape and the director
131
- // refusal, and the write is atomic.
132
- const list = loadConfigSafe(root).config?.review.afterMerge ?? [];
133
- const has = list.includes(base);
134
- const next = has === want ? list : want ? [...list, base] : list.filter((r) => r !== base);
135
- const result = setConfigKey(root, "review.afterMerge", JSON.stringify(next));
136
- if (!result.ok)
137
- return configKeyError(res, "review.afterMerge", result);
138
- sendJson(res, 200, {
139
- ok: true,
140
- role: base,
141
- afterMerge: want,
142
- message: result.unchanged
143
- ? want
144
- ? `${base}: already in review.afterMerge, so tumwater.json was not written`
145
- : `${base}: not in review.afterMerge, so nothing was cleared`
146
- : want
147
- ? `${base}: added to review.afterMerge`
148
- : `${base}: removed from review.afterMerge`,
149
- ...unchangedFlag(result),
150
- });
151
- }
152
90
  /** Handle POST /api/role-interval: the role drawer's min run interval editor sets or clears
153
91
  * one role's override of the team-wide minRunIntervalSeconds — the knob that makes one role
154
92
  * act rarely, distinct from the team-wide value. Same body discipline as /api/role-cap
@@ -15,7 +15,7 @@
15
15
  */
16
16
  export { handlePrompt, handlePromptRole, handlePromptCancel, handlePromptEdit, handlePromptTop } from "./gui-prompt-commands.js";
17
17
  export { handleQuestionAnswer, handleBacklogFile, handleBacklogCancel, handleBacklogRelease, handleBacklogTop, handleBacklogEdit } from "./gui-backlog-commands.js";
18
- export { handleConfigSet, handleConfigUnset, handleBudget, handleRoleCap, handleRoleReviewAfterMerge, handleRoleEnable, handleRoleDisable, handleRoleInstances, handleRoleInterval, handleRoleInstructions, handleRoleModel, handleRoleQuietHours, handleNotifyTest } from "./gui-config-commands.js";
18
+ export { handleConfigSet, handleConfigUnset, handleBudget, handleRoleCap, handleRoleEnable, handleRoleDisable, handleRoleInstances, handleRoleInterval, handleRoleInstructions, handleRoleModel, handleRoleQuietHours, handleNotifyTest } from "./gui-config-commands.js";
19
19
  export { handlePause, handlePauseRole, handleWake, handleWakeCancel } from "./gui-team-commands.js";
20
20
  export { handleResetCounters, handleRestart, handleReclaim, handleKillOrphans, handleDropBrokenPins, handleCommitBacklog, handleResyncInstall, handleRetireLeftovers, handleArchiveBacklog, handleAbort, handleAbortAll, handleStopSchedule, handleRoleNotes, handleRetire, handleRevert, handleQueueCancel, handleQueueTop, handlePostReviewAccept, handleBriefEdit } from "./gui-operator-commands.js";
21
21
  export { handleQuarantineRelease } from "./gui-quarantine-commands.js";
@@ -7,7 +7,7 @@
7
7
  * inputs with — and sendRequestResult, the shared boolean-claiming marker reply. The
8
8
  * per-domain `checked*` validators live with their domain's module, not here.
9
9
  */
10
- import { rejectBadRole } from "./gui-args.js";
10
+ import { rejectBadRole, requireSha } from "./gui-args.js";
11
11
  import { readJsonObject, sendJson } from "./gui-http-body.js";
12
12
  /** Every POST handler's shared opening: readJsonObject reads the body and — when it is
13
13
  * oversized, malformed, or not a JSON object — has already written the 413/400 itself. Its
@@ -38,6 +38,24 @@ export async function readRoleBody(req, res, example, root, allowMissing = false
38
38
  return null; // 400 already sent
39
39
  return body;
40
40
  }
41
+ /** The sha-addressed POST handlers' shared prologue: readPostBody (400/413 sent on a null
42
+ * return) plus requireSha's hex-commit-id validation (400 sent on a null return), so the
43
+ * queue-cancel/queue-top/post-review-accept/revert family states the body-discipline-and-sha
44
+ * check pair once instead of two guard lines per handler. Its null return means the response
45
+ * is already written and the handler must stop; callers keep their own `if (!read) return`
46
+ * bail. Exactly four call sites — handleRevert, handleQueueCancel, handleQueueTop, and
47
+ * handlePostReviewAccept — returning the body too so handleRevert can read its optional
48
+ * dryRun; the other three callers use only the sha. A caller that needs neither the body nor
49
+ * the example can read just `{ sha }` from the result. */
50
+ export async function readShaBody(req, res) {
51
+ const body = await readPostBody(req, res, '{"sha": "1a2b3c4"}');
52
+ if (body === null)
53
+ return null; // 4xx already sent — oversized, malformed, or not an object
54
+ const sha = requireSha(res, body.sha);
55
+ if (sha === null)
56
+ return null; // 400 already sent
57
+ return { body, sha };
58
+ }
41
59
  /** Reply for a boolean-claiming marker request (RequestResult): success maps to 200 with its
42
60
  * message, refusal to 409 — the request is valid but nothing can act on it, the same conflict
43
61
  * reading requestAbort's not-live error uses. /api/restart and /api/abort share it. */
@@ -14,7 +14,7 @@ import { eventRole, readEvents } from "../events/event-read.js";
14
14
  import { eventFeedItem, eventMatchesGrep } from "../events/event-format.js";
15
15
  import { readRunDetail, runCommitSha, runMergedSha } from "../run/run-detail-data.js";
16
16
  import { renderRunDetail, runDetailPatch, runDetailStat, runTranscriptLines } from "../run/run-detail-render.js";
17
- import { resolveRevertTarget } from "../operator/revert.js";
17
+ import { resolveRevertTarget } from "../operator/operator-revert.js";
18
18
  import { rolePayload } from "../roles/role-view.js";
19
19
  import { clampCount, intQuery, rejectBadRole, sinceMsQuery, untilMsQuery } from "./gui-args.js";
20
20
  import { badRequest, sendJson } from "./gui-http-body.js";
@@ -124,36 +124,67 @@ function windowOrCount(q, res) {
124
124
  return { kind: "count" };
125
125
  }
126
126
  export function handleHistory(q, res, root) {
127
+ const req = historyRequest(q, res);
128
+ if (req === null)
129
+ return;
130
+ const ctl = req.windowCtl;
131
+ if (ctl.kind === "window")
132
+ serveHistoryWindow(res, root, req, ctl.sinceMs, ctl.untilMs);
133
+ else
134
+ serveHistoryCount(q, res, root, req);
135
+ }
136
+ /** Parse the request prefix `GET /api/history` shares across both shapes: the `role`/`grep`
137
+ * filters, the `result` class list (resultClassesQuery, a 400 on an unknown class), the `model`
138
+ * selector list, the `summary` flag's mere presence, and the rival `sinceMs`-vs-`n` shape
139
+ * (windowOrCount, a 400 on both or a bad cap). Any 400 is already sent and yields null. */
140
+ function historyRequest(q, res) {
127
141
  const role = q.get("role") || null; // "" and absent both read all roles
128
142
  const grep = q.get("grep") || null; // "" and absent both filter nothing
129
143
  // `model` is a comma list of exact selectors (trimmed, deduplicated); "", absent and a list
130
144
  // with no non-empty piece all filter nothing, and an unknown selector keeps no rows.
131
145
  const classes = resultClassesQuery(q, res);
132
146
  if (classes === null)
133
- return;
134
- const inClasses = (rows) => (classes.length ? rows.filter((r) => classes.includes(historyClass(r.result))) : rows);
147
+ return null;
135
148
  const modelPieces = splitCommaListUnique(q.get("model") ?? "");
136
- const onModels = (rows) => (modelPieces.length ? runRowsOnModels(rows, modelPieces) : rows);
137
149
  const summary = q.get("summary") !== null; // the flag's presence, whatever its value
138
150
  const windowCtl = windowOrCount(q, res);
139
151
  if (windowCtl === null)
140
- return;
141
- if (windowCtl.kind === "window") {
142
- const { sinceMs, untilMs } = windowCtl;
143
- const { rows, covered } = readRunRowsSince(root, sinceMs, role, untilMs ?? null);
144
- const matched = onModels(inClasses(grep === null ? rows : runRowsMatchingGrep(rows, grep)));
145
- sendJson(res, 200, { rows: matched, covered, note: covered ? "" : SPARSE_WINDOW_NOTE, ...(summary ? { summary: summarizeRunRows(matched), modelSummary: summarizeRunRowsByModel(matched) } : {}) });
146
- return;
147
- }
152
+ return null;
153
+ return { role, grep, classes, modelPieces, summary, windowCtl };
154
+ }
155
+ /** The shared filter chain both shapes apply to the served rows: grep first, then the `result`
156
+ * outcome classes, then the `model` selectors — the CLI's `history --grep`, `--result` and
157
+ * `--model` rules, in that order. */
158
+ function historyRows(rows, req) {
159
+ const grepped = req.grep === null ? rows : runRowsMatchingGrep(rows, req.grep);
160
+ const classed = req.classes.length ? grepped.filter((r) => req.classes.includes(historyClass(r.result))) : grepped;
161
+ return req.modelPieces.length ? runRowsOnModels(classed, req.modelPieces) : classed;
162
+ }
163
+ /** The optional additive fold both answers share: when `summary` is present, `{ summary,
164
+ * modelSummary }` added to the 200 payload; absent, nothing (so the response is byte-for-byte
165
+ * today's). */
166
+ function historySummaryFields(rows, summary) {
167
+ return summary ? { summary: summarizeRunRows(rows), modelSummary: summarizeRunRowsByModel(rows) } : {};
168
+ }
169
+ /** Answer the trailing-window shape ({ rows, covered, note }): rows read through readRunRowsSince
170
+ * from `sinceMs` (untilMs drops the rows newer than now − untilMs when present, exactly as the
171
+ * CLI's `history --since … --until` window reads), then the shared historyRows filter, then the
172
+ * optional summary fold — key order rows, covered, note, summary, modelSummary. */
173
+ function serveHistoryWindow(res, root, req, sinceMs, untilMs) {
174
+ const { rows, covered } = readRunRowsSince(root, sinceMs, req.role, untilMs ?? null);
175
+ const matched = historyRows(rows, req);
176
+ sendJson(res, 200, { rows: matched, covered, note: covered ? "" : SPARSE_WINDOW_NOTE, ...historySummaryFields(matched, req.summary) });
177
+ }
178
+ /** Answer the newest-`n` count shape: `n` parses through intQuery (non-negative, default
179
+ * HISTORY_DEFAULT_RUNS) and is clamped to [1, HISTORY_MAX_RUNS] — the GUI clamps where the CLI
180
+ * fails fast — then rows read through readRunRows, filtered and answered through the shared
181
+ * historyRows/historySummaryFields, key order rows, summary, modelSummary. */
182
+ function serveHistoryCount(q, res, root, req) {
148
183
  const parsed = intQuery(q, res, "n", "non-negative", HISTORY_DEFAULT_RUNS);
149
184
  if (parsed === null)
150
185
  return;
151
- const n = clampCount(parsed, HISTORY_MAX_RUNS);
152
- let rows = readRunRows(root, n, role);
153
- if (grep !== null)
154
- rows = runRowsMatchingGrep(rows, grep);
155
- rows = onModels(inClasses(rows));
156
- sendJson(res, 200, summary ? { rows, summary: summarizeRunRows(rows), modelSummary: summarizeRunRowsByModel(rows) } : { rows });
186
+ const rows = historyRows(readRunRows(root, clampCount(parsed, HISTORY_MAX_RUNS), req.role), req);
187
+ sendJson(res, 200, { rows, ...historySummaryFields(rows, req.summary) });
157
188
  }
158
189
  /** How many of the log's newest events `GET /api/logs` scans by default, and the most it will
159
190
  * scan however large `n` asks: the CLI's `-n` shape (n bounds the scanned tail, not the printed
@@ -245,47 +276,64 @@ function transcriptText(root, role, run, detail, includePrompts, grep) {
245
276
  * payload's `role` is that run's; `result`/`model` narrow it the same way, and a miss is a 404
246
277
  * worded for any role. */
247
278
  export async function handleRun(q, res, root) {
248
- let role = q.get("role");
279
+ const target = runSelector(q, res, root);
280
+ if (target === null)
281
+ return;
282
+ const detail = readRunDetail(root, target.role, target.run);
283
+ if (detail === null) {
284
+ sendJson(res, 404, { error: runNotFoundMessage(target.role, target.run) });
285
+ return;
286
+ }
287
+ sendJson(res, 200, await runPayload(root, target.role, target.run, detail, q));
288
+ }
289
+ /** Resolve which run the request names — validating the role, then the numbered or `last` form. */
290
+ function runSelector(q, res, root) {
291
+ const role = q.get("role");
249
292
  const bareLast = role === null && q.get("last") !== null;
250
293
  if (!bareLast && rejectBadRole(root, res, role))
251
- return;
252
- let run;
253
- if (q.get("last") === null) {
254
- if (q.get("result") !== null)
255
- return badRequest(res, "result needs last");
256
- if (q.get("model") !== null)
257
- return badRequest(res, "model needs last");
258
- run = intQuery(q, res, "run", "positive");
259
- if (run === null)
260
- return;
294
+ return null;
295
+ return q.get("last") !== null ? lastRunSelector(q, res, root, role) : numberedRunSelector(q, res, role);
296
+ }
297
+ /** Resolve the explicit `run=N` selector of /api/run (three 400s, the miss worded by intQuery). */
298
+ function numberedRunSelector(q, res, role) {
299
+ if (q.get("result") !== null) {
300
+ badRequest(res, "result needs last");
301
+ return null;
261
302
  }
262
- else {
263
- if (q.get("run") !== null)
264
- return badRequest(res, "last cannot be combined with run (a number and the newest run are rival selectors)");
265
- const classes = resultClassesQuery(q, res);
266
- if (classes === null)
267
- return;
268
- const models = splitCommaListUnique(q.get("model") ?? "");
269
- const classFilter = classes.length ? classes : null;
270
- const modelFilter = models.length ? models : null;
271
- const row = newestRunRow(root, role, classFilter, modelFilter);
272
- if (row !== null)
273
- role = row.role;
274
- run = row?.run ?? null;
275
- if (run === null) {
276
- sendJson(res, 404, { error: runNotFoundLastMessage(role, classFilter, modelFilter) });
277
- return;
278
- }
303
+ if (q.get("model") !== null) {
304
+ badRequest(res, "model needs last");
305
+ return null;
279
306
  }
280
- const detail = readRunDetail(root, role, run);
281
- if (detail === null) {
282
- sendJson(res, 404, { error: runNotFoundMessage(role, run) });
283
- return;
307
+ const run = intQuery(q, res, "run", "positive");
308
+ if (run === null)
309
+ return null;
310
+ return { role: role, run };
311
+ }
312
+ /** Resolve the `last` selector, narrowed by `result`/`model`, to the role's newest run. */
313
+ function lastRunSelector(q, res, root, role) {
314
+ if (q.get("run") !== null) {
315
+ badRequest(res, "last cannot be combined with run (a number and the newest run are rival selectors)");
316
+ return null;
317
+ }
318
+ const classes = resultClassesQuery(q, res);
319
+ if (classes === null)
320
+ return null;
321
+ const models = splitCommaListUnique(q.get("model") ?? "");
322
+ const classFilter = classes.length ? classes : null;
323
+ const modelFilter = models.length ? models : null;
324
+ const row = newestRunRow(root, role, classFilter, modelFilter);
325
+ if (row === null) {
326
+ sendJson(res, 404, { error: runNotFoundLastMessage(role, classFilter, modelFilter) });
327
+ return null;
284
328
  }
329
+ return { role: row.role, run: row.run };
330
+ }
331
+ /** Assemble the 200 payload — the collector plus its rendered, stat and flag-gated fields. */
332
+ async function runPayload(root, role, run, detail, q) {
285
333
  const grep = (q.get("grep") ?? "").trim();
286
334
  const mergedAs = runMergedSha(detail);
287
335
  const target = mergedAs === null ? null : await resolveRevertTarget(root, mergedAs);
288
- sendJson(res, 200, {
336
+ return {
289
337
  ...detail,
290
338
  text: renderRunDetail(detail),
291
339
  stat: await runDetailStat(root, detail),
@@ -293,7 +341,7 @@ export async function handleRun(q, res, root) {
293
341
  hasCommit: runCommitSha(detail) !== null,
294
342
  ...(q.get("patch") !== null ? { patch: await runDetailPatch(root, detail) } : {}),
295
343
  ...(q.get("transcript") !== null ? { transcript: transcriptText(root, role, run, detail, q.get("prompt") !== null, grep === "" ? undefined : grep) } : {}),
296
- });
344
+ };
297
345
  }
298
346
  /** Handle GET /api/role?role=<id>: one role's inspection payload — rolePayload's object,
299
347
  * the same collection behind `tumwater role <id>` — so the drawer's Notes and Next run