tumwater 0.2.0 → 0.3.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 (446) 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 +42 -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 +4 -3
  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 +402 -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 +246 -55
  41. package/dist/src/backlog/backlog-cli-view.js +280 -0
  42. package/dist/src/backlog/backlog-cli.js +85 -274
  43. package/dist/src/backlog/backlog-commit.js +4 -5
  44. package/dist/src/backlog/backlog-eligibility.js +175 -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 +254 -0
  48. package/dist/src/backlog/backlog-release.js +117 -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-culprit.js +94 -0
  56. package/dist/src/baseline/main-guardrails.js +209 -0
  57. package/dist/src/baseline/main-red.js +139 -32
  58. package/dist/src/baseline/main-revert.js +133 -0
  59. package/dist/src/baseline/main-watch.js +184 -0
  60. package/dist/src/brief/brief-edit.js +9 -11
  61. package/dist/src/brief/brief.js +31 -51
  62. package/dist/src/budget/budget.js +30 -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 +1 -2
  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 +118 -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 +150 -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 +308 -590
  82. package/dist/src/cli/cli-repo-gate.js +11 -1
  83. package/dist/src/cli/cli-role-command.js +420 -0
  84. package/dist/src/cli/cli-start-summary.js +160 -0
  85. package/dist/src/cli/{cli-run.js → cli-start.js} +165 -237
  86. package/dist/src/cli/completion.js +149 -22
  87. package/dist/src/cli/config-commands.js +41 -18
  88. package/dist/src/cli/follow-view.js +75 -0
  89. package/dist/src/cli/help.js +189 -73
  90. package/dist/src/cli/log-commands.js +148 -82
  91. package/dist/src/cli/question-commands.js +205 -10
  92. package/dist/src/cli.js +106 -43
  93. package/dist/src/collections.js +26 -10
  94. package/dist/src/concurrency/check-permit.js +20 -3
  95. package/dist/src/concurrency/lock.js +17 -13
  96. package/dist/src/concurrency/provider-lease.js +44 -8
  97. package/dist/src/concurrency/semaphore.js +102 -26
  98. package/dist/src/config/config-aliases.js +0 -1
  99. package/dist/src/config/config-editable-keys.js +13 -1
  100. package/dist/src/config/config-fallbacks.js +93 -56
  101. package/dist/src/config/config-field-checks.js +40 -10
  102. package/dist/src/config/config-keys.js +30 -8
  103. package/dist/src/config/config-live.js +63 -24
  104. package/dist/src/config/config-request.js +137 -0
  105. package/dist/src/config/config-schema.js +31 -24
  106. package/dist/src/config/config-selector-checks.js +123 -0
  107. package/dist/src/config/config-validation-agents.js +80 -0
  108. package/dist/src/config/config-validation-model.js +116 -0
  109. package/dist/src/config/config-validation.js +141 -199
  110. package/dist/src/config/config-views.js +127 -53
  111. package/dist/src/config/config-write-core.js +135 -0
  112. package/dist/src/config/config-write.js +131 -264
  113. package/dist/src/config/config.js +48 -38
  114. package/dist/src/config/model-selector.js +34 -34
  115. package/dist/src/doctor/doctor-backlog-bugs.js +287 -0
  116. package/dist/src/doctor/doctor-backlog-plans.js +263 -0
  117. package/dist/src/doctor/doctor-backlog.js +100 -496
  118. package/dist/src/doctor/doctor-check-names.js +81 -0
  119. package/dist/src/doctor/doctor-checks.js +52 -579
  120. package/dist/src/doctor/doctor-claude-checks.js +145 -0
  121. package/dist/src/doctor/doctor-codex-checks.js +279 -0
  122. package/dist/src/doctor/doctor-contention-checks.js +380 -0
  123. package/dist/src/doctor/doctor-holds.js +82 -66
  124. package/dist/src/doctor/doctor-install.js +2 -1
  125. package/dist/src/doctor/doctor-model-checks.js +236 -402
  126. package/dist/src/doctor/doctor-notify.js +3 -4
  127. package/dist/src/doctor/doctor-opencode-checks.js +162 -0
  128. package/dist/src/doctor/doctor-orphans.js +182 -63
  129. package/dist/src/doctor/doctor-project-checks.js +303 -0
  130. package/dist/src/doctor/doctor.js +121 -77
  131. package/dist/src/doctor/image-probe.js +21 -8
  132. package/dist/src/doctor/outcome.js +3 -2
  133. package/dist/src/doctor/probe.js +7 -11
  134. package/dist/src/events/event-format.js +181 -86
  135. package/dist/src/events/event-labels.js +12 -5
  136. package/dist/src/events/event-legacy.js +39 -15
  137. package/dist/src/events/event-read.js +39 -39
  138. package/dist/src/events/event-tally.js +16 -0
  139. package/dist/src/events/event-window.js +60 -24
  140. package/dist/src/events/events.js +30 -13
  141. package/dist/src/events/notify.js +7 -7
  142. package/dist/src/failure/failure-cluster.js +12 -12
  143. package/dist/src/failure/failure-data.js +56 -56
  144. package/dist/src/failure/failure-render.js +31 -23
  145. package/dist/src/failure/failure-state-change.js +17 -12
  146. package/dist/src/failure/time-spend.js +99 -35
  147. package/dist/src/files/files.js +111 -35
  148. package/dist/src/files/json-files.js +3 -3
  149. package/dist/src/files/json-object.js +28 -7
  150. package/dist/src/files/stat-cache.js +2 -2
  151. package/dist/src/files/tail.js +1 -1
  152. package/dist/src/gates/bootstrap-gates.js +4 -5
  153. package/dist/src/gates/budget-gates.js +177 -84
  154. package/dist/src/gates/disk-gate.js +1 -1
  155. package/dist/src/gates/gate-polls.js +108 -58
  156. package/dist/src/gates/maintenance-quota.js +10 -13
  157. package/dist/src/gates/pause-gates.js +28 -10
  158. package/dist/src/gates/readiness.js +7 -3
  159. package/dist/src/gates/role-cap-gates.js +8 -8
  160. package/dist/src/gates/startup-gate.js +94 -32
  161. package/dist/src/gates/streak-gate.js +54 -30
  162. package/dist/src/git/commit-message.js +77 -46
  163. package/dist/src/git/git-diff.js +16 -10
  164. package/dist/src/git/git-run.js +79 -38
  165. package/dist/src/git/git.js +36 -34
  166. package/dist/src/git/slot-salvage.js +253 -0
  167. package/dist/src/git/slots-state.js +6 -6
  168. package/dist/src/git/worktree-pool.js +132 -38
  169. package/dist/src/git/worktree-use.js +4 -11
  170. package/dist/src/git/worktree.js +94 -37
  171. package/dist/src/gui/gui-admin-endpoints.js +96 -0
  172. package/dist/src/gui/gui-args.js +79 -8
  173. package/dist/src/gui/gui-backlog-commands.js +42 -18
  174. package/dist/src/gui/gui-backlog-endpoints.js +155 -0
  175. package/dist/src/gui/gui-command.js +8 -13
  176. package/dist/src/gui/gui-config-commands.js +94 -72
  177. package/dist/src/gui/gui-endpoint-commands.js +4 -3
  178. package/dist/src/gui/gui-endpoint-common.js +4 -4
  179. package/dist/src/gui/gui-endpoints.js +11 -380
  180. package/dist/src/gui/gui-history-endpoints.js +301 -0
  181. package/dist/src/gui/gui-operator-commands.js +108 -13
  182. package/dist/src/gui/gui-prompt-commands.js +11 -14
  183. package/dist/src/gui/gui-report-endpoints.js +90 -0
  184. package/dist/src/gui/gui-server.js +11 -4
  185. package/dist/src/gui/gui-team-commands.js +7 -7
  186. package/dist/src/gui/http-body.js +1 -1
  187. package/dist/src/history/history-data.js +122 -234
  188. package/dist/src/history/history-queued.js +147 -0
  189. package/dist/src/history/history-render.js +42 -22
  190. package/dist/src/history/history.js +193 -49
  191. package/dist/src/inbox/inbox-attachments.js +29 -30
  192. package/dist/src/inbox/inbox-cancel.js +10 -11
  193. package/dist/src/inbox/inbox-edit.js +9 -9
  194. package/dist/src/inbox/inbox-submit.js +18 -18
  195. package/dist/src/inbox/inbox-top.js +25 -9
  196. package/dist/src/inbox/inbox.js +42 -43
  197. package/dist/src/inbox/pending-prompt.js +26 -26
  198. package/dist/src/inbox/prompt-commands.js +62 -49
  199. package/dist/src/inbox/prompt-not-before.js +2 -2
  200. package/dist/src/init/init-templates.js +1 -2
  201. package/dist/src/init/init.js +28 -23
  202. package/dist/src/merge/backlog-conflicts.js +215 -35
  203. package/dist/src/merge/merge-batch.js +14 -8
  204. package/dist/src/merge/merge-cancel.js +53 -0
  205. package/dist/src/merge/merge-check-failures.js +44 -22
  206. package/dist/src/merge/merge-core.js +81 -32
  207. package/dist/src/merge/merge-diff.js +5 -4
  208. package/dist/src/merge/merge-drain.js +150 -11
  209. package/dist/src/merge/merge-git.js +177 -38
  210. package/dist/src/merge/merge-pipeline.js +15 -3
  211. package/dist/src/merge/merge-post-review.js +90 -0
  212. package/dist/src/merge/merge-queue-render.js +49 -11
  213. package/dist/src/merge/merge-queue.js +31 -15
  214. package/dist/src/merge/merge-requeue.js +102 -0
  215. package/dist/src/merge/merge-slot.js +61 -16
  216. package/dist/src/merge/merge-stack.js +33 -11
  217. package/dist/src/merge/merge-to-main.js +86 -27
  218. package/dist/src/merge/merge-top.js +69 -0
  219. package/dist/src/merge/merge-verify.js +12 -10
  220. package/dist/src/notes-cli.js +30 -15
  221. package/dist/src/operator/operator-commands.js +192 -81
  222. package/dist/src/operator/operator-intent.js +69 -42
  223. package/dist/src/operator/operator-requests.js +35 -40
  224. package/dist/src/operator/retire.js +5 -6
  225. package/dist/src/operator/revert.js +142 -34
  226. package/dist/src/orchestrator/orchestrator-launch.js +60 -52
  227. package/dist/src/orchestrator/orchestrator-poll.js +320 -0
  228. package/dist/src/orchestrator/orchestrator-scheduling.js +68 -369
  229. package/dist/src/orchestrator/orchestrator.js +128 -369
  230. package/dist/src/orchestrator/retention.js +42 -8
  231. package/dist/src/orchestrator/scheduling-admit.js +116 -0
  232. package/dist/src/orchestrator/scheduling-scope.js +201 -0
  233. package/dist/src/orchestrator/scheduling-skip.js +185 -0
  234. package/dist/src/orchestrator/startup-migration-steps.js +8 -7
  235. package/dist/src/orchestrator/startup-migrations.js +11 -7
  236. package/dist/src/paths.js +116 -49
  237. package/dist/src/pi/command-shape.js +12 -1
  238. package/dist/src/pi/pi-args.js +6 -4
  239. package/dist/src/pi/pi-backend.js +6 -6
  240. package/dist/src/pi/pi-bin.js +54 -63
  241. package/dist/src/pi/pi-listed-models.js +53 -0
  242. package/dist/src/pi/pi-models.js +63 -30
  243. package/dist/src/pi/pi-run-result.js +1 -1
  244. package/dist/src/pi/pi-settings.js +1 -1
  245. package/dist/src/pi/pi-stream.js +6 -6
  246. package/dist/src/pi/pi-watchdogs.js +18 -19
  247. package/dist/src/pi-extension/bounded-output.js +5 -3
  248. package/dist/src/pi-extension/context-budget.js +2 -2
  249. package/dist/src/pi-extension/context-shake.js +1 -1
  250. package/dist/src/pi-extension/context-window.js +0 -1
  251. package/dist/src/pi-extension/full-output.js +3 -10
  252. package/dist/src/pi-extension/strip-thinking.js +0 -1
  253. package/dist/src/pi-extension/trim-system-prompt.js +3 -4
  254. package/dist/src/pi-extension/write-guard.js +37 -0
  255. package/dist/src/process/launch-services.js +1 -2
  256. package/dist/src/process/node-bin.js +16 -0
  257. package/dist/src/process/process-group.js +8 -18
  258. package/dist/src/process/process.js +41 -2
  259. package/dist/src/process/run-marker.js +18 -6
  260. package/dist/src/process/supervisor.js +22 -21
  261. package/dist/src/prompt/dialect.js +17 -2
  262. package/dist/src/prompt/gate-notes.js +203 -0
  263. package/dist/src/prompt/gate-prompts.js +121 -211
  264. package/dist/src/prompt/notes-backlog.js +181 -0
  265. package/dist/src/prompt/principles.js +1 -2
  266. package/dist/src/prompt/prompt-followup.js +19 -14
  267. package/dist/src/prompt/prompt.js +129 -204
  268. package/dist/src/redeploy/redeploy-policy.js +24 -19
  269. package/dist/src/redeploy/redeploy-probes.js +1 -1
  270. package/dist/src/redeploy/redeploy.js +2 -3
  271. package/dist/src/redeploy/redeployer.js +20 -20
  272. package/dist/src/redeploy/self-reload.js +2 -1
  273. package/dist/src/report/report-data.js +161 -77
  274. package/dist/src/report/report-fold.js +129 -52
  275. package/dist/src/report/report-main-red.js +55 -0
  276. package/dist/src/report/report-render.js +133 -64
  277. package/dist/src/report/report-types.js +1 -0
  278. package/dist/src/report/report.js +26 -9
  279. package/dist/src/request-timeouts.js +2 -2
  280. package/dist/src/review/known-flakes.js +100 -1
  281. package/dist/src/review/post-merge-review.js +106 -0
  282. package/dist/src/review/protected-edits.js +4 -5
  283. package/dist/src/review/review-followup.js +8 -8
  284. package/dist/src/review/review-precheck.js +66 -23
  285. package/dist/src/review/review-run.js +395 -0
  286. package/dist/src/review/review-verdict.js +2 -2
  287. package/dist/src/review/review.js +155 -360
  288. package/dist/src/review/secret-leaks.js +36 -16
  289. package/dist/src/review/suite-rerun.js +0 -1
  290. package/dist/src/roles/legacy-role-migration.js +80 -7
  291. package/dist/src/roles/role-catalog.js +69 -12
  292. package/dist/src/roles/role-guidance.js +55 -20
  293. package/dist/src/roles/role-labels.js +1 -2
  294. package/dist/src/roles/role-render.js +67 -50
  295. package/dist/src/roles/role-view.js +59 -47
  296. package/dist/src/roles/roles.js +31 -31
  297. package/dist/src/roles/runner-ids.js +97 -0
  298. package/dist/src/run/acceptance-greps.js +119 -0
  299. package/dist/src/run/feedback-filings.js +51 -0
  300. package/dist/src/{tick → run}/qa-coverage.js +9 -27
  301. package/dist/src/{tick/tick-apply.js → run/run-apply.js} +95 -94
  302. package/dist/src/run/run-detail-data.js +179 -0
  303. package/dist/src/run/run-detail-render.js +98 -0
  304. package/dist/src/run/run-detail.js +168 -0
  305. package/dist/src/{tick/tick-finalize.js → run/run-finalize.js} +72 -60
  306. package/dist/src/{tick/tick-outcome.js → run/run-outcome.js} +6 -7
  307. package/dist/src/{tick/tick-prompt.js → run/run-prompt.js} +108 -73
  308. package/dist/src/{tick/tick-resume.js → run/run-resume.js} +55 -26
  309. package/dist/src/{tick/tick-stage.js → run/run-stage.js} +108 -50
  310. package/dist/src/{tick/tick-timing.js → run/run-timing.js} +43 -41
  311. package/dist/src/{tick/tick-usage.js → run/run-usage.js} +45 -37
  312. package/dist/src/{tick/tick-verdict.js → run/run-verdict.js} +91 -56
  313. package/dist/src/run/selfcheck-digest.js +40 -0
  314. package/dist/src/run/stage-backlog-checks.js +249 -0
  315. package/dist/src/run/stage-check.js +172 -0
  316. package/dist/src/run/stage-path-checks.js +730 -0
  317. package/dist/src/{loop → runner}/leftover.js +80 -25
  318. package/dist/src/{loop → runner}/model-fallback.js +12 -12
  319. package/dist/src/runner/revision.js +202 -0
  320. package/dist/src/{loop/loop-fallback.js → runner/runner-fallback.js} +8 -8
  321. package/dist/src/runner/runner-outcome.js +107 -0
  322. package/dist/src/{loop/loop-pi.js → runner/runner-pi.js} +140 -131
  323. package/dist/src/{loop/loop-recovery.js → runner/runner-recovery.js} +21 -15
  324. package/dist/src/{loop/loop-tick.js → runner/runner-run.js} +218 -240
  325. package/dist/src/{loop/loop-state.js → runner/runner-state-load.js} +61 -114
  326. package/dist/src/runner/runner-state.js +102 -0
  327. package/dist/src/{loop/loop.js → runner/runner.js} +123 -120
  328. package/dist/src/scheduling/backoff.js +38 -38
  329. package/dist/src/scheduling/claims.js +35 -22
  330. package/dist/src/scheduling/once-round.js +22 -22
  331. package/dist/src/scheduling/quiet-hours.js +5 -6
  332. package/dist/src/scheduling/scheduling.js +74 -64
  333. package/dist/src/scheduling/work-merged-cache.js +2 -2
  334. package/dist/src/status/status-budget.js +55 -47
  335. package/dist/src/status/status-data.js +43 -16
  336. package/dist/src/status/status-inbox.js +2 -2
  337. package/dist/src/status/status-install.js +0 -1
  338. package/dist/src/status/status-polls.js +28 -28
  339. package/dist/src/status/status-roles.js +39 -13
  340. package/dist/src/team/error-storm.js +8 -8
  341. package/dist/src/team/failure-spread.js +2 -2
  342. package/dist/src/team/orchestrator-info.js +15 -9
  343. package/dist/src/team/reclaim.js +5 -6
  344. package/dist/src/team/team-hold.js +7 -7
  345. package/dist/src/team/team-polls.js +91 -50
  346. package/dist/src/team/team-state.js +8 -2
  347. package/dist/src/text/datetime.js +63 -17
  348. package/dist/src/text/format.js +48 -11
  349. package/dist/src/text/markdown.js +8 -5
  350. package/dist/src/text/phrases.js +57 -23
  351. package/dist/src/text/suggest.js +1 -1
  352. package/dist/src/text/text.js +67 -13
  353. package/dist/src/ui/badges.js +65 -37
  354. package/dist/src/ui/gui/gui-client-backlog.js +292 -0
  355. package/dist/src/ui/gui/gui-client-boot.js +32 -26
  356. package/dist/src/ui/gui/gui-client-composer.js +8 -9
  357. package/dist/src/ui/gui/gui-client-drawer-config.js +212 -0
  358. package/dist/src/ui/gui/gui-client-drawer.js +126 -285
  359. package/dist/src/ui/gui/gui-client-feed.js +271 -0
  360. package/dist/src/ui/gui/gui-client-history.js +434 -67
  361. package/dist/src/ui/gui/gui-client-labels.js +1 -2
  362. package/dist/src/ui/gui/gui-client-markdown.js +1 -2
  363. package/dist/src/ui/gui/gui-client-model.js +27 -36
  364. package/dist/src/ui/gui/gui-client-operator.js +55 -51
  365. package/dist/src/ui/gui/gui-client-pending.js +227 -23
  366. package/dist/src/ui/gui/gui-client-report-chart.js +464 -0
  367. package/dist/src/ui/gui/gui-client-report.js +87 -427
  368. package/dist/src/ui/gui/{gui-client-loops.js → gui-client-roles.js} +43 -44
  369. package/dist/src/ui/gui/gui-client-settings.js +56 -4
  370. package/dist/src/ui/gui/gui-client-team.js +25 -465
  371. package/dist/src/ui/gui/gui-client.js +27 -25
  372. package/dist/src/ui/gui/gui-icons.js +0 -2
  373. package/dist/src/ui/gui/gui-page.js +9 -7
  374. package/dist/src/ui/gui/gui-styles.js +34 -34
  375. package/dist/src/ui/progress-data.js +9 -11
  376. package/dist/src/ui/{tick-progress-model.js → run-progress-model.js} +31 -31
  377. package/dist/src/ui/status-model.js +133 -125
  378. package/dist/src/ui/status-payload.js +136 -49
  379. package/dist/src/ui/status-render.js +101 -82
  380. package/dist/src/ui/team-alerts.js +248 -57
  381. package/dist/src/ui/tone.js +5 -6
  382. package/dist/src/ui/transcript-run.js +58 -0
  383. package/dist/src/ui/transcript-tail.js +28 -20
  384. package/dist/src/ui/transcript.js +63 -23
  385. package/dist/src/ui/tui/tui-app.js +0 -1
  386. package/dist/src/ui/tui/tui-backlog.js +51 -14
  387. package/dist/src/ui/tui/tui-editor-answer.js +27 -31
  388. package/dist/src/ui/tui/tui-editor-attach.js +3 -5
  389. package/dist/src/ui/tui/tui-editor-backends.js +132 -0
  390. package/dist/src/ui/tui/tui-editor-brief.js +10 -12
  391. package/dist/src/ui/tui/tui-editor-budget.js +35 -31
  392. package/dist/src/ui/tui/tui-editor-claude-reserve.js +24 -0
  393. package/dist/src/ui/tui/tui-editor-concurrency.js +24 -0
  394. package/dist/src/ui/tui/tui-editor-config.js +86 -0
  395. package/dist/src/ui/tui/tui-editor-entry.js +36 -47
  396. package/dist/src/ui/tui/tui-editor-fallback.js +31 -0
  397. package/dist/src/ui/tui/tui-editor-instances.js +37 -0
  398. package/dist/src/ui/tui/tui-editor-instructions.js +136 -0
  399. package/dist/src/ui/tui/tui-editor-interval.js +11 -9
  400. package/dist/src/ui/tui/tui-editor-model.js +29 -0
  401. package/dist/src/ui/tui/tui-editor-notes.js +3 -5
  402. package/dist/src/ui/tui/tui-editor-notify.js +25 -0
  403. package/dist/src/ui/tui/tui-editor-pause.js +48 -59
  404. package/dist/src/ui/tui/tui-editor-prompt.js +11 -16
  405. package/dist/src/ui/tui/tui-editor-provider-caps.js +27 -0
  406. package/dist/src/ui/tui/tui-editor-queued.js +5 -9
  407. package/dist/src/ui/tui/tui-editor-quiet.js +99 -35
  408. package/dist/src/ui/tui/tui-editor-retire.js +4 -6
  409. package/dist/src/ui/tui/tui-editor-role-key.js +76 -0
  410. package/dist/src/ui/tui/tui-editor-role-model.js +28 -0
  411. package/dist/src/ui/tui/tui-editor-search.js +45 -33
  412. package/dist/src/ui/tui/tui-editor-session.js +60 -34
  413. package/dist/src/ui/tui/tui-editor-slots.js +23 -0
  414. package/dist/src/ui/tui/tui-editors.js +35 -2
  415. package/dist/src/ui/tui/tui-frame.js +56 -11
  416. package/dist/src/ui/tui/tui-history.js +8 -9
  417. package/dist/src/ui/tui/tui-input.js +13 -11
  418. package/dist/src/ui/tui/tui-keymap.js +0 -1
  419. package/dist/src/ui/tui/tui-keys.js +24 -9
  420. package/dist/src/ui/tui/tui-pane-activity.js +14 -11
  421. package/dist/src/ui/tui/tui-pane-backlog.js +171 -126
  422. package/dist/src/ui/tui/tui-pane-brief.js +0 -1
  423. package/dist/src/ui/tui/tui-pane-ctx.js +2 -2
  424. package/dist/src/ui/tui/tui-pane-health.js +58 -8
  425. package/dist/src/ui/tui/tui-pane-history.js +402 -82
  426. package/dist/src/ui/tui/tui-pane-pending.js +308 -13
  427. package/dist/src/ui/tui/tui-pane-queued.js +71 -55
  428. package/dist/src/ui/tui/tui-pane-report.js +46 -14
  429. package/dist/src/ui/tui/tui-pane-session.js +25 -6
  430. package/dist/src/ui/tui/tui-pane-transcript.js +111 -37
  431. package/dist/src/ui/tui/tui-pane.js +80 -23
  432. package/dist/src/ui/tui/tui-panes.js +100 -30
  433. package/dist/src/ui/tui/tui.js +14 -14
  434. package/dist/src/verdict/fix-claim.js +82 -32
  435. package/dist/src/verdict/no-change.js +4 -4
  436. package/dist/src/verdict/refusal.js +40 -16
  437. package/dist/src/verdict/reply-contract.js +42 -31
  438. package/dist/src/version.js +0 -1
  439. package/package.json +2 -1
  440. package/dist/src/loop/revision.js +0 -70
  441. package/dist/src/roles/loop-ids.js +0 -77
  442. package/dist/src/tick/stage-check.js +0 -419
  443. package/dist/src/tick/telemetry-digest.js +0 -25
  444. package/dist/src/tick/tick-detail-data.js +0 -108
  445. package/dist/src/tick/tick-detail.js +0 -163
  446. package/dist/src/ui/gui/gui-client-sound.js +0 -53
@@ -0,0 +1,301 @@
1
+ /**
2
+ * The dashboard's GET run/event-log drill-down handlers (src/gui/gui-server.ts routes to them):
3
+ * transcript, history, logs, run, and role. Each answers its request and touches no socket
4
+ * beyond its own `res`; the domain work lives one layer down (ui/transcript.ts,
5
+ * history/history-data.ts, run/run-detail-data.ts, events/event-read.ts), so this module only
6
+ * adapts HTTP onto it. Barrelled into gui/gui-endpoints.ts beside the other per-family GET
7
+ * endpoint modules.
8
+ */
9
+ import { humanSeconds } from "../text/datetime.js";
10
+ import { readTranscript } from "../ui/transcript.js";
11
+ import { HISTORY_DEFAULT_RUNS, HISTORY_MAX_RUNS, HISTORY_RESULT_FLAG_NAMES, historyClass, parseHistoryResultClass, readRunRows, readRunRowsSince, summarizeRunRows, summarizeRunRowsByModel, runRowsMatchingGrep, runRowsOnModels, } from "../history/history-data.js";
12
+ import { LOGS_SINCE_MAX_MS, SPARSE_WINDOW_NOTE, readEventsSince } from "../events/event-window.js";
13
+ import { eventRole, readEvents } from "../events/event-read.js";
14
+ import { eventFeedItem, eventMatchesGrep } from "../events/event-format.js";
15
+ import { readRunDetail, runCommitSha, runMergedSha } from "../run/run-detail-data.js";
16
+ import { renderRunDetail, runDetailPatch, runDetailStat, runTranscriptLines } from "../run/run-detail-render.js";
17
+ import { resolveRevertTarget } from "../operator/revert.js";
18
+ import { rolePayload } from "../roles/role-view.js";
19
+ import { clampCount, intQuery, rejectBadRole, sinceMsQuery, untilMsQuery } from "./gui-args.js";
20
+ import { badRequest, sendJson } from "./http-body.js";
21
+ import { typoSuffix } from "../text/suggest.js";
22
+ import { gotSuffix, splitCommaList, splitCommaListUnique } from "../text/text.js";
23
+ import { newestRunRow, runNotFoundLastMessage, runNotFoundMessage } from "../run/run-detail.js";
24
+ /** Handle GET /api/transcript?role=<id>&n=N[&grep=<text>]: rendered transcript lines for a role's
25
+ * pi log (same rendering as `tumwater logs --transcript <id>`). A present `prompt` flag also
26
+ * renders each agent run's prompt, the text `logs --transcript <id> --prompt` prints.
27
+ * Unknown/missing role
28
+ * or a bad n → 400. `grep` keeps only the entries containing the text, case-insensitively, each
29
+ * under its run's separator (the `logs --transcript <id> --grep` rule); `n` bounds the entries
30
+ * scanned.
31
+ * User-defined roles are valid targets too — the GUI marks them with an asterisk, so clicking
32
+ * one must open its transcript: ids validate through rejectBadRole. The 400 message lists
33
+ * exactly the ids accepted. The query arrives pre-parsed — the server parses the target
34
+ * once (gui/gui-server.ts's parseRequestTarget) and threads it down, so the parse idiom lives
35
+ * in one place and cannot disagree with the routing or the token gate about what the URL said. */
36
+ export function handleTranscript(q, res, root) {
37
+ const role = q.get("role");
38
+ if (rejectBadRole(root, res, role))
39
+ return;
40
+ const n = intQuery(q, res, "n", "positive", 50);
41
+ if (n === null)
42
+ return;
43
+ const grep = (q.get("grep") ?? "").trim().toLowerCase();
44
+ const includePrompts = q.get("prompt") !== null;
45
+ sendJson(res, 200, {
46
+ lines: readTranscript(root, role, n, { includePrompts, grep: grep === "" ? undefined : grep }),
47
+ });
48
+ }
49
+ /** Parse `result=<class>[,...]` for /api/run?last and /api/history: the deduplicated classes in
50
+ * first-seen order (`[]` filters nothing), or null after sending the 400 for an unknown class. */
51
+ function resultClassesQuery(q, res) {
52
+ const classes = [];
53
+ const names = Object.values(HISTORY_RESULT_FLAG_NAMES);
54
+ for (const piece of splitCommaList(q.get("result") ?? "")) {
55
+ const cls = parseHistoryResultClass(piece);
56
+ if (cls === null) {
57
+ badRequest(res, `result must be one of ${names.join(", ")}${gotSuffix(piece)}${typoSuffix(piece, names)}`);
58
+ return null;
59
+ }
60
+ if (!classes.includes(cls))
61
+ classes.push(cls);
62
+ }
63
+ return classes;
64
+ }
65
+ /** Handle GET /api/history?role=<id>&n=N[&grep=<text>] — or ?role=<id>&sinceMs=<ms>[&untilMs=<ms>]
66
+ * for a trailing window (untilMs ends it that long ago, via untilMsQuery; without sinceMs → 400):
67
+ * the per-run rows the CLI's `tumwater history` derives from the event log,
68
+ * the dashboard's history tab renders them. The count shape ({ rows }) rides readRunRows (the
69
+ * shared scan, window growth included, so a role-filtered ask on a busy team cannot silently
70
+ * come back short); n is optional: absent → HISTORY_DEFAULT_RUNS; present but not a plain
71
+ * non-negative integer → 400 (the handleBacklog index discipline — an explicit count that does
72
+ * not parse is a client error, unlike the report's degrade-to-default typo rule); then clamped
73
+ * to [1, HISTORY_MAX_RUNS] — the GUI clamps where the CLI fails fast, its established
74
+ * convention. grep is optional: absent or empty filters nothing; present → the rows are
75
+ * filtered through history-data.ts's shared runRowsMatchingGrep rule, the same
76
+ * case-insensitive haystack rule `tumwater history --grep` uses (shared, not
77
+ * re-implemented), after the n/role scan so n bounds the scanned window and not the
78
+ * printed rows — the CLI's rule. The window shape ({ rows, covered, note }) rides
79
+ * readRunRowsSince, the collector behind `history --since`,
80
+ * so the browser and the CLI cannot disagree about the window's rows; a simultaneous n is a
81
+ * rival shape → 400 (the CLI's count-vs-window rule), sinceMs parses and is capped through
82
+ * sinceMsQuery (gui-args.ts, LOGS_SINCE_MAX_MS — the shared `history --since` cap), and note is
83
+ * SPARSE_WINDOW_NOTE when covered is false, else "". role is optional: absent or empty → all
84
+ * roles; present → passed straight through as the role filter with no config validation (a
85
+ * filter is not a target — an id with no runs legitimately yields zero rows), so the endpoint
86
+ * reads nothing but files, like /api/report and /api/failures. Reads files directly, so it
87
+ * present → passed straight through as the role filter with no config validation (a
88
+ * filter is not a target — an id with no runs legitimately yields zero rows), so the endpoint
89
+ * reads nothing but files, like /api/report and /api/failures. summary is optional: absent /
90
+ * empty adds nothing (the response is byte-for-byte today's); present (any value — the flag's
91
+ * mere presence, a bad value ignored like a missing one) adds an additive `summary` folding the
92
+ * exact served rows through the shared summarizeRunRows collector, the same tally
93
+ * `tumwater history --summary` prints, so the dashboard Summary and the table cannot disagree
94
+ * about what a run was. It also adds `modelSummary`, `{ models, total }`, the
95
+ * `history --summary --by model` tally of the same rows. `model=<selector>[,...]` keeps only the
96
+ * runs that started on exactly one of those models (the `history --model` rule), after `grep` and
97
+ * before the summaries fold. `result=<class>[,...]` keeps only the runs in those outcome classes
98
+ * (the `history --result` rule, parsed by resultClassesQuery like /api/run's), after `grep` and
99
+ * before `model`; an unknown class is a 400. Reads files directly, so it works whether
100
+ * or not the team is running. */
101
+ export function handleHistory(q, res, root) {
102
+ const role = q.get("role") || null; // "" and absent both read all roles
103
+ const grep = q.get("grep") || null; // "" and absent both filter nothing
104
+ // `model` is a comma list of exact selectors (trimmed, deduplicated); "", absent and a list
105
+ // with no non-empty piece all filter nothing, and an unknown selector keeps no rows.
106
+ const classes = resultClassesQuery(q, res);
107
+ if (classes === null)
108
+ return;
109
+ const inClasses = (rows) => (classes.length ? rows.filter((r) => classes.includes(historyClass(r.result))) : rows);
110
+ const modelPieces = splitCommaListUnique(q.get("model") ?? "");
111
+ const onModels = (rows) => (modelPieces.length ? runRowsOnModels(rows, modelPieces) : rows);
112
+ const summary = q.get("summary") !== null; // the flag's presence, whatever its value
113
+ if (q.get("sinceMs") !== null) {
114
+ if (q.get("n") !== null) {
115
+ badRequest(res, "sinceMs cannot be combined with n (a count and a window are rival shapes)");
116
+ return;
117
+ }
118
+ const sinceMs = sinceMsQuery(q, res, LOGS_SINCE_MAX_MS, (ms) => `sinceMs is capped at ${humanSeconds(LOGS_SINCE_MAX_MS / 1000)} (got ${ms})`);
119
+ if (sinceMs === null)
120
+ return;
121
+ const untilMs = untilMsQuery(q, res, sinceMs);
122
+ if (untilMs === null)
123
+ return;
124
+ const { rows, covered } = readRunRowsSince(root, sinceMs, role, untilMs ?? null);
125
+ const matched = onModels(inClasses(grep === null ? rows : runRowsMatchingGrep(rows, grep)));
126
+ sendJson(res, 200, { rows: matched, covered, note: covered ? "" : SPARSE_WINDOW_NOTE, ...(summary ? { summary: summarizeRunRows(matched), modelSummary: summarizeRunRowsByModel(matched) } : {}) });
127
+ return;
128
+ }
129
+ if (untilMsQuery(q, res, undefined) === null)
130
+ return; // a count has no window end to move
131
+ const parsed = intQuery(q, res, "n", "non-negative", HISTORY_DEFAULT_RUNS);
132
+ if (parsed === null)
133
+ return;
134
+ const n = clampCount(parsed, HISTORY_MAX_RUNS);
135
+ let rows = readRunRows(root, n, role);
136
+ if (grep !== null)
137
+ rows = runRowsMatchingGrep(rows, grep);
138
+ rows = onModels(inClasses(rows));
139
+ sendJson(res, 200, summary ? { rows, summary: summarizeRunRows(rows), modelSummary: summarizeRunRowsByModel(rows) } : { rows });
140
+ }
141
+ /** How many of the log's newest events `GET /api/logs` scans by default, and the most it will
142
+ * scan however large `n` asks: the CLI's `-n` shape (n bounds the scanned tail, not the printed
143
+ * rows), sized so the dashboard's search can reach well past the RECENT_EVENTS window the
144
+ * status poll carries. */
145
+ const LOGS_SEARCH_EVENTS = 500;
146
+ const LOGS_SEARCH_MAX_EVENTS = 5000;
147
+ /** Handle GET /api/logs?n=N|sinceMs=S[&untilMs=U][&grep=<text>][&role=<id>]: a slice of the raw
148
+ * event log scanned from its newest end, filtered with the CLI's own rules and shaped as the
149
+ * activity feed's items. `n` is optional (absent → LOGS_SEARCH_EVENTS) and clamps to [1,
150
+ * LOGS_SEARCH_MAX_EVENTS] after intQuery's non-negative parse (an explicit count that does not
151
+ * parse is a 400, the handleHistory discipline); it bounds the scanned tail, so a matching event
152
+ * older than the last n events is absent — the same scan-then-filter rule `tumwater logs -n N
153
+ * --grep` applies. grep (absent or empty filters nothing) matches through eventMatchesGrep, and
154
+ * role is a filter, not a target: an id with no events legitimately yields an empty list, so it
155
+ * does not validate through rejectBadRole (the handleHistory rule). The matched events answer
156
+ * oldest-first as eventFeedItem objects — the same shape the status payload's eventItems carry.
157
+ * Reads the tail through readEvents' stat-keyed cache (no covered byte end, unlike the CLI's
158
+ * follow), so a dashboard search that repeats on an unchanged log — the debounced filter fires
159
+ * one per filter change — costs one stat instead of re-reading and re-parsing the scanned tail.
160
+ * With `sinceMs` instead of `n` it serves the trailing window's events (readEventsSince, the
161
+ * `logs --since` reader) as { events, covered, note }, the /api/history window shape: sinceMs
162
+ * with n is a 400 (rival shapes), sinceMs parses and is capped through sinceMsQuery at
163
+ * LOGS_SINCE_MAX_MS, the window is not row-capped, and note is SPARSE_WINDOW_NOTE when the log
164
+ * does not reach back that far; untilMs (untilMsQuery) drops the events newer than now −
165
+ * untilMs, and without sinceMs it is a 400.
166
+ * Reads files directly, so it works whether or not the team is running. */
167
+ export function handleLogs(q, res, root) {
168
+ const role = q.get("role") || null; // "" and absent both read all roles
169
+ const grep = q.get("grep") || null; // "" and absent both filter nothing
170
+ const needle = grep === null ? null : grep.toLowerCase();
171
+ const keep = (e) => (role === null || eventRole(e) === role) && (needle === null || eventMatchesGrep(e, needle));
172
+ if (q.get("sinceMs") !== null) {
173
+ if (q.get("n") !== null) {
174
+ badRequest(res, "sinceMs cannot be combined with n (a count and a window are rival shapes)");
175
+ return;
176
+ }
177
+ const sinceMs = sinceMsQuery(q, res, LOGS_SINCE_MAX_MS, (ms) => `sinceMs is capped at ${humanSeconds(LOGS_SINCE_MAX_MS / 1000)} (got ${ms})`);
178
+ if (sinceMs === null)
179
+ return;
180
+ const untilMs = untilMsQuery(q, res, sinceMs);
181
+ if (untilMs === null)
182
+ return;
183
+ const { cutoff, events: windowEvents, covered } = readEventsSince(root, sinceMs);
184
+ const events = untilMs === undefined ? windowEvents : windowEvents.filter((e) => e.ts < cutoff + sinceMs - untilMs);
185
+ sendJson(res, 200, {
186
+ events: events.filter(keep).map(eventFeedItem),
187
+ covered,
188
+ note: covered ? "" : SPARSE_WINDOW_NOTE,
189
+ });
190
+ return;
191
+ }
192
+ if (untilMsQuery(q, res, undefined) === null)
193
+ return; // a count has no window end to move
194
+ const parsed = intQuery(q, res, "n", "non-negative", LOGS_SEARCH_EVENTS);
195
+ if (parsed === null)
196
+ return;
197
+ const n = clampCount(parsed, LOGS_SEARCH_MAX_EVENTS);
198
+ sendJson(res, 200, { events: readEvents(root, n).filter(keep).map(eventFeedItem) });
199
+ }
200
+ /** handleRun's `transcript` text: the run's transcript lines joined, else the miss note. */
201
+ function transcriptText(root, role, run, detail, includePrompts, grep) {
202
+ const { found, note } = runTranscriptLines(root, role, run, detail, { includePrompts, grep });
203
+ return found.length > 0 ? found.join("\n") : note;
204
+ }
205
+ /** Handle GET /api/run?role=<id>&run=N: one run's full event trail — readRunDetail's
206
+ * RunDetail, the collector `tumwater history <role> <n>` prints from, plus `text`: the same
207
+ * payload through renderRunDetail, pre-rendered server-side like /api/transcript's lines so
208
+ * the browser shows the CLI's exact rendering instead of re-implementing the event formatting.
209
+ * Serving the shared collector is the point: the CLI and this endpoint cannot drift on what one
210
+ * run's block contains. The role is a target here, not a filter, so it validates through
211
+ * rejectBadRole (unknown/missing → 400 naming the valid ids); the run is an explicit count
212
+ * through intQuery (missing or not a positive integer → 400); and a run the scanned window
213
+ * does not hold — never ran, or rotation ate it — answers 404 with runNotFoundMessage, the
214
+ * CLI's exact not-found wording,
215
+ * never a crash. `revertSha` is the run's landed commit's full sha when it is on the base branch
216
+ * of a primary checkout that is on it (resolveRevertTarget, the revert cores' own check), else
217
+ * null — the drill-down offers its revert button only when it is set. `hasCommit` says whether
218
+ * the run pinned a commit (runCommitSha), for the "Show patch" button; `patch` — the same text
219
+ * `history --patch` prints — is added only when the `patch` flag is present, whatever its
220
+ * value, so the default expand costs no extra `git show`. `transcript`, likewise flag-gated, is
221
+ * the text `history --transcript` prints (runTranscriptLines: the found lines joined, else its
222
+ * note), so the default expand reads no pi log; a present `prompt` flag also renders each agent
223
+ * run's prompt, the text `history --transcript --prompt` prints. A non-blank `grep` (with
224
+ * `transcript`) keeps only the entries containing the text, case-insensitively, each under its
225
+ * agent run's separator, the text `history --transcript --grep` prints, or its miss note. Reads
226
+ * files directly, so it works whether or not the team is running.
227
+ *
228
+ * A present `last` flag replaces `run` with the role's newest completed run, the one
229
+ * `tumwater history <role> --last` opens (newestRunRow, shared): `result` (a comma list of
230
+ * outcome classes) and `model` (a comma list of selectors) narrow it to the newest run in those
231
+ * classes or on those models, an absent or empty value filtering nothing. `last` with `run`, and
232
+ * `result`/`model` without `last`, are 400s; a class that is not one is a 400 worded like the
233
+ * CLI's; no match answers 404 with runNotFoundLastMessage. The payload is the numbered form's,
234
+ * the resolved number in `run`. With no `role`, `last` opens the newest completed run of any role
235
+ * (newestRunRow with a null role, the run bare `tumwater history --last` opens), and the
236
+ * payload's `role` is that run's; `result`/`model` narrow it the same way, and a miss is a 404
237
+ * worded for any role. */
238
+ export async function handleRun(q, res, root) {
239
+ let role = q.get("role");
240
+ const bareLast = role === null && q.get("last") !== null;
241
+ if (!bareLast && rejectBadRole(root, res, role))
242
+ return;
243
+ let run;
244
+ if (q.get("last") === null) {
245
+ if (q.get("result") !== null)
246
+ return badRequest(res, "result needs last");
247
+ if (q.get("model") !== null)
248
+ return badRequest(res, "model needs last");
249
+ run = intQuery(q, res, "run", "positive");
250
+ if (run === null)
251
+ return;
252
+ }
253
+ else {
254
+ if (q.get("run") !== null)
255
+ return badRequest(res, "last cannot be combined with run (a number and the newest run are rival selectors)");
256
+ const classes = resultClassesQuery(q, res);
257
+ if (classes === null)
258
+ return;
259
+ const models = splitCommaListUnique(q.get("model") ?? "");
260
+ const classFilter = classes.length ? classes : null;
261
+ const modelFilter = models.length ? models : null;
262
+ const row = newestRunRow(root, role, classFilter, modelFilter);
263
+ if (row !== null)
264
+ role = row.role;
265
+ run = row?.run ?? null;
266
+ if (run === null) {
267
+ sendJson(res, 404, { error: runNotFoundLastMessage(role, classFilter, modelFilter) });
268
+ return;
269
+ }
270
+ }
271
+ const detail = readRunDetail(root, role, run);
272
+ if (detail === null) {
273
+ sendJson(res, 404, { error: runNotFoundMessage(role, run) });
274
+ return;
275
+ }
276
+ const grep = (q.get("grep") ?? "").trim();
277
+ const mergedAs = runMergedSha(detail);
278
+ const target = mergedAs === null ? null : await resolveRevertTarget(root, mergedAs);
279
+ sendJson(res, 200, {
280
+ ...detail,
281
+ text: renderRunDetail(detail),
282
+ stat: await runDetailStat(root, detail),
283
+ revertSha: target?.ok ? target.full : null,
284
+ hasCommit: runCommitSha(detail) !== null,
285
+ ...(q.get("patch") !== null ? { patch: await runDetailPatch(root, detail) } : {}),
286
+ ...(q.get("transcript") !== null ? { transcript: transcriptText(root, role, run, detail, q.get("prompt") !== null, grep === "" ? undefined : grep) } : {}),
287
+ });
288
+ }
289
+ /** Handle GET /api/role?role=<id>: one role's inspection payload — rolePayload's object,
290
+ * the same collection behind `tumwater role <id>` — so the drawer's Notes and Next run
291
+ * prompt sections render what the CLI shows without re-implementing the assembly. The role
292
+ * is a target here, so it validates through rejectBadRole exactly like /api/transcript and
293
+ * /api/run (unknown/missing → the shared 400 naming the valid ids and correcting typos);
294
+ * rolePayload is only reached with an id rejectBadRole accepted, so its own unknown-role
295
+ * throw never fires. Reads files directly, so it works whether or not the team is running. */
296
+ export function handleRole(q, res, root) {
297
+ const role = q.get("role");
298
+ if (rejectBadRole(root, res, role))
299
+ return;
300
+ sendJson(res, 200, rolePayload(root, role));
301
+ }
@@ -1,19 +1,28 @@
1
- // vocabulary: plain
2
1
  /**
3
2
  * The dashboard's POST operator-lifecycle command handlers (barrelled into
4
3
  * gui/gui-endpoint-commands.ts, which routes to them from src/gui/gui-server.ts): reset-counters,
5
- * restart, reclaim, abort, stop-schedule, role-notes, retire, and brief-edit — every POST
6
- * handler that is not prompt/backlog/config/team. Each handler adapts HTTP onto the domain work
4
+ * restart, reclaim, abort, stop-schedule, role-notes, retire, revert, brief-edit, and
5
+ * queue-cancel — every POST handler that is not prompt/backlog/config/team. Each handler
6
+ * adapts HTTP onto the domain work
7
7
  * one layer down (operator-intent, role-notes-write, brief-edit) and touches no socket beyond
8
8
  * its own `res`.
9
9
  */
10
10
  import { cancelScheduledStopMessage, requestAbort, requestResetCounters, requestRestart, requestScheduledStop, runManualReclaim } from "../operator/operator-intent.js";
11
+ import { runDoctor } from "../doctor/doctor.js";
12
+ import { killOrphans, orphanKillLines } from "../doctor/doctor-orphans.js";
11
13
  import { saveRoleNote } from "../agent/role-notes-write.js";
12
14
  import { roleNotesPath } from "../paths.js";
13
15
  import { DIRECTOR_ROLE } from "../roles/roles.js";
14
16
  import { artifactPhrase, retireRole } from "../operator/retire.js";
15
- import { editInitialPrompt } from "../brief/brief-edit.js";
16
- import { validRoleIds } from "./gui-args.js";
17
+ import { previewRevert, revertFollowUp, revertMergedCommit } from "../operator/revert.js";
18
+ import { cancelQueuedMergeBySha } from "../merge/merge-cancel.js";
19
+ import { readMergeQueue } from "../merge/merge-queue.js";
20
+ import { mergeChanges } from "../merge/merge-slot.js";
21
+ import { queueInFlight } from "../status/status-polls.js";
22
+ import { orchestratorAlive } from "../team/orchestrator-info.js";
23
+ import { shortSha } from "../text/format.js";
24
+ import { editBriefText } from "../brief/brief-edit.js";
25
+ import { requireStringField, validRoleIds } from "./gui-args.js";
17
26
  import { badRequest, sendJson } from "./http-body.js";
18
27
  import { errorMessage, gotSuffix } from "../text/text.js";
19
28
  import { readPostBody, readRoleBody, sendRequestResult } from "./gui-endpoint-common.js";
@@ -77,6 +86,27 @@ export async function handleReclaim(req, res, root) {
77
86
  return;
78
87
  sendJson(res, 200, { ok: true, message: await runManualReclaim(root) });
79
88
  }
89
+ /** Handle POST /api/kill-orphans: the dashboard Health card's Kill orphans button — the same
90
+ * killOrphans `tumwater doctor --kill-orphans` runs, answering its lines joined with "; ". Same
91
+ * body discipline as /api/reclaim; `ok` is false when the scan failed or a signal was refused. */
92
+ export async function handleKillOrphans(req, res, root, kill = killOrphans) {
93
+ const body = await readPostBody(req, res, "{}");
94
+ if (!body)
95
+ return;
96
+ const report = await kill(root);
97
+ sendJson(res, 200, { ok: !("error" in report) && report.failed.length === 0, message: orphanKillLines(report).join("; ") });
98
+ }
99
+ /** Handle POST /api/doctor-reprobe: the dashboard Health card's Re-probe models button, the
100
+ * twin of `tumwater doctor --reprobe`. A POST because it spends one model call per claimed pi
101
+ * image-input model and rewrites `.tumwater/state/image-probes.json`; GET /api/doctor stays
102
+ * probe-free. Same body discipline as /api/kill-orphans; `run` takes the probe mode so a test
103
+ * can pin that the handler asks for "all" without spawning a probe. */
104
+ export async function handleDoctorReprobe(req, res, root, run = (r, p) => runDoctor(r, undefined, undefined, p)) {
105
+ const body = await readPostBody(req, res, "{}");
106
+ if (!body)
107
+ return;
108
+ sendJson(res, 200, await run(root, "all"));
109
+ }
80
110
  /** Handle POST /api/abort: the dashboard's per-row abort control — the same marker-writing
81
111
  * core `tumwater abort` calls (requestAbort). The role is required and validated like
82
112
  * /api/transcript; requestAbort's not-live error comes back 409 — the marker is valid but
@@ -88,7 +118,7 @@ export async function handleAbort(req, res, root) {
88
118
  return;
89
119
  sendRequestResult(res, requestAbort(root, body.role));
90
120
  }
91
- /** Handle POST /api/role-notes: the loop drawer's notebook Edit/Clear — the same saveRoleNote
121
+ /** Handle POST /api/role-notes: the role drawer's notes Edit/Clear — the same saveRoleNote
92
122
  * `tumwater role <id> --notes-file` / `--clear-notes` call, so the 4 KB cap and its wording
93
123
  * cannot drift. `"text": ""` clears. The director has no notes (400, the CLI's wording); a
94
124
  * non-string text is a 400 before anything is written; an over-cap note is a 400 carrying
@@ -100,10 +130,11 @@ export async function handleRoleNotes(req, res, root) {
100
130
  const role = body.role;
101
131
  if (role === DIRECTOR_ROLE)
102
132
  return badRequest(res, "the director has no notes");
103
- if (typeof body.text !== "string")
104
- return badRequest(res, `"text" must be a string — the full replacement note, or "" to clear it${gotSuffix(body.text)}`);
133
+ const text = requireStringField(res, body, "text", `the full replacement note, or "" to clear it`);
134
+ if (text === null)
135
+ return;
105
136
  try {
106
- sendJson(res, 200, { ok: true, message: saveRoleNote(roleNotesPath(root, role), body.text) });
137
+ sendJson(res, 200, { ok: true, message: saveRoleNote(roleNotesPath(root, role), text) });
107
138
  }
108
139
  catch (err) {
109
140
  badRequest(res, errorMessage(err));
@@ -129,16 +160,80 @@ export async function handleRetire(req, res, root) {
129
160
  sendJson(res, 409, { error: errorMessage(err) });
130
161
  }
131
162
  }
132
- /** Handle POST /api/brief: replace the initial prompt through editInitialPrompt — the same
163
+ /** Handle POST /api/revert: the History drill-down's "Revert this change" button — the same
164
+ * cores `tumwater revert <sha> [--dry-run]` calls. A `sha` that is not 4-64 hex digits (so never
165
+ * a flag or a ref name) or a non-boolean `dryRun` is a 400 and runs nothing. `dryRun: true`
166
+ * answers `{ ok, preview }` (previewRevert, writing nothing); otherwise the revert commits and
167
+ * answers `{ ok, headSha, beforeSha, message }`. A core's refusal is a 409 with its wording. */
168
+ export async function handleRevert(req, res, root) {
169
+ const body = await readPostBody(req, res, '{"sha": "1a2b3c4"}');
170
+ if (!body)
171
+ return;
172
+ if (typeof body.sha !== "string" || !/^[0-9a-f]{4,64}$/i.test(body.sha))
173
+ return badRequest(res, `"sha" must be a hex commit id of 4-64 digits${gotSuffix(body.sha)}`);
174
+ if (body.dryRun !== undefined && typeof body.dryRun !== "boolean")
175
+ return badRequest(res, `"dryRun" must be true or false${gotSuffix(body.dryRun)}`);
176
+ if (body.dryRun === true) {
177
+ const preview = await previewRevert(root, body.sha);
178
+ if (!preview.ok)
179
+ return sendJson(res, 409, { error: preview.error });
180
+ return sendJson(res, 200, { ok: true, preview });
181
+ }
182
+ const result = await revertMergedCommit(root, body.sha);
183
+ if (!result.ok)
184
+ return sendJson(res, 409, { error: result.error });
185
+ const followUp = revertFollowUp(result.subject);
186
+ sendJson(res, 200, {
187
+ ok: true,
188
+ headSha: result.headSha,
189
+ beforeSha: result.beforeSha,
190
+ subject: result.subject,
191
+ message: `main is now ${shortSha(result.headSha)}${followUp === null ? "" : ` — ${followUp}`}`,
192
+ });
193
+ }
194
+ /** Handle POST /api/queue-cancel: the Merge queue card row's two-click Cancel — the browser
195
+ * twin of `tumwater queue --cancel <n>`, resolving by pinned sha (the same removal
196
+ * `cancelQueuedMergeBySha` the CLI's position variant shares). A `sha` that is not 4-64 hex
197
+ * digits is a 400 and runs nothing. A sha a live merge marker names mid-merge is a 409 with the
198
+ * wait-then-revert wording and changes nothing; a sha no longer queued (a concurrent drain
199
+ * raced the click) is a 200 `{ status: "gone" }` data, not an error. Otherwise it answers the
200
+ * cancelled entry's role/commit/summary and removes the queue file and pin. */
201
+ export async function handleQueueCancel(req, res, root) {
202
+ const body = await readPostBody(req, res, '{"sha": "1a2b3c4"}');
203
+ if (!body)
204
+ return;
205
+ if (typeof body.sha !== "string" || !/^[0-9a-f]{4,64}$/i.test(body.sha))
206
+ return badRequest(res, `"sha" must be a hex commit id of 4-64 digits${gotSuffix(body.sha)}`);
207
+ const view = readMergeQueue(root);
208
+ const sha = body.sha.toLowerCase();
209
+ const inFlight = queueInFlight(root, view.entries, orchestratorAlive(root));
210
+ if (inFlight && ((typeof inFlight.sha === "string" && inFlight.sha.toLowerCase() === sha) || mergeChanges(inFlight).some((c) => c.sha.toLowerCase() === sha)))
211
+ return sendJson(res, 409, { error: `this merge is being merged right now — wait for it to land, then \`tumwater revert ${shortSha(body.sha)}\`` });
212
+ const outcome = await cancelQueuedMergeBySha(root, body.sha);
213
+ if (outcome.status === "gone")
214
+ return sendJson(res, 200, { ok: true, status: "gone" });
215
+ if (outcome.status !== "cancelled")
216
+ return;
217
+ sendJson(res, 200, {
218
+ ok: true,
219
+ status: "cancelled",
220
+ role: outcome.role,
221
+ commit: outcome.sha,
222
+ summary: outcome.summary,
223
+ message: `cancelled the merge of ${shortSha(outcome.sha)} — ${outcome.summary}`,
224
+ });
225
+ }
226
+ /** Handle POST /api/brief: replace the brief text through editBriefText — the same
133
227
  * checks, locked write, and commit as `tumwater brief --file`. A non-string prompt or any
134
228
  * refusal is a 400 and writes nothing. */
135
229
  export async function handleBriefEdit(req, res, root) {
136
230
  const body = await readPostBody(req, res, '{"prompt": "…"}');
137
231
  if (!body)
138
232
  return;
139
- if (typeof body.prompt !== "string")
140
- return badRequest(res, `"prompt" must be a string — the full replacement brief${gotSuffix(body.prompt)}`);
141
- const outcome = await editInitialPrompt(root, body.prompt);
233
+ const prompt = requireStringField(res, body, "prompt", "the full replacement brief");
234
+ if (prompt === null)
235
+ return;
236
+ const outcome = await editBriefText(root, prompt);
142
237
  if (!outcome.ok)
143
238
  return badRequest(res, outcome.error);
144
239
  sendJson(res, 200, { file: outcome.file, changed: outcome.changed, committed: outcome.committed });
@@ -1,4 +1,3 @@
1
- // vocabulary: plain
2
1
  /**
3
2
  * The dashboard's POST prompt-queueing command handlers (barrelled into
4
3
  * gui/gui-endpoint-commands.ts, which routes to them from src/gui/gui-server.ts): prompt,
@@ -19,7 +18,7 @@ import { PAUSE_FOR_MAX_MS, submitRolePromptAndWake } from "../operator/operator-
19
18
  import { DIRECTOR_ROLE } from "../roles/roles.js";
20
19
  import { rejectBadQueueFileName, rejectBadRole, requirePromptText } from "./gui-args.js";
21
20
  import { badRequest, OK_GONE, sendJson } from "./http-body.js";
22
- import { errorMessage, gotSuffix } from "../text/text.js";
21
+ import { errorMessage, gotSuffix, joinParts } from "../text/text.js";
23
22
  import { readPostBody, readRoleBody } from "./gui-endpoint-common.js";
24
23
  /** Handle POST /api/prompt: queue a director prompt. An over-long prompt
25
24
  * (DIRECTOR_PROMPT_MAX_CHARS) is a user-input error, not a server fault: the shared length
@@ -93,10 +92,10 @@ function checkedPromptFields(res, body, role = DIRECTOR_ROLE) {
93
92
  return null;
94
93
  return { text, images, notBeforeMs };
95
94
  }
96
- /** Handle POST /api/prompt-role: queue a prompt for one loop — the dashboard's per-row
95
+ /** Handle POST /api/prompt-role: queue a prompt for one role — the dashboard's per-row
97
96
  * prompt affordance submits here, calling the same path `tumwater prompt --role <id>` uses
98
- * (submitRolePrompt enqueues into that loop's own queue and logs under it, then a single-role
99
- * wake brings the live loop in within one poll; the wake message rides back for the flash).
97
+ * (submitRolePrompt enqueues into that role's own queue and logs under it, then a single-role
98
+ * wake brings the live role in within one poll; the wake message rides back for the flash).
100
99
  * The role validates exactly like /api/transcript (the shared rejectBadRole wording), and the
101
100
  * body discipline is /api/prompt's: readJsonObject → 400 malformed/non-object, 413 oversized,
102
101
  * text a non-empty string within the shared length rule → 400 otherwise — plus the same
@@ -110,9 +109,9 @@ export async function handlePromptRole(req, res, root) {
110
109
  if (!fields)
111
110
  return;
112
111
  // Image-input submit gate (plans/image-input.md "Attachments", part 5/5): an attachment the
113
- // target loop cannot read (and that no tier reroutes) answers 400 naming the fix before
112
+ // target role cannot read (and that no tier reroutes) answers 400 naming the fix before
114
113
  // anything is queued — the same gate submitRolePrompt enforces, surfaced as a user-input
115
- // error rather than the server's 500. A loop that reroutes a blind read is not refused; its
114
+ // error rather than the server's 500. A role that reroutes a blind read is not refused; its
116
115
  // notice rides in the confirmation the composer flashes.
117
116
  let notice = "";
118
117
  if (fields.images && fields.images.length > 0) {
@@ -123,12 +122,10 @@ export async function handlePromptRole(req, res, root) {
123
122
  }
124
123
  notice = guidance.notice ?? "";
125
124
  }
126
- const message = [
125
+ const message = joinParts([
127
126
  submitRolePromptAndWake(root, role, fields.text, fields.images, fields.notBeforeMs),
128
127
  notice,
129
- ]
130
- .filter(Boolean)
131
- .join("\n");
128
+ ], "\n");
132
129
  sendJson(res, 200, { ok: true, message });
133
130
  }
134
131
  /** Handle POST /api/prompt-cancel: retract one queued prompt addressed by its queue file —
@@ -136,7 +133,7 @@ export async function handlePromptRole(req, res, root) {
136
133
  * position-addressed `prompt --cancel`. The role is optional (absent cancels from the
137
134
  * director's queue) and validates exactly like /api/transcript; a file name failing the
138
135
  * shared rejectBadQueueFileName guard is a user-input error answered 400 with
139
- * nothing touched on disk. A vanished file answers 200 { status: "gone" } — the loop already
136
+ * nothing touched on disk. A vanished file answers 200 { status: "gone" } — the role already
140
137
  * dequeued the prompt, which is data for the flash line, not a server fault. Same body
141
138
  * discipline as /api/prompt (readJsonObject → 400 malformed/non-object, 413 oversized). */
142
139
  export async function handlePromptCancel(req, res, root) {
@@ -161,7 +158,7 @@ export async function handlePromptCancel(req, res, root) {
161
158
  * and images validate through the same checkedPromptFields /api/prompt and /api/prompt-role
162
159
  * use; editQueuedFile throws for a bad name or an over-cap text — both user-input errors
163
160
  * answered 400 (a filesystem failure reads as 400 like handleConfigSet's failed write). A
164
- * vanished file answers 200 { ok: true, status: "gone" } — the loop already dequeued the
161
+ * vanished file answers 200 { ok: true, status: "gone" } — the role already dequeued the
165
162
  * prompt, which is data for the flash line, not a server error; an edit answers 200 with the
166
163
  * old and new operator-facing text the client flashes. Same body discipline as /api/prompt
167
164
  * (readJsonObject → 400 malformed/non-object, 413 oversized). */
@@ -179,7 +176,7 @@ export async function handlePromptEdit(req, res, root) {
179
176
  return;
180
177
  // Image-input gate (plans/image-input.md "Attachments", part 5/5), the edit twin of
181
178
  // handlePromptRole's: an image edited into the queued prompt answers 400 naming the fix
182
- // when the target loop cannot read it (and no tier reroutes it) — the same gate
179
+ // when the target role cannot read it (and no tier reroutes it) — the same gate
183
180
  // submitRolePrompt enforces — before anything is written.
184
181
  if (fields.images && fields.images.length > 0) {
185
182
  const guidance = imagePromptGuidance(root, role);
@@ -0,0 +1,90 @@
1
+ /**
2
+ * The dashboard's GET report and failures handlers (src/gui/gui-server.ts routes to them):
3
+ * report and failures. Each answers its request and touches no socket beyond its own `res`;
4
+ * the domain work lives one layer down (report/report-data.ts, failure/failure-data.ts), so
5
+ * this module only adapts HTTP onto it. Barrelled into gui/gui-endpoints.ts beside the other
6
+ * per-family GET endpoint modules.
7
+ */
8
+ import { collectReport, collectReportSince } from "../report/report-data.js";
9
+ import { collectFailureReport, collectFailureReportSince } from "../failure/failure-data.js";
10
+ import { renderFailureMarkdown } from "../failure/failure-render.js";
11
+ import { renderSinceReportMarkdown } from "../report/report-render.js";
12
+ import { REPORT_SINCE_MAX_MS, reportSinceMsg } from "../events/event-window.js";
13
+ import { roleListQuery, sinceMsQuery, untilMsQuery, windowDays } from "./gui-args.js";
14
+ import { badRequest, sendJson } from "./http-body.js";
15
+ /** Parse and validate the role-scoped, sinceMs-or-days query the report and failures endpoints
16
+ * both take: the `?role=` filter, a comma-separated list (each id trimmed, deduplicated, and
17
+ * validated by rejectBadRole — an unknown id or an empty list → 400 naming it) and, when
18
+ * `?sinceMs=` is present, the trailing-window length (reportSinceMs, which 400s a simultaneous
19
+ * `?days=`). Returns null once a 400 is sent. `sinceMs` is defined exactly when the caller
20
+ * should use the since shape; otherwise it falls through to the days shape with `windowDays(q)`.
21
+ * The one home of that shared scaffolding, so the two endpoints cannot drift on the role rule or
22
+ * the days-vs-sinceMs mutual exclusion (the server twin of reportEndpoint on the GUI client). */
23
+ function reportScope(q, res, root) {
24
+ const roles = roleListQuery(q, res, root);
25
+ if (roles === null)
26
+ return null;
27
+ if (q.get("sinceMs") !== null) {
28
+ const sinceMs = reportSinceMs(q, res);
29
+ if (sinceMs === null)
30
+ return null;
31
+ return { roles, sinceMs };
32
+ }
33
+ return { roles };
34
+ }
35
+ /** Parse and bound the report endpoints' `?sinceMs=` trailing-window length: a simultaneous
36
+ * `?days=` is the CLI's rival shape (whole local days vs a trailing window) → 400 naming both;
37
+ * otherwise the length rides sinceMsQuery (gui-args.ts) — positive-integer parse, then a 400
38
+ * naming the cap rather than a silent clamp (an explicit window typo must not shrink to some
39
+ * other window) — with the "must be between 1 and …" wording. Returns the parsed length, or
40
+ * null once the 400 is sent. */
41
+ function reportSinceMs(q, res) {
42
+ if (q.get("days") !== null) {
43
+ badRequest(res, "days and sinceMs are mutually exclusive; days counts whole local days, sinceMs totals a trailing window");
44
+ return null;
45
+ }
46
+ return sinceMsQuery(q, res, REPORT_SINCE_MAX_MS, reportSinceMsg);
47
+ }
48
+ /** Handle GET /api/report?days=N[&role=<id>[,<id>...]] or
49
+ * /api/report?sinceMs=<ms>[&untilMs=<ms>][&role=<id>[,<id>...]]: the
50
+ * usage report — the day series (collectReport's ReportData) as JSON the dashboard's report tab
51
+ * charts, or, when sinceMs is set, the trailing-window totals rendered through
52
+ * renderSinceReportMarkdown as { markdown } (the CLI's `report --since` shape). The day window
53
+ * follows windowDays. An absent role is the team-wide report; the listed roles, summed
54
+ * (each id validated by reportScope). Reads files directly, so it works whether or not the team
55
+ * is running. */
56
+ export function handleReport(q, res, root) {
57
+ const scope = reportScope(q, res, root);
58
+ if (scope === null)
59
+ return;
60
+ const untilMs = untilMsQuery(q, res, scope.sinceMs);
61
+ if (untilMs === null)
62
+ return;
63
+ if (scope.sinceMs !== undefined) {
64
+ sendJson(res, 200, { markdown: renderSinceReportMarkdown(collectReportSince(root, scope.sinceMs, scope.roles, untilMs)) });
65
+ return;
66
+ }
67
+ sendJson(res, 200, collectReport(root, windowDays(q), scope.roles));
68
+ }
69
+ /** Handle GET /api/failures?days=N[&role=<id>[,<id>...]] or
70
+ * /api/failures?sinceMs=<ms>[&untilMs=<ms>][&role=<id>[,<id>...]]: the
71
+ * same bounded Markdown failure digest the selfcheck role feeds on and `tumwater report
72
+ * --failures` prints, as JSON ({ markdown }) — the dashboard's failures tab renders it; with
73
+ * sinceMs it is the trailing-window digest (collectFailureReportSince) through the same render,
74
+ * the CLI's `report --failures --since` shape. The days window follows windowDays (so it can
75
+ * never drift from /api/report's). role rides the same rule as
76
+ * handleReport: absent → the team digest, present → the validated listed roles, summed. Reads files
77
+ * directly, so it works with no team running. */
78
+ export function handleFailures(q, res, root) {
79
+ const scope = reportScope(q, res, root);
80
+ if (scope === null)
81
+ return;
82
+ const untilMs = untilMsQuery(q, res, scope.sinceMs);
83
+ if (untilMs === null)
84
+ return;
85
+ if (scope.sinceMs !== undefined) {
86
+ sendJson(res, 200, { markdown: renderFailureMarkdown(collectFailureReportSince(root, scope.sinceMs, scope.roles, untilMs)) });
87
+ return;
88
+ }
89
+ sendJson(res, 200, { markdown: renderFailureMarkdown(collectFailureReport(root, windowDays(q), scope.roles)) });
90
+ }