tumwater 0.1.2 → 0.2.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 (361) hide show
  1. package/README.md +74 -108
  2. package/dist/build-info.json +2 -2
  3. package/dist/src/agent/agent-args.js +24 -0
  4. package/dist/src/agent/agent-backend.js +2 -2
  5. package/dist/src/agent/backend-traits.js +60 -0
  6. package/dist/src/agent/claude/claude-account.js +61 -8
  7. package/dist/src/agent/claude/claude-args.js +12 -15
  8. package/dist/src/agent/claude/claude-backend.js +51 -41
  9. package/dist/src/agent/claude/claude-env.js +7 -3
  10. package/dist/src/agent/claude/claude-stream.js +7 -16
  11. package/dist/src/agent/image-input.js +107 -0
  12. package/dist/src/agent/notes-shim.js +41 -0
  13. package/dist/src/agent/opencode/opencode-args.js +61 -0
  14. package/dist/src/agent/opencode/opencode-backend.js +255 -0
  15. package/dist/src/agent/opencode/opencode-catalog.js +184 -0
  16. package/dist/src/agent/opencode/opencode-config.js +188 -0
  17. package/dist/src/agent/opencode/opencode-env.js +36 -0
  18. package/dist/src/agent/opencode/opencode-session-id.js +37 -0
  19. package/dist/src/agent/opencode/opencode-stream.js +278 -0
  20. package/dist/src/agent/role-notes-write.js +16 -32
  21. package/dist/src/agent/run-agent.js +95 -18
  22. package/dist/src/agent/session-ledger.js +57 -1
  23. package/dist/src/backlog/backlog-archive.js +296 -0
  24. package/dist/src/backlog/backlog-cli.js +573 -0
  25. package/dist/src/backlog/backlog-commit.js +31 -0
  26. package/dist/src/backlog/backlog-eligibility.js +357 -33
  27. package/dist/src/backlog/backlog-md.js +98 -18
  28. package/dist/src/backlog/backlog-render.js +62 -11
  29. package/dist/src/backlog/backlog-structure.js +261 -58
  30. package/dist/src/backlog/backlog-write.js +197 -319
  31. package/dist/src/backlog/backlog.js +113 -16
  32. package/dist/src/backlog/question-answer.js +339 -0
  33. package/dist/src/baseline/main-baseline.js +75 -88
  34. package/dist/src/baseline/main-red.js +38 -30
  35. package/dist/src/brief/brief-edit.js +43 -0
  36. package/dist/src/{brief.js → brief/brief.js} +66 -10
  37. package/dist/src/budget/budget.js +32 -15
  38. package/dist/src/budget/fallback-breaker.js +8 -7
  39. package/dist/src/build/build-check-detect.js +8 -7
  40. package/dist/src/build/build-check-events.js +68 -22
  41. package/dist/src/build/build-check-report.js +22 -23
  42. package/dist/src/build/build-check-scoped.js +51 -62
  43. package/dist/src/build/build-check.js +15 -14
  44. package/dist/src/build/build-info.js +2 -1
  45. package/dist/src/build/build-stage.js +23 -22
  46. package/dist/src/build/dep-install.js +22 -21
  47. package/dist/src/build/host-sleep.js +1 -1
  48. package/dist/src/change/change-data.js +25 -14
  49. package/dist/src/change/change-render.js +11 -10
  50. package/dist/src/cli/brief-command.js +58 -0
  51. package/dist/src/cli/cli-args.js +25 -5
  52. package/dist/src/cli/cli-command-args.js +46 -14
  53. package/dist/src/cli/cli-editor.js +64 -0
  54. package/dist/src/cli/cli-flag-specs.js +54 -16
  55. package/dist/src/cli/cli-marker-commands.js +13 -10
  56. package/dist/src/cli/cli-output.js +9 -0
  57. package/dist/src/cli/cli-query-commands.js +458 -49
  58. package/dist/src/cli/cli-repo-gate.js +1 -1
  59. package/dist/src/cli/cli-run.js +208 -59
  60. package/dist/src/cli/completion.js +207 -0
  61. package/dist/src/cli/config-commands.js +14 -11
  62. package/dist/src/cli/help.js +305 -110
  63. package/dist/src/cli/log-commands.js +127 -21
  64. package/dist/src/cli/notify-command.js +33 -18
  65. package/dist/src/cli/question-commands.js +29 -224
  66. package/dist/src/cli.js +121 -40
  67. package/dist/src/collections.js +66 -16
  68. package/dist/src/concurrency/check-permit.js +16 -17
  69. package/dist/src/concurrency/lock.js +6 -5
  70. package/dist/src/concurrency/provider-lease.js +69 -0
  71. package/dist/src/concurrency/semaphore.js +258 -36
  72. package/dist/src/config/config-aliases.js +106 -0
  73. package/dist/src/config/config-editable-keys.js +3 -3
  74. package/dist/src/config/config-fallbacks.js +379 -0
  75. package/dist/src/config/config-field-checks.js +19 -10
  76. package/dist/src/config/config-keys.js +160 -0
  77. package/dist/src/config/config-live.js +64 -12
  78. package/dist/src/config/config-schema.js +34 -22
  79. package/dist/src/config/config-validation.js +132 -48
  80. package/dist/src/config/config-views.js +119 -337
  81. package/dist/src/config/config-write.js +138 -177
  82. package/dist/src/config/config.js +60 -49
  83. package/dist/src/config/model-selector.js +28 -2
  84. package/dist/src/doctor/doctor-backlog.js +546 -49
  85. package/dist/src/doctor/doctor-checks.js +534 -56
  86. package/dist/src/doctor/doctor-holds.js +193 -53
  87. package/dist/src/doctor/doctor-install.js +30 -0
  88. package/dist/src/doctor/doctor-launch-services.js +2 -2
  89. package/dist/src/doctor/doctor-model-checks.js +549 -157
  90. package/dist/src/doctor/doctor-notify.js +27 -0
  91. package/dist/src/doctor/doctor-orphans.js +10 -9
  92. package/dist/src/doctor/doctor-render.js +36 -9
  93. package/dist/src/doctor/doctor.js +80 -22
  94. package/dist/src/doctor/image-probe.js +244 -0
  95. package/dist/src/doctor/probe.js +64 -0
  96. package/dist/src/events/event-format.js +226 -102
  97. package/dist/src/events/event-labels.js +116 -0
  98. package/dist/src/events/event-legacy.js +140 -0
  99. package/dist/src/events/event-read.js +58 -22
  100. package/dist/src/events/event-window.js +27 -7
  101. package/dist/src/events/events.js +18 -14
  102. package/dist/src/events/notify.js +137 -38
  103. package/dist/src/failure/failure-cluster.js +12 -9
  104. package/dist/src/failure/failure-data.js +96 -48
  105. package/dist/src/failure/failure-render.js +69 -58
  106. package/dist/src/failure/failure-state-change.js +33 -26
  107. package/dist/src/failure/time-spend.js +79 -59
  108. package/dist/src/files/file-queue.js +16 -4
  109. package/dist/src/files/files.js +18 -6
  110. package/dist/src/files/json-object.js +18 -3
  111. package/dist/src/files/tail.js +1 -1
  112. package/dist/src/gates/bootstrap-gates.js +5 -4
  113. package/dist/src/gates/budget-gates.js +250 -34
  114. package/dist/src/gates/disk-gate.js +18 -18
  115. package/dist/src/gates/gate-polls.js +117 -69
  116. package/dist/src/gates/maintenance-quota.js +29 -29
  117. package/dist/src/gates/pause-gates.js +11 -11
  118. package/dist/src/gates/readiness.js +2 -1
  119. package/dist/src/gates/role-cap-gates.js +16 -21
  120. package/dist/src/gates/startup-gate.js +75 -31
  121. package/dist/src/gates/streak-gate.js +18 -16
  122. package/dist/src/git/commit-message.js +12 -5
  123. package/dist/src/git/git-diff.js +16 -16
  124. package/dist/src/git/git-run.js +8 -8
  125. package/dist/src/git/git.js +86 -55
  126. package/dist/src/git/slots-state.js +14 -5
  127. package/dist/src/git/worktree-pool.js +25 -18
  128. package/dist/src/git/worktree-use.js +15 -14
  129. package/dist/src/git/worktree.js +18 -18
  130. package/dist/src/gui/gui-args.js +10 -14
  131. package/dist/src/gui/gui-backlog-commands.js +273 -0
  132. package/dist/src/gui/gui-command.js +1 -1
  133. package/dist/src/gui/gui-config-commands.js +440 -0
  134. package/dist/src/gui/gui-endpoint-commands.js +16 -669
  135. package/dist/src/gui/gui-endpoint-common.js +49 -0
  136. package/dist/src/gui/gui-endpoints.js +116 -56
  137. package/dist/src/gui/gui-operator-commands.js +145 -0
  138. package/dist/src/gui/gui-prompt-commands.js +234 -0
  139. package/dist/src/gui/gui-server.js +28 -8
  140. package/dist/src/gui/gui-team-commands.js +164 -0
  141. package/dist/src/gui/http-body.js +17 -1
  142. package/dist/src/history/history-data.js +194 -98
  143. package/dist/src/history/history-render.js +76 -0
  144. package/dist/src/history/history.js +37 -58
  145. package/dist/src/inbox/inbox-attachments.js +75 -14
  146. package/dist/src/inbox/inbox-cancel.js +7 -5
  147. package/dist/src/inbox/inbox-edit.js +1 -1
  148. package/dist/src/inbox/inbox-submit.js +44 -4
  149. package/dist/src/inbox/inbox-top.js +83 -0
  150. package/dist/src/inbox/inbox.js +8 -2
  151. package/dist/src/inbox/pending-prompt.js +1 -1
  152. package/dist/src/inbox/prompt-commands.js +170 -80
  153. package/dist/src/init/init-templates.js +16 -14
  154. package/dist/src/init/init.js +12 -11
  155. package/dist/src/loop/leftover.js +28 -27
  156. package/dist/src/loop/loop-fallback.js +2 -2
  157. package/dist/src/loop/loop-pi.js +106 -52
  158. package/dist/src/loop/loop-recovery.js +15 -15
  159. package/dist/src/loop/loop-state.js +139 -13
  160. package/dist/src/loop/loop-tick.js +194 -86
  161. package/dist/src/loop/loop.js +72 -36
  162. package/dist/src/loop/model-fallback.js +3 -2
  163. package/dist/src/loop/revision.js +16 -7
  164. package/dist/src/merge/backlog-conflicts.js +155 -0
  165. package/dist/src/{landing/landing-batch.js → merge/merge-batch.js} +90 -89
  166. package/dist/src/{landing/landing-check-failures.js → merge/merge-check-failures.js} +87 -41
  167. package/dist/src/{landing/landing-core.js → merge/merge-core.js} +119 -95
  168. package/dist/src/{landing/landing-diff.js → merge/merge-diff.js} +1 -1
  169. package/dist/src/merge/merge-drain.js +212 -0
  170. package/dist/src/{landing/landing-git.js → merge/merge-git.js} +94 -21
  171. package/dist/src/merge/merge-pipeline.js +99 -0
  172. package/dist/src/{landing/landing-questions.js → merge/merge-questions.js} +9 -6
  173. package/dist/src/merge/merge-queue-render.js +75 -0
  174. package/dist/src/{landing/landing-queue.js → merge/merge-queue.js} +95 -36
  175. package/dist/src/merge/merge-slot.js +340 -0
  176. package/dist/src/{landing/landing-stack.js → merge/merge-stack.js} +66 -62
  177. package/dist/src/{landing/landing-merge.js → merge/merge-to-main.js} +72 -62
  178. package/dist/src/merge/merge-verify.js +212 -0
  179. package/dist/src/notes-cli.js +97 -0
  180. package/dist/src/operator/operator-commands.js +154 -47
  181. package/dist/src/operator/operator-intent.js +87 -49
  182. package/dist/src/operator/operator-requests.js +52 -52
  183. package/dist/src/operator/retire.js +62 -27
  184. package/dist/src/operator/revert.js +71 -0
  185. package/dist/src/orchestrator/orchestrator-launch.js +74 -14
  186. package/dist/src/orchestrator/orchestrator-scheduling.js +129 -44
  187. package/dist/src/orchestrator/orchestrator.js +158 -100
  188. package/dist/src/orchestrator/retention.js +29 -12
  189. package/dist/src/orchestrator/startup-migration-steps.js +51 -0
  190. package/dist/src/orchestrator/startup-migrations.js +40 -0
  191. package/dist/src/paths.js +128 -37
  192. package/dist/src/pi/pi-args.js +26 -3
  193. package/dist/src/pi/pi-backend.js +15 -23
  194. package/dist/src/pi/pi-bin.js +18 -12
  195. package/dist/src/pi/pi-event-line.js +17 -1
  196. package/dist/src/pi/pi-failure.js +87 -0
  197. package/dist/src/pi/pi-models.js +126 -38
  198. package/dist/src/pi/pi-run-result.js +1 -1
  199. package/dist/src/pi/pi-settings.js +86 -0
  200. package/dist/src/pi/pi-stream.js +131 -107
  201. package/dist/src/pi/pi-watchdogs.js +47 -43
  202. package/dist/src/pi-extension/bounded-output.js +20 -5
  203. package/dist/src/pi-extension/context-budget.js +3 -1
  204. package/dist/src/pi-extension/context-shake.js +54 -21
  205. package/dist/src/pi-extension/context-window.js +37 -0
  206. package/dist/src/pi-extension/full-output.js +2 -2
  207. package/dist/src/pi-extension/strip-thinking.js +80 -0
  208. package/dist/src/pi-extension/trim-system-prompt.js +38 -0
  209. package/dist/src/process/launch-services.js +11 -10
  210. package/dist/src/process/process-table.js +1 -1
  211. package/dist/src/process/process.js +7 -7
  212. package/dist/src/process/run-marker.js +29 -6
  213. package/dist/src/process/supervisor.js +47 -25
  214. package/dist/src/prompt/dialect.js +30 -10
  215. package/dist/src/prompt/gate-prompts.js +170 -67
  216. package/dist/src/prompt/principles.js +1 -0
  217. package/dist/src/prompt/prompt-followup.js +35 -20
  218. package/dist/src/prompt/prompt.js +249 -91
  219. package/dist/src/redeploy/redeploy-policy.js +10 -10
  220. package/dist/src/redeploy/redeploy.js +6 -4
  221. package/dist/src/redeploy/redeployer.js +63 -55
  222. package/dist/src/redeploy/self-reload.js +1 -1
  223. package/dist/src/report/report-data.js +264 -255
  224. package/dist/src/report/report-fold.js +353 -0
  225. package/dist/src/report/report-render.js +270 -33
  226. package/dist/src/request-timeouts.js +1 -1
  227. package/dist/src/review/known-flakes.js +34 -14
  228. package/dist/src/review/protected-edits.js +33 -0
  229. package/dist/src/review/review-followup.js +16 -4
  230. package/dist/src/review/review-precheck.js +14 -13
  231. package/dist/src/review/review-verdict.js +14 -0
  232. package/dist/src/review/review.js +81 -39
  233. package/dist/src/review/secret-leaks.js +127 -0
  234. package/dist/src/review/suite-rerun.js +4 -3
  235. package/dist/src/roles/legacy-role-ids.js +21 -0
  236. package/dist/src/roles/legacy-role-migration.js +62 -0
  237. package/dist/src/roles/loop-ids.js +19 -2
  238. package/dist/src/roles/role-catalog.js +125 -135
  239. package/dist/src/roles/role-guidance.js +64 -31
  240. package/dist/src/roles/role-labels.js +10 -0
  241. package/dist/src/roles/role-render.js +194 -18
  242. package/dist/src/roles/role-view.js +216 -17
  243. package/dist/src/roles/roles.js +18 -15
  244. package/dist/src/scheduling/backoff.js +29 -19
  245. package/dist/src/scheduling/claims.js +25 -9
  246. package/dist/src/scheduling/once-round.js +11 -11
  247. package/dist/src/scheduling/quiet-hours.js +25 -32
  248. package/dist/src/scheduling/scheduling.js +43 -24
  249. package/dist/src/scheduling/{work-landed-cache.js → work-merged-cache.js} +14 -14
  250. package/dist/src/status/status-budget.js +115 -0
  251. package/dist/src/status/status-data.js +77 -184
  252. package/dist/src/status/status-inbox.js +47 -0
  253. package/dist/src/status/status-install.js +27 -0
  254. package/dist/src/status/status-polls.js +68 -33
  255. package/dist/src/status/status-roles.js +105 -0
  256. package/dist/src/{fleet → team}/error-storm.js +10 -10
  257. package/dist/src/{fleet → team}/failure-spread.js +8 -8
  258. package/dist/src/{fleet → team}/orchestrator-info.js +86 -18
  259. package/dist/src/{fleet → team}/reclaim.js +5 -4
  260. package/dist/src/{fleet/fleet-hold.js → team/team-hold.js} +55 -21
  261. package/dist/src/{fleet/fleet-polls.js → team/team-polls.js} +116 -65
  262. package/dist/src/{fleet/fleet-state.js → team/team-state.js} +62 -37
  263. package/dist/src/text/datetime.js +42 -2
  264. package/dist/src/text/format.js +10 -1
  265. package/dist/src/text/markdown.js +1 -1
  266. package/dist/src/text/phrases.js +35 -34
  267. package/dist/src/text/suggest.js +3 -2
  268. package/dist/src/text/text.js +2 -2
  269. package/dist/src/tick/qa-coverage.js +2 -1
  270. package/dist/src/tick/stage-check.js +147 -26
  271. package/dist/src/tick/telemetry-digest.js +10 -9
  272. package/dist/src/tick/tick-apply.js +101 -72
  273. package/dist/src/tick/tick-detail-data.js +32 -18
  274. package/dist/src/tick/tick-detail.js +62 -21
  275. package/dist/src/tick/tick-finalize.js +29 -22
  276. package/dist/src/tick/tick-outcome.js +2 -1
  277. package/dist/src/tick/tick-prompt.js +94 -26
  278. package/dist/src/tick/tick-resume.js +29 -13
  279. package/dist/src/tick/tick-stage.js +91 -47
  280. package/dist/src/tick/tick-timing.js +60 -59
  281. package/dist/src/tick/tick-usage.js +53 -23
  282. package/dist/src/tick/tick-verdict.js +76 -10
  283. package/dist/src/ui/badges.js +118 -67
  284. package/dist/src/ui/gui/gui-client-boot.js +209 -30
  285. package/dist/src/ui/gui/gui-client-composer.js +102 -23
  286. package/dist/src/ui/gui/gui-client-drawer.js +330 -75
  287. package/dist/src/ui/gui/gui-client-history.js +96 -38
  288. package/dist/src/ui/gui/gui-client-labels.js +24 -0
  289. package/dist/src/ui/gui/gui-client-loops.js +94 -52
  290. package/dist/src/ui/gui/gui-client-markdown.js +1 -0
  291. package/dist/src/ui/gui/gui-client-model.js +63 -43
  292. package/dist/src/ui/gui/gui-client-operator.js +129 -39
  293. package/dist/src/ui/gui/gui-client-pending.js +36 -32
  294. package/dist/src/ui/gui/gui-client-report.js +339 -46
  295. package/dist/src/ui/gui/gui-client-settings.js +148 -14
  296. package/dist/src/ui/gui/gui-client-sound.js +2 -1
  297. package/dist/src/ui/gui/{gui-client-fleet.js → gui-client-team.js} +196 -72
  298. package/dist/src/ui/gui/gui-client.js +26 -20
  299. package/dist/src/ui/gui/gui-page.js +19 -19
  300. package/dist/src/ui/gui/gui-styles.js +12 -4
  301. package/dist/src/ui/progress-data.js +5 -3
  302. package/dist/src/ui/status-model.js +184 -122
  303. package/dist/src/ui/status-payload.js +119 -32
  304. package/dist/src/ui/status-render.js +122 -52
  305. package/dist/src/ui/team-alerts.js +341 -0
  306. package/dist/src/ui/tick-progress-model.js +14 -14
  307. package/dist/src/ui/tone.js +11 -8
  308. package/dist/src/ui/transcript-tail.js +1 -0
  309. package/dist/src/ui/transcript.js +23 -16
  310. package/dist/src/ui/tui/tui-app.js +1 -0
  311. package/dist/src/ui/tui/tui-backlog.js +85 -11
  312. package/dist/src/ui/tui/tui-editor-answer.js +97 -0
  313. package/dist/src/ui/tui/tui-editor-attach.js +107 -0
  314. package/dist/src/ui/tui/tui-editor-brief.js +107 -0
  315. package/dist/src/ui/tui/tui-editor-budget.js +155 -0
  316. package/dist/src/ui/tui/tui-editor-ctx.js +1 -0
  317. package/dist/src/ui/tui/tui-editor-entry.js +230 -0
  318. package/dist/src/ui/tui/tui-editor-interval.js +122 -0
  319. package/dist/src/ui/tui/tui-editor-notes.js +114 -0
  320. package/dist/src/ui/tui/tui-editor-pause.js +266 -0
  321. package/dist/src/ui/tui/tui-editor-prompt.js +95 -0
  322. package/dist/src/ui/tui/tui-editor-queued.js +98 -0
  323. package/dist/src/ui/tui/tui-editor-quiet.js +106 -0
  324. package/dist/src/ui/tui/tui-editor-retire.js +83 -0
  325. package/dist/src/ui/tui/tui-editor-search.js +73 -0
  326. package/dist/src/ui/tui/tui-editor-session.js +97 -0
  327. package/dist/src/ui/tui/tui-editors.js +152 -0
  328. package/dist/src/ui/tui/tui-frame.js +67 -55
  329. package/dist/src/ui/tui/tui-history.js +12 -7
  330. package/dist/src/ui/tui/tui-input.js +49 -44
  331. package/dist/src/ui/tui/tui-keymap.js +1 -0
  332. package/dist/src/ui/tui/tui-keys.js +91 -1704
  333. package/dist/src/ui/tui/tui-pane-activity.js +99 -0
  334. package/dist/src/ui/tui/tui-pane-backlog.js +224 -0
  335. package/dist/src/ui/tui/tui-pane-brief.js +30 -0
  336. package/dist/src/ui/tui/tui-pane-ctx.js +11 -0
  337. package/dist/src/ui/tui/tui-pane-health.js +124 -0
  338. package/dist/src/ui/tui/tui-pane-history.js +316 -0
  339. package/dist/src/ui/tui/tui-pane-pending.js +74 -0
  340. package/dist/src/ui/tui/tui-pane-queued.js +143 -0
  341. package/dist/src/ui/tui/tui-pane-report.js +122 -0
  342. package/dist/src/ui/tui/tui-pane-session.js +101 -0
  343. package/dist/src/ui/tui/tui-pane-transcript.js +150 -0
  344. package/dist/src/ui/tui/tui-pane.js +66 -28
  345. package/dist/src/ui/tui/tui-panes.js +294 -0
  346. package/dist/src/ui/tui/tui.js +45 -25
  347. package/dist/src/verdict/fix-claim.js +141 -38
  348. package/dist/src/verdict/no-change.js +19 -0
  349. package/dist/src/verdict/refusal.js +92 -8
  350. package/dist/src/verdict/reply-contract.js +17 -5
  351. package/dist/src/version.js +3 -2
  352. package/package.json +4 -2
  353. package/dist/src/agent/mcp/role-notes-server.js +0 -160
  354. package/dist/src/landing/backlog-conflicts.js +0 -116
  355. package/dist/src/landing/landing-drain.js +0 -204
  356. package/dist/src/landing/landing-pipeline.js +0 -96
  357. package/dist/src/landing/landing-queue-render.js +0 -31
  358. package/dist/src/landing/landing-slot.js +0 -246
  359. package/dist/src/landing/landing-vetting.js +0 -196
  360. package/dist/src/pi-extension/role-notes.js +0 -40
  361. package/dist/src/ui/fleet-alerts.js +0 -174
@@ -0,0 +1,49 @@
1
+ /**
2
+ * The request-body helpers shared by the dashboard's per-domain POST command modules
3
+ * (gui-prompt-commands, gui-backlog-commands, gui-config-commands, gui-team-commands, and
4
+ * gui-operator-commands, all barrelled into gui/gui-endpoint-commands.ts): readPostBody /
5
+ * readRoleBody — the body-discipline-and-role-check pair each mutating handler guards its
6
+ * inputs with — and sendRequestResult, the shared boolean-claiming marker reply. The
7
+ * per-domain `checked*` validators live with their domain's module, not here.
8
+ */
9
+ import { rejectBadRole } from "./gui-args.js";
10
+ import { readJsonObject, sendJson } from "./http-body.js";
11
+ /** Every POST handler's shared opening: readJsonObject reads the body and — when it is
12
+ * oversized, malformed, or not a JSON object — has already written the 413/400 itself. Its
13
+ * null return therefore means the response is sent and the handler must stop without touching
14
+ * it again; each handler used to restate that contract in its own inline comment, and this is
15
+ * the one home for it. Handlers keep their own `if (!body) return` bail so the stop stays
16
+ * visible in their flow. */
17
+ export async function readPostBody(req, res, example) {
18
+ const body = await readJsonObject(req, res, example);
19
+ if (body === null)
20
+ return null; // 4xx already sent — oversized or not a JSON object
21
+ return body;
22
+ }
23
+ /** The role-targeting POST handlers' shared prologue: readPostBody (400/413 sent on a null
24
+ * return) plus rejectBadRole's shared role validation (400 sent on a null return), so the
25
+ * wake/abort/prompt-role/pause-role family states the body-discipline-and-role-check pairing
26
+ * once instead of two guard lines per handler. Its null return means the response is already
27
+ * written and the handler must stop; callers keep their own `if (!body) return` bail. Exactly
28
+ * seven call sites — handlePromptRole, handleWake, handleWakeCancel and handleResetCounters
29
+ * (allowMissing, the {} → all-roles default), handleAbort, handlePauseRole, and
30
+ * handleRoleNotes; handlePromptCancel deliberately does not ride it, since its role is
31
+ * optional and validates only when present. */
32
+ export async function readRoleBody(req, res, example, root, allowMissing = false) {
33
+ const body = await readPostBody(req, res, example);
34
+ if (body === null)
35
+ return null; // 4xx already sent — oversized, malformed, or not an object
36
+ if (rejectBadRole(root, res, body.role, allowMissing))
37
+ return null; // 400 already sent
38
+ return body;
39
+ }
40
+ /** Reply for a boolean-claiming marker request (RequestResult): success maps to 200 with its
41
+ * message, refusal to 409 — the request is valid but nothing can act on it, the same conflict
42
+ * reading requestAbort's not-live error uses. /api/restart and /api/abort share it. */
43
+ export function sendRequestResult(res, result) {
44
+ if (!result.ok) {
45
+ sendJson(res, 409, { error: result.error });
46
+ return;
47
+ }
48
+ sendJson(res, 200, { ok: true, message: result.message });
49
+ }
@@ -1,26 +1,42 @@
1
- import { openBugEntries, openQuestionEntries, plannedPlanEntries } from "../backlog/backlog.js";
1
+ /**
2
+ * The dashboard's GET data endpoint handlers (src/gui/gui-server.ts routes to them): transcript,
3
+ * prompt-text, backlog, report, failures, history, tick, diff, config — the read-only surface.
4
+ * The POST
5
+ * operator endpoints (prompt, prompt-role, prompt-cancel, prompt-edit, budget, config-set, pause,
6
+ * wake, restart, abort, pause-role) and their body-discipline helpers live in
7
+ * gui/gui-endpoint-commands.ts. Each handler answers its request and touches no socket beyond
8
+ * its own `res`; server lifecycle, routing, the static page, and the token gate stay in
9
+ * gui/gui-server.ts. The domain work itself lives one layer down (transcript.ts, backlog.ts,
10
+ * report-data.ts, src/failure/failure-data.ts, history-data.ts, tick-detail-data.ts, config.ts)
11
+ * — this module only adapts HTTP onto it.
12
+ */
13
+ import { refusalNote, waitingNote } from "../backlog/backlog-eligibility.js";
14
+ import { answeredQuestionEntries, donePlanEntries, fixedBugEntries, openBugEntries, openQuestionEntries, plannedPlanEntries } from "../backlog/backlog.js";
2
15
  import { runDoctor } from "../doctor/doctor.js";
3
- import { loadConfigSafe } from "../config/config.js";
16
+ import { knownRoleIds, loadConfigSafe } from "../config/config.js";
17
+ import { loopEnabled } from "../roles/loop-ids.js";
18
+ import { artifactPhrase, previewRetire } from "../operator/retire.js";
4
19
  import { EDITABLE_CONFIG_KEYS } from "../config/config-editable-keys.js";
5
20
  import { collectReport, collectReportSince } from "../report/report-data.js";
6
21
  import { collectFailureReport, collectFailureReportSince } from "../failure/failure-data.js";
7
22
  import { renderFailureMarkdown } from "../failure/failure-render.js";
8
23
  import { renderSinceReportMarkdown } from "../report/report-render.js";
9
- import { REPORT_SINCE_MAX_MS } from "../events/event-window.js";
24
+ import { REPORT_SINCE_MAX_MS, reportSinceMsg } from "../events/event-window.js";
10
25
  import { humanSeconds } from "../text/datetime.js";
11
- import { collectFleetChanges, collectRoleChange } from "../change/change-data.js";
26
+ import { collectTeamChanges, collectRoleChange } from "../change/change-data.js";
12
27
  import { readTranscript } from "../ui/transcript.js";
13
- import { HISTORY_DEFAULT_TICKS, HISTORY_MAX_TICKS, readTickRows, readTickRowsSince, tickRowsMatchingGrep } from "../history/history-data.js";
28
+ import { HISTORY_DEFAULT_TICKS, HISTORY_MAX_TICKS, readTickRows, readTickRowsSince, summarizeTickRows, tickRowsMatchingGrep } from "../history/history-data.js";
14
29
  import { LOGS_SINCE_MAX_MS, SPARSE_WINDOW_NOTE } from "../events/event-window.js";
15
30
  import { eventRole, readEvents } from "../events/event-read.js";
16
31
  import { eventFeedItem, eventMatchesGrep } from "../events/event-format.js";
17
32
  import { readTickDetail } from "../tick/tick-detail-data.js";
18
- import { renderTickDetail, tickNotFoundMessage } from "../tick/tick-detail.js";
33
+ import { renderTickDetail, tickDetailStat, tickNotFoundMessage } from "../tick/tick-detail.js";
19
34
  import { rolePayload } from "../roles/role-view.js";
20
35
  import { DIRECTOR_ROLE } from "../roles/roles.js";
21
36
  import { readQueuedPromptBody } from "../inbox/inbox-edit.js";
22
37
  import { intQuery, rejectBadQueueFileName, rejectBadRole, sinceMsQuery, windowDays } from "./gui-args.js";
23
- import { sendJson } from "./http-body.js";
38
+ import { readBrief } from "../brief/brief.js";
39
+ import { badRequest, okGone, sendJson } from "./http-body.js";
24
40
  import { typoSuffix } from "../text/suggest.js";
25
41
  /** Handle GET /api/transcript?role=<id>&n=N: rendered transcript lines for one loop's pi
26
42
  * log (same rendering as `tumwater logs --role <id>`). Unknown/missing role or a bad n → 400.
@@ -46,27 +62,33 @@ const BACKLOG_FILE_READERS = new Map([
46
62
  ["plans", plannedPlanEntries],
47
63
  ["bugs", openBugEntries],
48
64
  ["questions", openQuestionEntries],
65
+ ["done", donePlanEntries],
66
+ ["fixed", fixedBugEntries],
67
+ ["answered", (root) => answeredQuestionEntries(root).reverse()],
49
68
  ]);
50
69
  const BACKLOG_FILE_NAMES = [...BACKLOG_FILE_READERS.keys()];
51
70
  const BACKLOG_FILE_VALUES = BACKLOG_FILE_NAMES.join(", ");
52
- /** Handle GET /api/backlog?file=<plans|bugs|questions>&index=N: one backlog entry's full
53
- * text ({title, body}), fetched on demand so multi-KB bodies (long repros, whole plans) never
54
- * ride the 1-second /api/status poll. index addresses the Nth entry of that file's open
55
- * section in the same order statusPayload lists its titles — PLANS.md ## Planned,
56
- * BUGS.md ## Open, QUESTIONS.md ## Open — zero-based. Unknown/missing file, missing or bad
57
- * index, and out-of-range index → 400 JSON error via sendJson. The query arrives pre-parsed
58
- * (gui/gui-server.ts's parseRequestTarget), like every GET-data handler here. */
71
+ /** Handle GET /api/backlog?file=<plans|bugs|questions|done|fixed|answered>&index=N: one
72
+ * backlog entry's full text ({title, body}), fetched on demand so multi-KB bodies (long repros,
73
+ * whole plans) never ride the 1-second /api/status poll. index addresses the Nth entry of that
74
+ * file's section in the same order statusPayload lists its titles — plans: PLANS.md ## Planned,
75
+ * bugs: BUGS.md ## Open, questions: QUESTIONS.md ## Open, done: PLANS.md ## Done and fixed:
76
+ * BUGS.md ## Fixed (both newest first, the order of the dashboard's recent rows) and answered:
77
+ * QUESTIONS.md ## Answered, newest first (the file is appended to, so the reverse of file
78
+ * order, unlike done/fixed) — zero-based.
79
+ * Unknown/missing file, missing or bad index, and out-of-range index
80
+ * → 400 JSON error via sendJson.
81
+ * The query arrives pre-parsed (gui/gui-server.ts's parseRequestTarget), like every
82
+ * GET-data handler here. */
59
83
  export function handleBacklog(q, res, root) {
60
84
  const file = q.get("file");
61
85
  if (file === null) {
62
- sendJson(res, 400, { error: `file required (valid values: ${BACKLOG_FILE_VALUES})` });
86
+ badRequest(res, `file required (valid values: ${BACKLOG_FILE_VALUES})`);
63
87
  return;
64
88
  }
65
89
  const reader = BACKLOG_FILE_READERS.get(file);
66
90
  if (!reader) {
67
- sendJson(res, 400, {
68
- error: `unknown file ${JSON.stringify(file)} (valid values: ${BACKLOG_FILE_VALUES})${typoSuffix(file, BACKLOG_FILE_NAMES)}`,
69
- });
91
+ badRequest(res, `unknown file ${JSON.stringify(file)} (valid values: ${BACKLOG_FILE_VALUES})${typoSuffix(file, BACKLOG_FILE_NAMES)}`);
70
92
  return;
71
93
  }
72
94
  const entries = reader(root);
@@ -75,25 +97,25 @@ export function handleBacklog(q, res, root) {
75
97
  return;
76
98
  const entry = entries[index];
77
99
  if (!entry) {
78
- sendJson(res, 400, { error: `index ${index} out of range (${file} has ${entries.length} open entries)` });
100
+ badRequest(res, `index ${index} out of range (${file} has ${entries.length} entries)`);
79
101
  return;
80
102
  }
81
- sendJson(res, 200, { title: entry.title, body: entry.body });
103
+ sendJson(res, 200, { title: entry.title, body: entry.body, refused: refusalNote(entry.body) !== null, waiting: waitingNote(entry.body) !== null });
82
104
  }
83
- /** Handle GET /api/diff[?role=<id>]: the unlanded-change view the CLI prints from the same
84
- * collectors — no `role` → collectFleetChanges (exactly `tumwater diff --json`'s document:
105
+ /** Handle GET /api/diff[?role=<id>]: the unmerged-change view the CLI prints from the same
106
+ * collectors — no `role` → collectTeamChanges (exactly `tumwater diff --json`'s document:
85
107
  * `{ mainBranch, roles }`, no per-role patch halves); `?role=<id>` → collectRoleChange (the
86
108
  * full per-role view with `diff`/`uncommittedDiff`, `tumwater diff --role <id> --json`). The
87
109
  * role is a target, not a filter, so it validates through rejectBadRole exactly like
88
- * /api/transcript and /api/tick (unknown/missing → 400 naming the valid ids) — the same ids
110
+ * /api/transcript and /api/run (unknown/missing → 400 naming the valid ids) — the same ids
89
111
  * `parseRoleFlag` accepts on the CLI, including disabled and user-defined loops. The
90
112
  * collectors degrade absent worktrees and missing base branches to their `state` instead of
91
113
  * throwing, so a fresh or half-built repo still answers 200. Reads git plumbing directly, so
92
- * it works whether or not the fleet is running. */
114
+ * it works whether or not the team is running. */
93
115
  export async function handleDiff(q, res, root) {
94
116
  const role = q.get("role");
95
117
  if (role === null) {
96
- sendJson(res, 200, await collectFleetChanges(root));
118
+ sendJson(res, 200, await collectTeamChanges(root));
97
119
  return;
98
120
  }
99
121
  if (rejectBadRole(root, res, role))
@@ -104,10 +126,10 @@ export async function handleDiff(q, res, root) {
104
126
  * usage report — the day series (collectReport's ReportData) as JSON the dashboard's report tab
105
127
  * charts, or, when sinceMs is set, the trailing-window totals rendered through
106
128
  * renderSinceReportMarkdown as { markdown } (the CLI's `report --since` shape), bounded by the
107
- * reportSinceMs helper. The day window follows windowDays. An absent role is the fleet-wide
129
+ * reportSinceMs helper. The day window follows windowDays. An absent role is the team-wide
108
130
  * report exactly as before; a present one is a filter, validated through rejectBadRole (unknown,
109
131
  * including the empty string → 400 naming the valid ids) and handed to the collector's role
110
- * scope. Reads files directly, so it works whether or not the fleet is running. */
132
+ * scope. Reads files directly, so it works whether or not the team is running. */
111
133
  export function handleReport(q, res, root) {
112
134
  const role = q.get("role");
113
135
  if (role !== null && rejectBadRole(root, res, role))
@@ -129,19 +151,19 @@ export function handleReport(q, res, root) {
129
151
  * null once the 400 is sent. */
130
152
  function reportSinceMs(q, res) {
131
153
  if (q.get("days") !== null) {
132
- sendJson(res, 400, { error: "days and sinceMs are mutually exclusive; days counts whole local days, sinceMs totals a trailing window" });
154
+ badRequest(res, "days and sinceMs are mutually exclusive; days counts whole local days, sinceMs totals a trailing window");
133
155
  return null;
134
156
  }
135
- return sinceMsQuery(q, res, REPORT_SINCE_MAX_MS, (sinceMs) => `sinceMs must be between 1 and ${humanSeconds(REPORT_SINCE_MAX_MS / 1000)} (got ${sinceMs})`);
157
+ return sinceMsQuery(q, res, REPORT_SINCE_MAX_MS, reportSinceMsg);
136
158
  }
137
159
  /** Handle GET /api/failures?days=N[&role=<id>] or /api/failures?sinceMs=<ms>[&role=<id>]: the
138
160
  * same bounded Markdown failure digest the telemetry loop feeds on and `tumwater report
139
161
  * --failures` prints, as JSON ({ markdown }) — the dashboard's failures tab renders it; with
140
162
  * sinceMs it is the trailing-window digest (collectFailureReportSince) through the same render,
141
163
  * the CLI's `report --failures --since` shape. The days window follows windowDays (so it can
142
- * never drift from /api/report's). role rides the same absent-fleet / present-filter rule as
143
- * handleReport: absent → the fleet digest, present → validated and scoped. Reads files
144
- * directly, so it works with no fleet running. */
164
+ * never drift from /api/report's). role rides the same absent-team / present-filter rule as
165
+ * handleReport: absent → the team digest, present → validated and scoped. Reads files
166
+ * directly, so it works with no team running. */
145
167
  export function handleFailures(q, res, root) {
146
168
  const role = q.get("role");
147
169
  if (role !== null && rejectBadRole(root, res, role))
@@ -158,16 +180,17 @@ export function handleFailures(q, res, root) {
158
180
  /** Handle GET /api/history?role=<id>&n=N[&grep=<text>] — or ?role=<id>&sinceMs=<ms> for a
159
181
  * trailing window: the per-tick rows the CLI's `tumwater history` derives from the event log,
160
182
  * the dashboard's history tab renders them. The count shape ({ rows }) rides readTickRows (the
161
- * shared scan, window growth included, so a role-filtered ask on a busy fleet cannot silently
183
+ * shared scan, window growth included, so a role-filtered ask on a busy team cannot silently
162
184
  * come back short); n is optional: absent → HISTORY_DEFAULT_TICKS; present but not a plain
163
185
  * non-negative integer → 400 (the handleBacklog index discipline — an explicit count that does
164
186
  * not parse is a client error, unlike the report's degrade-to-default typo rule); then clamped
165
187
  * to [1, HISTORY_MAX_TICKS] — the GUI clamps where the CLI fails fast, its established
166
188
  * convention. grep is optional: absent or empty filters nothing; present → the rows are
167
- * filtered through history-data.ts's shared tickRowsMatchingGrep rule, the same case-insensitive haystack rule
168
- * `tumwater history --grep` uses (shared, not re-implemented), after the n/role scan so n
169
- * bounds the scanned window and not the printed rows — the CLI's rule. The window shape
170
- * ({ rows, covered, note }) rides readTickRowsSince, the collector behind `history --since`,
189
+ * filtered through history-data.ts's shared tickRowsMatchingGrep rule, the same
190
+ * case-insensitive haystack rule `tumwater history --grep` uses (shared, not
191
+ * re-implemented), after the n/role scan so n bounds the scanned window and not the
192
+ * printed rows — the CLI's rule. The window shape ({ rows, covered, note }) rides
193
+ * readTickRowsSince, the collector behind `history --since`,
171
194
  * so the browser and the CLI cannot disagree about the window's rows; a simultaneous n is a
172
195
  * rival shape → 400 (the CLI's count-vs-window rule), sinceMs parses and is capped through
173
196
  * sinceMsQuery (gui-args.ts, LOGS_SINCE_MAX_MS — the shared `history --since` cap), and note is
@@ -175,13 +198,21 @@ export function handleFailures(q, res, root) {
175
198
  * loops; present → passed straight through as the role filter with no config validation (a
176
199
  * filter is not a target — an id with no ticks legitimately yields zero rows), so the endpoint
177
200
  * reads nothing but files, like /api/report and /api/failures. Reads files directly, so it
178
- * works whether or not the fleet is running. */
201
+ * present → passed straight through as the role filter with no config validation (a
202
+ * filter is not a target — an id with no ticks legitimately yields zero rows), so the endpoint
203
+ * reads nothing but files, like /api/report and /api/failures. summary is optional: absent /
204
+ * empty adds nothing (the response is byte-for-byte today's); present (any value — the flag's
205
+ * mere presence, a bad value ignored like a missing one) adds an additive `summary` folding the
206
+ * exact served rows through the shared summarizeTickRows collector, the same tally
207
+ * `tumwater history --summary` prints, so the dashboard Summary and the table cannot disagree
208
+ * about what a tick was. Reads files directly, so it works whether or not the team is running. */
179
209
  export function handleHistory(q, res, root) {
180
210
  const role = q.get("role") || null; // "" and absent both read all loops
181
211
  const grep = q.get("grep") || null; // "" and absent both filter nothing
212
+ const summary = q.get("summary") !== null; // the flag's presence, whatever its value
182
213
  if (q.get("sinceMs") !== null) {
183
214
  if (q.get("n") !== null) {
184
- sendJson(res, 400, { error: "sinceMs cannot be combined with n (a count and a window are rival shapes)" });
215
+ badRequest(res, "sinceMs cannot be combined with n (a count and a window are rival shapes)");
185
216
  return;
186
217
  }
187
218
  const sinceMs = sinceMsQuery(q, res, LOGS_SINCE_MAX_MS, (ms) => `sinceMs is capped at ${humanSeconds(LOGS_SINCE_MAX_MS / 1000)} (got ${ms})`);
@@ -189,7 +220,7 @@ export function handleHistory(q, res, root) {
189
220
  return;
190
221
  const { rows, covered } = readTickRowsSince(root, sinceMs, role);
191
222
  const matched = grep === null ? rows : tickRowsMatchingGrep(rows, grep);
192
- sendJson(res, 200, { rows: matched, covered, note: covered ? "" : SPARSE_WINDOW_NOTE });
223
+ sendJson(res, 200, { rows: matched, covered, note: covered ? "" : SPARSE_WINDOW_NOTE, ...(summary ? { summary: summarizeTickRows(matched) } : {}) });
193
224
  return;
194
225
  }
195
226
  const parsed = intQuery(q, res, "n", "non-negative", HISTORY_DEFAULT_TICKS);
@@ -199,7 +230,7 @@ export function handleHistory(q, res, root) {
199
230
  let rows = readTickRows(root, n, role);
200
231
  if (grep !== null)
201
232
  rows = tickRowsMatchingGrep(rows, grep);
202
- sendJson(res, 200, { rows });
233
+ sendJson(res, 200, summary ? { rows, summary: summarizeTickRows(rows) } : { rows });
203
234
  }
204
235
  /** How many of the log's newest events `GET /api/logs` scans by default, and the most it will
205
236
  * scan however large `n` asks: the CLI's `-n` shape (n bounds the scanned tail, not the printed
@@ -220,7 +251,7 @@ const LOGS_SEARCH_MAX_EVENTS = 5000;
220
251
  * Reads the tail through readEvents' stat-keyed cache (no covered byte end, unlike the CLI's
221
252
  * follow), so a dashboard search that repeats on an unchanged log — the debounced filter fires
222
253
  * one per filter change — costs one stat instead of re-reading and re-parsing the scanned tail.
223
- * Reads files directly, so it works whether or not the fleet is running. */
254
+ * Reads files directly, so it works whether or not the team is running. */
224
255
  export function handleLogs(q, res, root) {
225
256
  const parsed = intQuery(q, res, "n", "non-negative", LOGS_SEARCH_EVENTS);
226
257
  if (parsed === null)
@@ -233,7 +264,7 @@ export function handleLogs(q, res, root) {
233
264
  const matched = events.filter((e) => (role === null || eventRole(e) === role) && (needle === null || eventMatchesGrep(e, needle)));
234
265
  sendJson(res, 200, { events: matched.map(eventFeedItem) });
235
266
  }
236
- /** Handle GET /api/tick?role=<id>&tick=N: one tick's full event trail — readTickDetail's
267
+ /** Handle GET /api/run?role=<id>&run=N: one tick's full event trail — readTickDetail's
237
268
  * TickDetail, the collector `tumwater tick <role> <n>` prints from, plus `text`: the same
238
269
  * payload through renderTickDetail, pre-rendered server-side like /api/transcript's lines so
239
270
  * the browser shows the CLI's exact rendering instead of re-implementing the event formatting.
@@ -243,12 +274,12 @@ export function handleLogs(q, res, root) {
243
274
  * through intQuery (missing or not a positive integer → 400); and a tick the scanned window
244
275
  * does not hold — never ran, or rotation ate it — answers 404 with tickNotFoundMessage, the
245
276
  * CLI's exact not-found wording,
246
- * never a crash. Reads files directly, so it works whether or not the fleet is running. */
247
- export function handleTick(q, res, root) {
277
+ * never a crash. Reads files directly, so it works whether or not the team is running. */
278
+ export async function handleTick(q, res, root) {
248
279
  const role = q.get("role");
249
280
  if (rejectBadRole(root, res, role))
250
281
  return;
251
- const tick = intQuery(q, res, "tick", "positive");
282
+ const tick = intQuery(q, res, "run", "positive");
252
283
  if (tick === null)
253
284
  return;
254
285
  const detail = readTickDetail(root, role, tick);
@@ -256,15 +287,15 @@ export function handleTick(q, res, root) {
256
287
  sendJson(res, 404, { error: tickNotFoundMessage(role, tick) });
257
288
  return;
258
289
  }
259
- sendJson(res, 200, { ...detail, text: renderTickDetail(detail) });
290
+ sendJson(res, 200, { ...detail, text: renderTickDetail(detail), stat: await tickDetailStat(root, detail) });
260
291
  }
261
292
  /** Handle GET /api/role?role=<id>: one loop's inspection payload — rolePayload's object,
262
293
  * the same collection behind `tumwater role <id>` — so the drawer's Notebook and Next tick
263
294
  * prompt sections render what the CLI shows without re-implementing the assembly. The role
264
295
  * is a target here, so it validates through rejectBadRole exactly like /api/transcript and
265
- * /api/tick (unknown/missing → the shared 400 naming the valid ids and correcting typos);
296
+ * /api/run (unknown/missing → the shared 400 naming the valid ids and correcting typos);
266
297
  * rolePayload is only reached with an id rejectBadRole accepted, so its own unknown-role
267
- * throw never fires. Reads files directly, so it works whether or not the fleet is running. */
298
+ * throw never fires. Reads files directly, so it works whether or not the team is running. */
268
299
  export function handleRole(q, res, root) {
269
300
  const role = q.get("role");
270
301
  if (rejectBadRole(root, res, role))
@@ -278,9 +309,10 @@ export function handleRole(q, res, root) {
278
309
  * marker and the `[image attached: …]` reference block are stripped, both plumbing the edit
279
310
  * carries over unchanged. The role is optional (absent reads the director's queue) and
280
311
  * validates exactly like /api/transcript; a file name failing the shared
281
- * rejectBadQueueFileName guard is a user-input error answered 400 with nothing read. A vanished file answers 200
282
- * { ok: true, status: "gone" } — the loop already dequeued it, which is data for the UI, not
283
- * a server fault. Reads one file directly, so it works whether or not the fleet is running. */
312
+ * rejectBadQueueFileName guard is a user-input error answered 400 with nothing read. A
313
+ * vanished file answers 200 { ok: true, status: "gone" } — the loop already dequeued
314
+ * it, which is data for the UI, not a server fault. Reads one file directly, so it works
315
+ * whether or not the team is running. */
284
316
  export function handlePromptText(q, res, root) {
285
317
  const roleParam = q.get("role");
286
318
  if (roleParam !== null && rejectBadRole(root, res, roleParam))
@@ -291,21 +323,21 @@ export function handlePromptText(q, res, root) {
291
323
  return;
292
324
  const text = readQueuedPromptBody(root, role, file);
293
325
  if (text === null) {
294
- sendJson(res, 200, { ok: true, status: "gone" });
326
+ okGone(res);
295
327
  return;
296
328
  }
297
329
  sendJson(res, 200, { ok: true, text });
298
330
  }
299
331
  /** Handle GET /api/doctor: the same DoctorReport `tumwater doctor --json` prints, so the
300
332
  * Settings view's Health check card and the CLI cannot disagree. Read-only — the checks write
301
- * nothing (runDoctor's own contract) — and it works whether or not the fleet is running. `run`
333
+ * nothing (runDoctor's own contract) — and it works whether or not the team is running. `run`
302
334
  * exists so tests can skip the host probes; a throw reaches the server's outer 500. */
303
- export async function handleDoctor(res, root, run = runDoctor) {
335
+ export async function handleDoctor(res, root, run = (r) => runDoctor(r, undefined, undefined, "never")) {
304
336
  sendJson(res, 200, await run(root));
305
337
  }
306
338
  /** Handle GET /api/config: the Settings view's resolved values for exactly
307
339
  * EDITABLE_CONFIG_KEYS — read through the same load path `tumwater config get` uses
308
- * (loadConfigSafe, defaults merged in), so the page shows what the fleet would actually
340
+ * (loadConfigSafe, defaults merged in), so the page shows what the team would actually
309
341
  * load. A broken or invalid tumwater.json answers 500 with validateConfig's message instead
310
342
  * of serving a half-empty panel. */
311
343
  export function handleConfig(res, root) {
@@ -320,3 +352,31 @@ export function handleConfig(res, root) {
320
352
  out[key] = record[key] ?? null;
321
353
  sendJson(res, 200, out);
322
354
  }
355
+ /** Handle GET /api/retire: the Settings view's "Disabled roles" card — every configured,
356
+ * disabled, non-director role, with what `tumwater retire` would remove (previewRetire, so the
357
+ * rails and wording are the CLI's) and the rails' objections. `wouldRemove` is empty when there
358
+ * is nothing to retire (a role disabled with a clean checkout), so the card can also re-enable
359
+ * a role via POST /api/role-enable. Read-only (previewRetire's contract) and works whether or
360
+ * not the team runs; a throw reaches the server's outer 500, an unreadable tumwater.json
361
+ * answers 500 like /api/config. */
362
+ export async function handleRetireList(res, root) {
363
+ const { config, error } = loadConfigSafe(root);
364
+ if (config === undefined) {
365
+ sendJson(res, 500, { error });
366
+ return;
367
+ }
368
+ const roles = [];
369
+ for (const role of knownRoleIds(config).filter((id) => id !== DIRECTOR_ROLE && !loopEnabled(config, id))) {
370
+ const { wouldRemove, objections } = await previewRetire(root, role);
371
+ roles.push({ role, wouldRemove, phrases: wouldRemove.map((a) => artifactPhrase(a, role)), objections });
372
+ }
373
+ sendJson(res, 200, { roles });
374
+ }
375
+ /** Handle GET /api/brief: the brief file's name and the current initial prompt (what
376
+ * `tumwater brief --json` prints), or 404 when no brief carries a prompt block. */
377
+ export function handleBrief(res, root) {
378
+ const brief = readBrief(root);
379
+ if (brief === null)
380
+ return sendJson(res, 404, { error: "no project brief with a prompt block found — run `tumwater init` first" });
381
+ sendJson(res, 200, { file: brief.file, prompt: brief.prompt });
382
+ }
@@ -0,0 +1,145 @@
1
+ // vocabulary: plain
2
+ /**
3
+ * The dashboard's POST operator-lifecycle command handlers (barrelled into
4
+ * 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
7
+ * one layer down (operator-intent, role-notes-write, brief-edit) and touches no socket beyond
8
+ * its own `res`.
9
+ */
10
+ import { cancelScheduledStopMessage, requestAbort, requestResetCounters, requestRestart, requestScheduledStop, runManualReclaim } from "../operator/operator-intent.js";
11
+ import { saveRoleNote } from "../agent/role-notes-write.js";
12
+ import { roleNotesPath } from "../paths.js";
13
+ import { DIRECTOR_ROLE } from "../roles/roles.js";
14
+ import { artifactPhrase, retireRole } from "../operator/retire.js";
15
+ import { editInitialPrompt } from "../brief/brief-edit.js";
16
+ import { validRoleIds } from "./gui-args.js";
17
+ import { badRequest, sendJson } from "./http-body.js";
18
+ import { errorMessage, gotSuffix } from "../text/text.js";
19
+ import { readPostBody, readRoleBody, sendRequestResult } from "./gui-endpoint-common.js";
20
+ import { wakeInSecondsMs } from "./gui-team-commands.js";
21
+ /** Handle POST /api/reset-counters: the dashboard's counter reset — the same marker-writing
22
+ * core `tumwater reset-counters` calls (requestResetCounters). Same body discipline and
23
+ * `{}` → every configured role default as /api/wake. */
24
+ export async function handleResetCounters(req, res, root) {
25
+ const body = await readRoleBody(req, res, '{"role": "feature"}', root, true);
26
+ if (!body)
27
+ return;
28
+ sendJson(res, 200, {
29
+ ok: true,
30
+ message: requestResetCounters(root, body.role === undefined ? validRoleIds(root) : [body.role]),
31
+ });
32
+ }
33
+ /** Handle POST /api/stop-schedule: the dashboard's "Stop the team later" control — the same
34
+ * cores `tumwater stop --in`/`--cancel` call (requestScheduledStop, cancelScheduledStopMessage).
35
+ * `{"cancel": true}` drops the marker (no live team needed, like the CLI); otherwise
36
+ * `inSeconds` is required and takes /api/wake's positive-finite-and-capped rule. A stopped
37
+ * team answers 409 with the not-running error. */
38
+ export async function handleStopSchedule(req, res, root) {
39
+ const body = await readPostBody(req, res, '{"inSeconds": 7200}');
40
+ if (!body)
41
+ return;
42
+ if (body.cancel === true) {
43
+ if (body.inSeconds !== undefined) {
44
+ badRequest(res, "cancel and inSeconds cannot be combined");
45
+ return;
46
+ }
47
+ sendJson(res, 200, { ok: true, message: cancelScheduledStopMessage(root) });
48
+ return;
49
+ }
50
+ const inMs = wakeInSecondsMs(res, body);
51
+ if (inMs === null)
52
+ return;
53
+ if (inMs === undefined) {
54
+ badRequest(res, "inSeconds or cancel is required");
55
+ return;
56
+ }
57
+ sendRequestResult(res, requestScheduledStop(root, inMs));
58
+ }
59
+ /** Handle POST /api/restart: the dashboard's build-stale alert's refresh button — the same
60
+ * marker-writing core a CLI surface would call (requestRestart), so the wording and the
61
+ * not-pending refusal cannot drift. The not-pending error comes back 409 like /api/abort's
62
+ * not-live one: the request is valid but nothing can act on it. Same body discipline as
63
+ * /api/wake (readJsonObject → 400 malformed/non-object, 413 oversized; the body itself is
64
+ * ignored — there are no options to force a restart). */
65
+ export async function handleRestart(req, res, root) {
66
+ const body = await readPostBody(req, res, "{}");
67
+ if (!body)
68
+ return;
69
+ sendRequestResult(res, requestRestart(root));
70
+ }
71
+ /** Handle POST /api/reclaim: the dashboard disk row's reclaim button — the same manual pass
72
+ * `tumwater reclaim` runs (runManualReclaim), answering its confirmation verbatim. Same body
73
+ * discipline as /api/restart (the body is ignored); a throw reaches the server's outer 500. */
74
+ export async function handleReclaim(req, res, root) {
75
+ const body = await readPostBody(req, res, "{}");
76
+ if (!body)
77
+ return;
78
+ sendJson(res, 200, { ok: true, message: await runManualReclaim(root) });
79
+ }
80
+ /** Handle POST /api/abort: the dashboard's per-row abort control — the same marker-writing
81
+ * core `tumwater abort` calls (requestAbort). The role is required and validated like
82
+ * /api/transcript; requestAbort's not-live error comes back 409 — the marker is valid but
83
+ * nothing can consume it, a conflict rather than a client 400. The director variant's
84
+ * message (the discarded-prompt note) rides through verbatim. */
85
+ export async function handleAbort(req, res, root) {
86
+ const body = await readRoleBody(req, res, '{"role": "feature"}', root);
87
+ if (!body)
88
+ return;
89
+ sendRequestResult(res, requestAbort(root, body.role));
90
+ }
91
+ /** Handle POST /api/role-notes: the loop drawer's notebook Edit/Clear — the same saveRoleNote
92
+ * `tumwater role <id> --notes-file` / `--clear-notes` call, so the 4 KB cap and its wording
93
+ * cannot drift. `"text": ""` clears. The director has no notes (400, the CLI's wording); a
94
+ * non-string text is a 400 before anything is written; an over-cap note is a 400 carrying
95
+ * saveRoleNote's byte-count message. */
96
+ export async function handleRoleNotes(req, res, root) {
97
+ const body = await readRoleBody(req, res, '{"role": "feature", "text": "…"}', root);
98
+ if (!body)
99
+ return;
100
+ const role = body.role;
101
+ if (role === DIRECTOR_ROLE)
102
+ 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)}`);
105
+ try {
106
+ sendJson(res, 200, { ok: true, message: saveRoleNote(roleNotesPath(root, role), body.text) });
107
+ }
108
+ catch (err) {
109
+ badRequest(res, errorMessage(err));
110
+ }
111
+ }
112
+ /** Handle POST /api/retire: the Settings view's Retire button — the same retireRole core
113
+ * `tumwater retire --role <id>` calls, never forced (a `force` key is a 400 naming the CLI
114
+ * override). A rail's refusal or an unreadable tumwater.json is a 409 carrying the core's
115
+ * message and removes nothing; success answers what was removed in the CLI's wording. */
116
+ export async function handleRetire(req, res, root) {
117
+ const body = await readRoleBody(req, res, '{"role": "improve"}', root);
118
+ if (!body)
119
+ return;
120
+ const role = body.role;
121
+ if (body.force !== undefined)
122
+ return badRequest(res, "the dashboard never forces a retire — run `tumwater retire --role <id> --force` from the CLI");
123
+ try {
124
+ const { removed, skipped } = await retireRole(root, role, {});
125
+ const message = removed.length === 0 ? `nothing to remove for role ${role}` : removed.map((a) => `removed ${artifactPhrase(a, role)}`).join("; ");
126
+ sendJson(res, 200, { ok: true, removed, skipped, message });
127
+ }
128
+ catch (err) {
129
+ sendJson(res, 409, { error: errorMessage(err) });
130
+ }
131
+ }
132
+ /** Handle POST /api/brief: replace the initial prompt through editInitialPrompt — the same
133
+ * checks, locked write, and commit as `tumwater brief --file`. A non-string prompt or any
134
+ * refusal is a 400 and writes nothing. */
135
+ export async function handleBriefEdit(req, res, root) {
136
+ const body = await readPostBody(req, res, '{"prompt": "…"}');
137
+ if (!body)
138
+ 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);
142
+ if (!outcome.ok)
143
+ return badRequest(res, outcome.error);
144
+ sendJson(res, 200, { file: outcome.file, changed: outcome.changed, committed: outcome.committed });
145
+ }