tumwater 0.1.3 → 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 (359) hide show
  1. package/README.md +63 -28
  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-eligibility.js +332 -43
  26. package/dist/src/backlog/backlog-md.js +79 -17
  27. package/dist/src/backlog/backlog-render.js +62 -11
  28. package/dist/src/backlog/backlog-structure.js +261 -58
  29. package/dist/src/backlog/backlog-write.js +178 -441
  30. package/dist/src/backlog/backlog.js +113 -16
  31. package/dist/src/backlog/question-answer.js +339 -0
  32. package/dist/src/baseline/main-baseline.js +75 -88
  33. package/dist/src/baseline/main-red.js +32 -30
  34. package/dist/src/{brief-edit.js → brief/brief-edit.js} +12 -11
  35. package/dist/src/{brief.js → brief/brief.js} +33 -8
  36. package/dist/src/budget/budget.js +32 -15
  37. package/dist/src/budget/fallback-breaker.js +8 -7
  38. package/dist/src/build/build-check-detect.js +8 -7
  39. package/dist/src/build/build-check-events.js +68 -22
  40. package/dist/src/build/build-check-report.js +22 -23
  41. package/dist/src/build/build-check-scoped.js +51 -62
  42. package/dist/src/build/build-check.js +15 -14
  43. package/dist/src/build/build-info.js +2 -1
  44. package/dist/src/build/build-stage.js +23 -22
  45. package/dist/src/build/dep-install.js +22 -21
  46. package/dist/src/build/host-sleep.js +1 -1
  47. package/dist/src/change/change-data.js +13 -11
  48. package/dist/src/change/change-render.js +11 -10
  49. package/dist/src/cli/brief-command.js +9 -7
  50. package/dist/src/cli/cli-args.js +25 -5
  51. package/dist/src/cli/cli-command-args.js +43 -10
  52. package/dist/src/cli/cli-editor.js +13 -1
  53. package/dist/src/cli/cli-flag-specs.js +31 -6
  54. package/dist/src/cli/cli-marker-commands.js +13 -10
  55. package/dist/src/cli/cli-query-commands.js +439 -56
  56. package/dist/src/cli/cli-repo-gate.js +1 -1
  57. package/dist/src/cli/cli-run.js +124 -68
  58. package/dist/src/cli/completion.js +70 -12
  59. package/dist/src/cli/config-commands.js +14 -11
  60. package/dist/src/cli/help.js +264 -124
  61. package/dist/src/cli/log-commands.js +127 -21
  62. package/dist/src/cli/notify-command.js +33 -18
  63. package/dist/src/cli/question-commands.js +13 -224
  64. package/dist/src/cli.js +78 -36
  65. package/dist/src/collections.js +66 -16
  66. package/dist/src/concurrency/check-permit.js +16 -17
  67. package/dist/src/concurrency/lock.js +6 -5
  68. package/dist/src/concurrency/provider-lease.js +69 -0
  69. package/dist/src/concurrency/semaphore.js +258 -36
  70. package/dist/src/config/config-aliases.js +106 -0
  71. package/dist/src/config/config-editable-keys.js +3 -3
  72. package/dist/src/config/config-fallbacks.js +379 -0
  73. package/dist/src/config/config-field-checks.js +19 -10
  74. package/dist/src/config/config-keys.js +160 -0
  75. package/dist/src/config/config-live.js +64 -12
  76. package/dist/src/config/config-schema.js +34 -22
  77. package/dist/src/config/config-validation.js +132 -48
  78. package/dist/src/config/config-views.js +107 -325
  79. package/dist/src/config/config-write.js +138 -177
  80. package/dist/src/config/config.js +60 -49
  81. package/dist/src/config/model-selector.js +28 -2
  82. package/dist/src/doctor/doctor-backlog.js +494 -68
  83. package/dist/src/doctor/doctor-checks.js +509 -58
  84. package/dist/src/doctor/doctor-holds.js +139 -78
  85. package/dist/src/doctor/doctor-install.js +30 -0
  86. package/dist/src/doctor/doctor-launch-services.js +2 -2
  87. package/dist/src/doctor/doctor-model-checks.js +549 -157
  88. package/dist/src/doctor/doctor-notify.js +2 -3
  89. package/dist/src/doctor/doctor-orphans.js +10 -9
  90. package/dist/src/doctor/doctor-render.js +36 -9
  91. package/dist/src/doctor/doctor.js +76 -25
  92. package/dist/src/doctor/image-probe.js +244 -0
  93. package/dist/src/doctor/probe.js +64 -0
  94. package/dist/src/events/event-format.js +219 -106
  95. package/dist/src/events/event-labels.js +116 -0
  96. package/dist/src/events/event-legacy.js +140 -0
  97. package/dist/src/events/event-read.js +58 -22
  98. package/dist/src/events/event-window.js +27 -7
  99. package/dist/src/events/events.js +18 -14
  100. package/dist/src/events/notify.js +80 -30
  101. package/dist/src/failure/failure-cluster.js +12 -9
  102. package/dist/src/failure/failure-data.js +96 -48
  103. package/dist/src/failure/failure-render.js +69 -58
  104. package/dist/src/failure/failure-state-change.js +33 -26
  105. package/dist/src/failure/time-spend.js +79 -59
  106. package/dist/src/files/file-queue.js +16 -4
  107. package/dist/src/files/files.js +18 -6
  108. package/dist/src/files/json-object.js +18 -3
  109. package/dist/src/files/tail.js +1 -1
  110. package/dist/src/gates/bootstrap-gates.js +5 -4
  111. package/dist/src/gates/budget-gates.js +250 -34
  112. package/dist/src/gates/disk-gate.js +18 -18
  113. package/dist/src/gates/gate-polls.js +117 -69
  114. package/dist/src/gates/maintenance-quota.js +29 -29
  115. package/dist/src/gates/pause-gates.js +11 -11
  116. package/dist/src/gates/readiness.js +2 -1
  117. package/dist/src/gates/role-cap-gates.js +16 -21
  118. package/dist/src/gates/startup-gate.js +75 -31
  119. package/dist/src/gates/streak-gate.js +18 -16
  120. package/dist/src/git/commit-message.js +6 -5
  121. package/dist/src/git/git-diff.js +16 -16
  122. package/dist/src/git/git-run.js +8 -8
  123. package/dist/src/git/git.js +79 -59
  124. package/dist/src/git/slots-state.js +14 -5
  125. package/dist/src/git/worktree-pool.js +25 -18
  126. package/dist/src/git/worktree-use.js +15 -14
  127. package/dist/src/git/worktree.js +18 -18
  128. package/dist/src/gui/gui-args.js +10 -14
  129. package/dist/src/gui/gui-backlog-commands.js +273 -0
  130. package/dist/src/gui/gui-command.js +1 -1
  131. package/dist/src/gui/gui-config-commands.js +440 -0
  132. package/dist/src/gui/gui-endpoint-commands.js +16 -716
  133. package/dist/src/gui/gui-endpoint-common.js +49 -0
  134. package/dist/src/gui/gui-endpoints.js +105 -54
  135. package/dist/src/gui/gui-operator-commands.js +145 -0
  136. package/dist/src/gui/gui-prompt-commands.js +234 -0
  137. package/dist/src/gui/gui-server.js +25 -8
  138. package/dist/src/gui/gui-team-commands.js +164 -0
  139. package/dist/src/gui/http-body.js +17 -1
  140. package/dist/src/history/history-data.js +179 -104
  141. package/dist/src/history/history-render.js +76 -0
  142. package/dist/src/history/history.js +29 -70
  143. package/dist/src/inbox/inbox-attachments.js +66 -10
  144. package/dist/src/inbox/inbox-cancel.js +7 -5
  145. package/dist/src/inbox/inbox-edit.js +1 -1
  146. package/dist/src/inbox/inbox-submit.js +44 -4
  147. package/dist/src/inbox/inbox-top.js +83 -0
  148. package/dist/src/inbox/inbox.js +8 -2
  149. package/dist/src/inbox/pending-prompt.js +1 -1
  150. package/dist/src/inbox/prompt-commands.js +114 -73
  151. package/dist/src/init/init-templates.js +16 -14
  152. package/dist/src/init/init.js +12 -11
  153. package/dist/src/loop/leftover.js +28 -27
  154. package/dist/src/loop/loop-fallback.js +2 -2
  155. package/dist/src/loop/loop-pi.js +104 -50
  156. package/dist/src/loop/loop-recovery.js +15 -15
  157. package/dist/src/loop/loop-state.js +130 -13
  158. package/dist/src/loop/loop-tick.js +190 -88
  159. package/dist/src/loop/loop.js +68 -36
  160. package/dist/src/loop/model-fallback.js +3 -2
  161. package/dist/src/loop/revision.js +16 -7
  162. package/dist/src/merge/backlog-conflicts.js +155 -0
  163. package/dist/src/{landing/landing-batch.js → merge/merge-batch.js} +90 -89
  164. package/dist/src/{landing/landing-check-failures.js → merge/merge-check-failures.js} +87 -41
  165. package/dist/src/{landing/landing-core.js → merge/merge-core.js} +119 -95
  166. package/dist/src/{landing/landing-diff.js → merge/merge-diff.js} +1 -1
  167. package/dist/src/merge/merge-drain.js +212 -0
  168. package/dist/src/{landing/landing-git.js → merge/merge-git.js} +94 -20
  169. package/dist/src/merge/merge-pipeline.js +99 -0
  170. package/dist/src/{landing/landing-questions.js → merge/merge-questions.js} +9 -6
  171. package/dist/src/merge/merge-queue-render.js +75 -0
  172. package/dist/src/{landing/landing-queue.js → merge/merge-queue.js} +95 -36
  173. package/dist/src/merge/merge-slot.js +340 -0
  174. package/dist/src/{landing/landing-stack.js → merge/merge-stack.js} +66 -62
  175. package/dist/src/{landing/landing-merge.js → merge/merge-to-main.js} +72 -62
  176. package/dist/src/merge/merge-verify.js +212 -0
  177. package/dist/src/notes-cli.js +97 -0
  178. package/dist/src/operator/operator-commands.js +113 -45
  179. package/dist/src/operator/operator-intent.js +74 -53
  180. package/dist/src/operator/operator-requests.js +52 -52
  181. package/dist/src/operator/retire.js +47 -28
  182. package/dist/src/operator/revert.js +71 -0
  183. package/dist/src/orchestrator/orchestrator-launch.js +74 -14
  184. package/dist/src/orchestrator/orchestrator-scheduling.js +74 -43
  185. package/dist/src/orchestrator/orchestrator.js +149 -96
  186. package/dist/src/orchestrator/retention.js +29 -12
  187. package/dist/src/orchestrator/startup-migration-steps.js +51 -0
  188. package/dist/src/orchestrator/startup-migrations.js +40 -0
  189. package/dist/src/paths.js +124 -38
  190. package/dist/src/pi/pi-args.js +26 -3
  191. package/dist/src/pi/pi-backend.js +15 -23
  192. package/dist/src/pi/pi-bin.js +18 -12
  193. package/dist/src/pi/pi-event-line.js +17 -1
  194. package/dist/src/pi/pi-failure.js +87 -0
  195. package/dist/src/pi/pi-models.js +126 -38
  196. package/dist/src/pi/pi-run-result.js +1 -1
  197. package/dist/src/pi/pi-settings.js +86 -0
  198. package/dist/src/pi/pi-stream.js +131 -107
  199. package/dist/src/pi/pi-watchdogs.js +4 -3
  200. package/dist/src/pi-extension/bounded-output.js +20 -5
  201. package/dist/src/pi-extension/context-budget.js +3 -1
  202. package/dist/src/pi-extension/context-shake.js +54 -21
  203. package/dist/src/pi-extension/context-window.js +37 -0
  204. package/dist/src/pi-extension/full-output.js +2 -2
  205. package/dist/src/pi-extension/strip-thinking.js +80 -0
  206. package/dist/src/pi-extension/trim-system-prompt.js +38 -0
  207. package/dist/src/process/launch-services.js +11 -10
  208. package/dist/src/process/process-table.js +1 -1
  209. package/dist/src/process/process.js +7 -7
  210. package/dist/src/process/run-marker.js +29 -6
  211. package/dist/src/process/supervisor.js +36 -25
  212. package/dist/src/prompt/dialect.js +30 -10
  213. package/dist/src/prompt/gate-prompts.js +170 -67
  214. package/dist/src/prompt/principles.js +1 -0
  215. package/dist/src/prompt/prompt-followup.js +29 -20
  216. package/dist/src/prompt/prompt.js +249 -91
  217. package/dist/src/redeploy/redeploy-policy.js +10 -10
  218. package/dist/src/redeploy/redeploy.js +6 -4
  219. package/dist/src/redeploy/redeployer.js +63 -55
  220. package/dist/src/redeploy/self-reload.js +1 -1
  221. package/dist/src/report/report-data.js +259 -254
  222. package/dist/src/report/report-fold.js +353 -0
  223. package/dist/src/report/report-render.js +235 -44
  224. package/dist/src/request-timeouts.js +1 -1
  225. package/dist/src/review/known-flakes.js +34 -14
  226. package/dist/src/review/protected-edits.js +4 -3
  227. package/dist/src/review/review-followup.js +16 -4
  228. package/dist/src/review/review-precheck.js +14 -13
  229. package/dist/src/review/review-verdict.js +14 -0
  230. package/dist/src/review/review.js +55 -34
  231. package/dist/src/review/secret-leaks.js +127 -0
  232. package/dist/src/review/suite-rerun.js +4 -3
  233. package/dist/src/roles/legacy-role-ids.js +21 -0
  234. package/dist/src/roles/legacy-role-migration.js +62 -0
  235. package/dist/src/roles/loop-ids.js +19 -2
  236. package/dist/src/roles/role-catalog.js +109 -131
  237. package/dist/src/roles/role-guidance.js +64 -31
  238. package/dist/src/roles/role-labels.js +10 -0
  239. package/dist/src/roles/role-render.js +194 -18
  240. package/dist/src/roles/role-view.js +216 -17
  241. package/dist/src/roles/roles.js +16 -14
  242. package/dist/src/scheduling/backoff.js +29 -19
  243. package/dist/src/scheduling/claims.js +15 -8
  244. package/dist/src/scheduling/once-round.js +11 -11
  245. package/dist/src/scheduling/quiet-hours.js +25 -32
  246. package/dist/src/scheduling/scheduling.js +28 -25
  247. package/dist/src/scheduling/{work-landed-cache.js → work-merged-cache.js} +14 -14
  248. package/dist/src/status/status-budget.js +115 -0
  249. package/dist/src/status/status-data.js +72 -184
  250. package/dist/src/status/status-inbox.js +47 -0
  251. package/dist/src/status/status-install.js +27 -0
  252. package/dist/src/status/status-polls.js +50 -39
  253. package/dist/src/status/status-roles.js +105 -0
  254. package/dist/src/{fleet → team}/error-storm.js +10 -10
  255. package/dist/src/{fleet → team}/failure-spread.js +8 -8
  256. package/dist/src/{fleet → team}/orchestrator-info.js +84 -18
  257. package/dist/src/{fleet → team}/reclaim.js +5 -4
  258. package/dist/src/{fleet/fleet-hold.js → team/team-hold.js} +55 -21
  259. package/dist/src/{fleet/fleet-polls.js → team/team-polls.js} +116 -65
  260. package/dist/src/{fleet/fleet-state.js → team/team-state.js} +39 -38
  261. package/dist/src/text/datetime.js +42 -2
  262. package/dist/src/text/format.js +10 -1
  263. package/dist/src/text/markdown.js +1 -1
  264. package/dist/src/text/phrases.js +35 -34
  265. package/dist/src/text/suggest.js +3 -2
  266. package/dist/src/text/text.js +2 -2
  267. package/dist/src/tick/qa-coverage.js +2 -1
  268. package/dist/src/tick/stage-check.js +142 -25
  269. package/dist/src/tick/telemetry-digest.js +10 -9
  270. package/dist/src/tick/tick-apply.js +101 -72
  271. package/dist/src/tick/tick-detail-data.js +23 -22
  272. package/dist/src/tick/tick-detail.js +37 -24
  273. package/dist/src/tick/tick-finalize.js +29 -22
  274. package/dist/src/tick/tick-outcome.js +2 -1
  275. package/dist/src/tick/tick-prompt.js +94 -26
  276. package/dist/src/tick/tick-resume.js +29 -13
  277. package/dist/src/tick/tick-stage.js +67 -53
  278. package/dist/src/tick/tick-timing.js +60 -59
  279. package/dist/src/tick/tick-usage.js +53 -23
  280. package/dist/src/tick/tick-verdict.js +75 -10
  281. package/dist/src/ui/badges.js +113 -69
  282. package/dist/src/ui/gui/gui-client-boot.js +202 -29
  283. package/dist/src/ui/gui/gui-client-composer.js +102 -23
  284. package/dist/src/ui/gui/gui-client-drawer.js +330 -75
  285. package/dist/src/ui/gui/gui-client-history.js +94 -37
  286. package/dist/src/ui/gui/gui-client-labels.js +24 -0
  287. package/dist/src/ui/gui/gui-client-loops.js +90 -47
  288. package/dist/src/ui/gui/gui-client-markdown.js +1 -0
  289. package/dist/src/ui/gui/gui-client-model.js +60 -43
  290. package/dist/src/ui/gui/gui-client-operator.js +73 -43
  291. package/dist/src/ui/gui/gui-client-pending.js +34 -31
  292. package/dist/src/ui/gui/gui-client-report.js +330 -47
  293. package/dist/src/ui/gui/gui-client-settings.js +126 -18
  294. package/dist/src/ui/gui/gui-client-sound.js +2 -1
  295. package/dist/src/ui/gui/{gui-client-fleet.js → gui-client-team.js} +190 -74
  296. package/dist/src/ui/gui/gui-client.js +26 -20
  297. package/dist/src/ui/gui/gui-page.js +18 -19
  298. package/dist/src/ui/gui/gui-styles.js +12 -4
  299. package/dist/src/ui/progress-data.js +5 -3
  300. package/dist/src/ui/status-model.js +176 -102
  301. package/dist/src/ui/status-payload.js +114 -36
  302. package/dist/src/ui/status-render.js +121 -52
  303. package/dist/src/ui/team-alerts.js +341 -0
  304. package/dist/src/ui/tick-progress-model.js +14 -14
  305. package/dist/src/ui/tone.js +9 -8
  306. package/dist/src/ui/transcript-tail.js +1 -0
  307. package/dist/src/ui/transcript.js +23 -16
  308. package/dist/src/ui/tui/tui-app.js +1 -0
  309. package/dist/src/ui/tui/tui-backlog.js +85 -11
  310. package/dist/src/ui/tui/tui-editor-answer.js +97 -0
  311. package/dist/src/ui/tui/tui-editor-attach.js +107 -0
  312. package/dist/src/ui/tui/tui-editor-brief.js +107 -0
  313. package/dist/src/ui/tui/tui-editor-budget.js +155 -0
  314. package/dist/src/ui/tui/tui-editor-ctx.js +1 -0
  315. package/dist/src/ui/tui/tui-editor-entry.js +230 -0
  316. package/dist/src/ui/tui/tui-editor-interval.js +122 -0
  317. package/dist/src/ui/tui/tui-editor-notes.js +114 -0
  318. package/dist/src/ui/tui/tui-editor-pause.js +266 -0
  319. package/dist/src/ui/tui/tui-editor-prompt.js +95 -0
  320. package/dist/src/ui/tui/tui-editor-queued.js +98 -0
  321. package/dist/src/ui/tui/tui-editor-quiet.js +106 -0
  322. package/dist/src/ui/tui/tui-editor-retire.js +83 -0
  323. package/dist/src/ui/tui/tui-editor-search.js +73 -0
  324. package/dist/src/ui/tui/tui-editor-session.js +97 -0
  325. package/dist/src/ui/tui/tui-editors.js +152 -0
  326. package/dist/src/ui/tui/tui-frame.js +61 -57
  327. package/dist/src/ui/tui/tui-history.js +12 -7
  328. package/dist/src/ui/tui/tui-input.js +44 -50
  329. package/dist/src/ui/tui/tui-keymap.js +1 -0
  330. package/dist/src/ui/tui/tui-keys.js +87 -1821
  331. package/dist/src/ui/tui/tui-pane-activity.js +99 -0
  332. package/dist/src/ui/tui/tui-pane-backlog.js +224 -0
  333. package/dist/src/ui/tui/tui-pane-brief.js +30 -0
  334. package/dist/src/ui/tui/tui-pane-ctx.js +11 -0
  335. package/dist/src/ui/tui/tui-pane-health.js +124 -0
  336. package/dist/src/ui/tui/tui-pane-history.js +316 -0
  337. package/dist/src/ui/tui/tui-pane-pending.js +74 -0
  338. package/dist/src/ui/tui/tui-pane-queued.js +143 -0
  339. package/dist/src/ui/tui/tui-pane-report.js +122 -0
  340. package/dist/src/ui/tui/tui-pane-session.js +101 -0
  341. package/dist/src/ui/tui/tui-pane-transcript.js +150 -0
  342. package/dist/src/ui/tui/tui-pane.js +52 -28
  343. package/dist/src/ui/tui/tui-panes.js +294 -0
  344. package/dist/src/ui/tui/tui.js +44 -26
  345. package/dist/src/verdict/fix-claim.js +56 -33
  346. package/dist/src/verdict/no-change.js +19 -0
  347. package/dist/src/verdict/refusal.js +43 -11
  348. package/dist/src/verdict/reply-contract.js +17 -5
  349. package/dist/src/version.js +3 -2
  350. package/package.json +4 -2
  351. package/dist/src/agent/mcp/role-notes-server.js +0 -160
  352. package/dist/src/landing/backlog-conflicts.js +0 -116
  353. package/dist/src/landing/landing-drain.js +0 -204
  354. package/dist/src/landing/landing-pipeline.js +0 -96
  355. package/dist/src/landing/landing-queue-render.js +0 -56
  356. package/dist/src/landing/landing-slot.js +0 -256
  357. package/dist/src/landing/landing-vetting.js +0 -196
  358. package/dist/src/pi-extension/role-notes.js +0 -40
  359. package/dist/src/ui/fleet-alerts.js +0 -174
@@ -0,0 +1,234 @@
1
+ // vocabulary: plain
2
+ /**
3
+ * The dashboard's POST prompt-queueing command handlers (barrelled into
4
+ * gui/gui-endpoint-commands.ts, which routes to them from src/gui/gui-server.ts): prompt,
5
+ * prompt-role, prompt-cancel, prompt-edit, and prompt-top — the `checkedPromptImages` /
6
+ * `checkedPromptDefer` / `checkedPromptFields` validators they share ride with them. Each
7
+ * handler adapts HTTP onto the domain work one layer down (inbox-submit, inbox-cancel,
8
+ * inbox-edit, inbox-top, operator-intent) and touches no socket beyond its own `res`.
9
+ */
10
+ import { submitPrompt } from "../inbox/inbox-submit.js";
11
+ import { imagePromptGuidance } from "../inbox/inbox-submit.js";
12
+ import { promptImagesProblem } from "../inbox/inbox-attachments.js";
13
+ import { promptPreview } from "../inbox/inbox.js";
14
+ import { cancelQueuedFile } from "../inbox/inbox-cancel.js";
15
+ import { editQueuedFile } from "../inbox/inbox-edit.js";
16
+ import { topQueuedFile } from "../inbox/inbox-top.js";
17
+ import { parseDurationMs } from "../cli/cli-args.js";
18
+ import { PAUSE_FOR_MAX_MS, submitRolePromptAndWake } from "../operator/operator-intent.js";
19
+ import { DIRECTOR_ROLE } from "../roles/roles.js";
20
+ import { rejectBadQueueFileName, rejectBadRole, requirePromptText } from "./gui-args.js";
21
+ import { badRequest, OK_GONE, sendJson } from "./http-body.js";
22
+ import { errorMessage, gotSuffix } from "../text/text.js";
23
+ import { readPostBody, readRoleBody } from "./gui-endpoint-common.js";
24
+ /** Handle POST /api/prompt: queue a director prompt. An over-long prompt
25
+ * (DIRECTOR_PROMPT_MAX_CHARS) is a user-input error, not a server fault: the shared length
26
+ * rule answers 400 — an unexpected submit failure (EACCES) still reaches the server's outer
27
+ * catch as the 500 its gui-server-transport test pins. An optional images array (the composer's
28
+ * dropped/pasted attachments) is validated before anything is queued: a bad image — wrong
29
+ * extension, too many, undecodable, oversized — answers 400 naming the rule, with no queue
30
+ * file and no image written. */
31
+ export async function handlePrompt(req, res, root) {
32
+ const body = await readPostBody(req, res, '{"text": "..."}');
33
+ if (!body)
34
+ return;
35
+ const fields = checkedPromptFields(res, body);
36
+ if (!fields)
37
+ return;
38
+ submitPrompt(root, fields.text, fields.images, fields.notBeforeMs);
39
+ sendJson(res, 200, { ok: true });
40
+ }
41
+ /** The images field of a prompt endpoint's body, validated before anything touches the disk.
42
+ * Three outcomes, deliberately distinct: `undefined` — no images field, a text-only submit,
43
+ * continue without attachments; an array — the field was present and valid, attach it (a
44
+ * present-but-empty array is fine and saves nothing); `null` — the field was present and
45
+ * invalid, the 400 naming the rule is already sent, stop. The absent and invalid cases must
46
+ * not share a value: a text-only POST is the common path and must proceed. */
47
+ function checkedPromptImages(res, body) {
48
+ if (body.images === undefined)
49
+ return undefined;
50
+ const problem = promptImagesProblem(body.images);
51
+ if (problem) {
52
+ badRequest(res, problem);
53
+ return null;
54
+ }
55
+ return body.images;
56
+ }
57
+ /** The defer field of a prompt endpoint's body, validated before anything touches the disk.
58
+ * Three outcomes, deliberately distinct like checkedPromptImages: `undefined` — no defer
59
+ * field, a prompt that runs as soon as its turn comes; a number — the epoch ms the prompt is
60
+ * deliverable from (`Date.now() + ms`); `null` — the field was present but not a duration
61
+ * this grammar accepts, or its duration is over the 90d cap, and the 400 naming the rule is
62
+ * already sent, so stop. The grammar is cli-args.ts's parseDurationMs — the same one
63
+ * `tumwater prompt --at` and `pause --for` accept — so a deferral spelled here computes the
64
+ * same `tumwater:not-before` marker the CLI writes. */
65
+ function checkedPromptDefer(res, body) {
66
+ if (body.defer === undefined)
67
+ return undefined;
68
+ const ms = typeof body.defer === "string" ? parseDurationMs(body.defer) : null;
69
+ if (ms === null) {
70
+ badRequest(res, `defer needs a duration like 45s, 90m, 1h30m, or 2d${gotSuffix(body.defer)}`);
71
+ return null;
72
+ }
73
+ if (ms > PAUSE_FOR_MAX_MS) {
74
+ badRequest(res, `defer is capped at 90d${gotSuffix(body.defer)}`);
75
+ return null;
76
+ }
77
+ return Date.now() + ms;
78
+ }
79
+ /** The prompt endpoints' shared body tail: requirePromptText (400 on a null return) then
80
+ * checkedPromptImages then checkedPromptDefer (400 on a null return from either), so
81
+ * /api/prompt and /api/prompt-role read the same three fields in the same order and a body
82
+ * wrong in several still answers the text problem first. Null means a 400 is already sent;
83
+ * callers keep their own `if (!fields) return` bail. */
84
+ function checkedPromptFields(res, body, role = DIRECTOR_ROLE) {
85
+ const text = requirePromptText(res, body, role);
86
+ if (text === null)
87
+ return null;
88
+ const images = checkedPromptImages(res, body);
89
+ if (images === null)
90
+ return null;
91
+ const notBeforeMs = checkedPromptDefer(res, body);
92
+ if (notBeforeMs === null)
93
+ return null;
94
+ return { text, images, notBeforeMs };
95
+ }
96
+ /** Handle POST /api/prompt-role: queue a prompt for one loop — the dashboard's per-row
97
+ * 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).
100
+ * The role validates exactly like /api/transcript (the shared rejectBadRole wording), and the
101
+ * body discipline is /api/prompt's: readJsonObject → 400 malformed/non-object, 413 oversized,
102
+ * text a non-empty string within the shared length rule → 400 otherwise — plus the same
103
+ * optional images array and its validate-before-anything-is-written discipline. */
104
+ export async function handlePromptRole(req, res, root) {
105
+ const body = await readRoleBody(req, res, '{"role": "feature", "text": "..."}', root);
106
+ if (!body)
107
+ return;
108
+ const role = body.role;
109
+ const fields = checkedPromptFields(res, body, role);
110
+ if (!fields)
111
+ return;
112
+ // 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
114
+ // 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
116
+ // notice rides in the confirmation the composer flashes.
117
+ let notice = "";
118
+ if (fields.images && fields.images.length > 0) {
119
+ const guidance = imagePromptGuidance(root, role);
120
+ if (guidance.refusal) {
121
+ badRequest(res, guidance.refusal);
122
+ return;
123
+ }
124
+ notice = guidance.notice ?? "";
125
+ }
126
+ const message = [
127
+ submitRolePromptAndWake(root, role, fields.text, fields.images, fields.notBeforeMs),
128
+ notice,
129
+ ]
130
+ .filter(Boolean)
131
+ .join("\n");
132
+ sendJson(res, 200, { ok: true, message });
133
+ }
134
+ /** Handle POST /api/prompt-cancel: retract one queued prompt addressed by its queue file —
135
+ * the dashboard's per-row cancel affordance, the file-addressed twin of the CLI's
136
+ * position-addressed `prompt --cancel`. The role is optional (absent cancels from the
137
+ * director's queue) and validates exactly like /api/transcript; a file name failing the
138
+ * 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
140
+ * dequeued the prompt, which is data for the flash line, not a server fault. Same body
141
+ * discipline as /api/prompt (readJsonObject → 400 malformed/non-object, 413 oversized). */
142
+ export async function handlePromptCancel(req, res, root) {
143
+ const body = await readPostBody(req, res, '{"role": "feature", "file": "<stamp>-<seq>-<pid>.md"}');
144
+ if (!body)
145
+ return;
146
+ if (body.role !== undefined && rejectBadRole(root, res, body.role))
147
+ return;
148
+ const role = body.role === undefined ? DIRECTOR_ROLE : body.role;
149
+ if (rejectBadQueueFileName(res, body.file))
150
+ return;
151
+ const outcome = cancelQueuedFile(root, role, body.file);
152
+ sendJson(res, 200, outcome.status === "cancelled"
153
+ ? { ok: true, status: "cancelled", preview: promptPreview(outcome.text) }
154
+ : OK_GONE);
155
+ }
156
+ /** Handle POST /api/prompt-edit: rewrite one queued prompt addressed by its queue file — the
157
+ * dashboard's per-row edit affordance, the file-addressed twin of the CLI's position-addressed
158
+ * `prompt --edit`. The role is optional (absent edits the director's queue) and validates
159
+ * exactly like /api/prompt-cancel; a file name failing the shared rejectBadQueueFileName
160
+ * guard is a user-input error answered 400 with nothing touched on disk. Text
161
+ * and images validate through the same checkedPromptFields /api/prompt and /api/prompt-role
162
+ * use; editQueuedFile throws for a bad name or an over-cap text — both user-input errors
163
+ * 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
165
+ * prompt, which is data for the flash line, not a server error; an edit answers 200 with the
166
+ * old and new operator-facing text the client flashes. Same body discipline as /api/prompt
167
+ * (readJsonObject → 400 malformed/non-object, 413 oversized). */
168
+ export async function handlePromptEdit(req, res, root) {
169
+ const body = await readPostBody(req, res, '{"role": "qa", "file": "<stamp>.md", "text": "..."}');
170
+ if (!body)
171
+ return;
172
+ if (body.role !== undefined && rejectBadRole(root, res, body.role))
173
+ return;
174
+ const role = body.role === undefined ? DIRECTOR_ROLE : body.role;
175
+ if (rejectBadQueueFileName(res, body.file))
176
+ return;
177
+ const fields = checkedPromptFields(res, body, role);
178
+ if (!fields)
179
+ return;
180
+ // Image-input gate (plans/image-input.md "Attachments", part 5/5), the edit twin of
181
+ // 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
183
+ // submitRolePrompt enforces — before anything is written.
184
+ if (fields.images && fields.images.length > 0) {
185
+ const guidance = imagePromptGuidance(root, role);
186
+ if (guidance.refusal) {
187
+ badRequest(res, guidance.refusal);
188
+ return;
189
+ }
190
+ }
191
+ let outcome;
192
+ try {
193
+ outcome = editQueuedFile(root, role, body.file, fields.text, fields.images);
194
+ }
195
+ catch (err) {
196
+ badRequest(res, errorMessage(err));
197
+ return;
198
+ }
199
+ sendJson(res, 200, outcome.status === "edited"
200
+ ? { ok: true, status: "edited", oldText: outcome.oldText, newText: outcome.newText }
201
+ : OK_GONE);
202
+ }
203
+ /** Handle POST /api/prompt-top: move one queued prompt to the head of its role's queue,
204
+ * addressed by its queue file — the dashboard's per-row Move to top affordance, the
205
+ * file-addressed twin of the CLI's position-addressed `prompt --top`. The role is optional
206
+ * (absent moves in the director's queue) and validates exactly like /api/prompt-cancel; a
207
+ * file name failing the shared rejectBadQueueFileName guard is a user-input error answered
208
+ * 400 with nothing touched on disk. A vanished file answers 200 { status: "gone" } and the
209
+ * queue head answers 200 { status: "already_first" } — both data for the flash line, not
210
+ * server faults, so a stale poll can never reorder the wrong prompt. Same body discipline
211
+ * as /api/prompt (readJsonObject → 400 malformed/non-object, 413 oversized). */
212
+ export async function handlePromptTop(req, res, root) {
213
+ const body = await readPostBody(req, res, '{"role": "qa", "file": "<stamp>.md"}');
214
+ if (!body)
215
+ return;
216
+ if (body.role !== undefined && rejectBadRole(root, res, body.role))
217
+ return;
218
+ const role = body.role === undefined ? DIRECTOR_ROLE : body.role;
219
+ if (rejectBadQueueFileName(res, body.file))
220
+ return;
221
+ let outcome;
222
+ try {
223
+ outcome = topQueuedFile(root, role, body.file);
224
+ }
225
+ catch (err) {
226
+ badRequest(res, errorMessage(err));
227
+ return;
228
+ }
229
+ sendJson(res, 200, outcome.status === "topped"
230
+ ? { ok: true, status: "topped", preview: outcome.preview }
231
+ : outcome.status === "already_first"
232
+ ? { ok: true, status: "already_first" }
233
+ : OK_GONE);
234
+ }
@@ -8,10 +8,10 @@ import http from "node:http";
8
8
  import { GUI_PAGE } from "../ui/gui/gui-page.js";
9
9
  import { statusPayload } from "../ui/status-payload.js";
10
10
  import { captureStartupBuild, createReloadWatch, reexecSelf, watchReloadSupervisor, } from "../redeploy/self-reload.js";
11
- import { warnEvent } from "../events/events.js";
11
+ import { warnEvent, TUMWATER_SOURCE } from "../events/events.js";
12
12
  import { errorMessage } from "../text/text.js";
13
- import { handleBacklog, handleBrief, handleConfig, handleDiff, handleFailures, handleReport, handleHistory, handleLogs, handlePromptText, handleTick, handleRole, handleTranscript, handleDoctor, } from "./gui-endpoints.js";
14
- import { handleAbort, handleBriefEdit, handleBudget, handleConfigSet, handleConfigUnset, handlePause, handlePauseRole, handlePrompt, handleBacklogFile, handleBacklogCancel, handleBacklogEdit, handlePromptCancel, handlePromptEdit, handlePromptRole, handleQuestionAnswer, handleReclaim, handleRestart, handleStopSchedule, handleResetCounters, handleRoleNotes, handleWake, handleWakeCancel, } from "./gui-endpoint-commands.js";
13
+ import { handleBacklog, handleBrief, handleConfig, handleDiff, handleFailures, handleReport, handleHistory, handleLogs, handlePromptText, handleTick, handleRole, handleTranscript, handleDoctor, handleRetireList, } from "./gui-endpoints.js";
14
+ import { handleAbort, handleBriefEdit, handleBudget, handleConfigSet, handleConfigUnset, handleRoleCap, handleRoleEnable, handleRoleDisable, handleRoleInstances, handleRoleInterval, handleRoleInstructions, handleRoleModel, handleRoleQuietHours, handleNotifyTest, handlePause, handlePauseRole, handlePrompt, handleBacklogFile, handleBacklogCancel, handleBacklogRelease, handleBacklogTop, handleBacklogEdit, handlePromptCancel, handlePromptEdit, handlePromptRole, handlePromptTop, handleQuestionAnswer, handleReclaim, handleRestart, handleStopSchedule, handleResetCounters, handleRoleNotes, handleRetire, handleWake, handleWakeCancel, } from "./gui-endpoint-commands.js";
15
15
  import { sendJson } from "./http-body.js";
16
16
  /** Constant-time credential comparison for the shared-token gate: `timingSafeEqual` throws
17
17
  * on unequal lengths, so the length equality is the guard. A naive `===` string compare
@@ -50,7 +50,10 @@ function crossOriginRequest(req) {
50
50
  const raw = req.headers.origin;
51
51
  if (raw === undefined)
52
52
  return false; // no Origin: not a browser — allow
53
- const origin = Array.isArray(raw) ? raw[0] : raw;
53
+ // A `typeof` guard narrows to a concrete `string[]` (so `raw[0]` is `string | undefined`
54
+ // under noUncheckedIndexedAccess) instead of `Array.isArray`'s `any[]`, whose `[0]` is `any`;
55
+ // an empty array would be undefined either way, so `?? ""` only spells the first value out.
56
+ const origin = typeof raw === "string" ? raw : (raw[0] ?? "");
54
57
  if (origin === "null")
55
58
  return true;
56
59
  try {
@@ -99,7 +102,7 @@ export function loopbackHostAllowed(hostHeader) {
99
102
  * handler's purpose. The warning is best-effort, the same policy the cleanup warn sites apply. */
100
103
  function recordServerError(root, err) {
101
104
  try {
102
- warnEvent(root, "harness", `dashboard server error: ${errorMessage(err)}`);
105
+ warnEvent(root, TUMWATER_SOURCE, `dashboard server error: ${errorMessage(err)}`);
103
106
  }
104
107
  catch {
105
108
  // The log is another process's to fix; the dashboard stays up either way.
@@ -107,7 +110,7 @@ function recordServerError(root, err) {
107
110
  }
108
111
  /** Start the dashboard server. Binds to 127.0.0.1 by default; with `allInterfaces` it
109
112
  * binds the unspecified address (every interface, IPv4 and IPv6), making the dashboard —
110
- * including the director prompt box, which anyone reaching it can use to steer the fleet —
113
+ * including the director prompt box, which anyone reaching it can use to steer the team —
111
114
  * available to the whole network. There is no authentication by default — exposing it is
112
115
  * the caller's deliberate choice — and an optional shared token (`gui --token <secret>`,
113
116
  * passed as `token`) gates every route: requests must carry it as `Authorization: Bearer
@@ -145,30 +148,44 @@ export function startGui(root, port, allInterfaces = false, token = "", watch =
145
148
  "GET /api/logs": (_req, res, target) => handleLogs(target.searchParams, res, root),
146
149
  "GET /api/transcript": (_req, res, target) => handleTranscript(target.searchParams, res, root),
147
150
  "GET /api/prompt-text": (_req, res, target) => handlePromptText(target.searchParams, res, root),
148
- "GET /api/tick": (_req, res, target) => handleTick(target.searchParams, res, root),
151
+ "GET /api/run": (_req, res, target) => handleTick(target.searchParams, res, root),
149
152
  "GET /api/role": (_req, res, target) => handleRole(target.searchParams, res, root),
150
153
  "GET /api/diff": (_req, res, target) => handleDiff(target.searchParams, res, root),
151
154
  "GET /api/backlog": (_req, res, target) => handleBacklog(target.searchParams, res, root),
152
155
  "GET /api/config": (_req, res) => handleConfig(res, root),
153
156
  "GET /api/brief": (_req, res) => handleBrief(res, root),
154
157
  "GET /api/doctor": (_req, res) => handleDoctor(res, root),
158
+ "GET /api/retire": (_req, res) => handleRetireList(res, root),
155
159
  "POST /api/brief": (req, res) => handleBriefEdit(req, res, root),
156
160
  "POST /api/config-set": (req, res) => handleConfigSet(req, res, root),
157
161
  "POST /api/config-unset": (req, res) => handleConfigUnset(req, res, root),
162
+ "POST /api/notify-test": (req, res) => handleNotifyTest(req, res, root),
158
163
  "POST /api/prompt": (req, res) => handlePrompt(req, res, root),
159
164
  "POST /api/prompt-role": (req, res) => handlePromptRole(req, res, root),
160
165
  "POST /api/backlog-file": (req, res) => handleBacklogFile(req, res, root),
161
166
  "POST /api/backlog-cancel": (req, res) => handleBacklogCancel(req, res, root),
167
+ "POST /api/backlog-release": (req, res) => handleBacklogRelease(req, res, root),
168
+ "POST /api/backlog-top": (req, res) => handleBacklogTop(req, res, root),
162
169
  "POST /api/backlog-edit": (req, res) => handleBacklogEdit(req, res, root),
163
170
  "POST /api/prompt-cancel": (req, res) => handlePromptCancel(req, res, root),
171
+ "POST /api/prompt-top": (req, res) => handlePromptTop(req, res, root),
164
172
  "POST /api/prompt-edit": (req, res) => handlePromptEdit(req, res, root),
165
173
  "POST /api/question-answer": (req, res) => handleQuestionAnswer(req, res, root),
166
174
  "POST /api/budget": (req, res) => handleBudget(req, res, root),
175
+ "POST /api/role-cap": (req, res) => handleRoleCap(req, res, root),
176
+ "POST /api/role-enable": (req, res) => handleRoleEnable(req, res, root),
177
+ "POST /api/role-disable": (req, res) => handleRoleDisable(req, res, root),
178
+ "POST /api/role-interval": (req, res) => handleRoleInterval(req, res, root),
179
+ "POST /api/role-instances": (req, res) => handleRoleInstances(req, res, root),
180
+ "POST /api/role-instructions": (req, res) => handleRoleInstructions(req, res, root),
181
+ "POST /api/role-model": (req, res) => handleRoleModel(req, res, root),
182
+ "POST /api/role-quiet-hours": (req, res) => handleRoleQuietHours(req, res, root),
167
183
  "POST /api/pause": (req, res) => handlePause(req, res, root),
168
184
  "POST /api/wake": (req, res) => handleWake(req, res, root),
169
185
  "POST /api/wake-cancel": (req, res) => handleWakeCancel(req, res, root),
170
186
  "POST /api/reset-counters": (req, res) => handleResetCounters(req, res, root),
171
187
  "POST /api/role-notes": (req, res) => handleRoleNotes(req, res, root),
188
+ "POST /api/retire": (req, res) => handleRetire(req, res, root),
172
189
  "POST /api/restart": (req, res) => handleRestart(req, res, root),
173
190
  "POST /api/stop-schedule": (req, res) => handleStopSchedule(req, res, root),
174
191
  "POST /api/reclaim": (req, res) => handleReclaim(req, res, root),
@@ -210,7 +227,7 @@ export function startGui(root, port, allInterfaces = false, token = "", watch =
210
227
  // Cross-origin gate, ahead of every state-changing route: a hostile webpage can POST
211
228
  // to the open localhost server with a CORS-safelisted text/plain body (no preflight,
212
229
  // no readable response) and the side effect alone is the payload — a forged director
213
- // prompt, a fleet pause, an abort. Browsers mark every cross-site POST with an Origin
230
+ // prompt, a team pause, an abort. Browsers mark every cross-site POST with an Origin
214
231
  // naming the hostile server, so one header check here refuses the forgery while the
215
232
  // dashboard's own POSTs (same-host Origin) and non-browser clients (no Origin) pass.
216
233
  if (req.method === "POST" && crossOriginRequest(req)) {
@@ -0,0 +1,164 @@
1
+ /**
2
+ * The dashboard's POST team-pause/wake command handlers (barrelled into
3
+ * gui/gui-endpoint-commands.ts, which routes to them from src/gui/gui-server.ts): pause,
4
+ * pause-role, wake, and wake-cancel — with the `pauseForSecondsMs` / `pauseReasonField` /
5
+ * `wakeInSecondsMs` body rules they share. Each handler adapts HTTP onto the domain work one
6
+ * layer down (team/team-state, operator-intent) and touches no socket beyond its own `res`.
7
+ */
8
+ import { baseRoleOf, INSTANCE_ROLES } from "../roles/loop-ids.js";
9
+ import { normalizePauseReason, pauseTeam, pauseRole, resumeTeam, resumeRole } from "../team/team-state.js";
10
+ import { cancelWakes, PAUSE_FOR_MAX_MS, requestWake } from "../operator/operator-intent.js";
11
+ import { requirePausedFlag, validRoleIds } from "./gui-args.js";
12
+ import { badRequest, sendJson } from "./http-body.js";
13
+ import { gotSuffix } from "../text/text.js";
14
+ import { readPostBody, readRoleBody } from "./gui-endpoint-common.js";
15
+ /** The shared optional-seconds rule behind the timed pause's `forSeconds` and the scheduled
16
+ * wake/stop's `inSeconds`: the field is absent (undefined) or a positive finite number of
17
+ * seconds within the CLI's 90-day cap — the two parallel validators used to restate this
18
+ * smell, one per field name. Its null return means the 400 is already sent and the caller must
19
+ * stop; its number return is the delay in milliseconds, which the timed-pause callers add
20
+ * their own clock to (the marker's `until` deadline) and the wake/stop-schedule callers use
21
+ * directly. `capHint` rides after the cap message for a field whose capped form earns an extra
22
+ * pointer (pause's standing-pause note); the messages name the field so pause (`forSeconds`)
23
+ * and wake/stop-schedule (`inSeconds`) each read correctly. */
24
+ function boundedSecondsMs(res, body, field, capHint = "") {
25
+ const seconds = body[field];
26
+ if (seconds === undefined)
27
+ return undefined;
28
+ if (typeof seconds !== "number" || !Number.isFinite(seconds) || seconds <= 0) {
29
+ badRequest(res, `${field} must be a positive number of seconds${gotSuffix(seconds)}`);
30
+ return null;
31
+ }
32
+ if (seconds * 1000 > PAUSE_FOR_MAX_MS) {
33
+ badRequest(res, `${field} is capped at ${PAUSE_FOR_MAX_MS / 1000} seconds (90 days)${gotSuffix(seconds)}${capHint}`);
34
+ return null;
35
+ }
36
+ return Math.round(seconds * 1000);
37
+ }
38
+ /** The shared `forSeconds` rule of /api/pause and /api/pause-role: undefined when absent, the
39
+ * deadline in epoch milliseconds when valid, null after sending the 400 (only with
40
+ * `paused: true`, a positive finite number, within the CLI's 90-day cap — the numeric body
41
+ * rides boundedSecondsMs). */
42
+ function pauseForSecondsMs(res, body, pausing) {
43
+ const forSeconds = body.forSeconds;
44
+ if (forSeconds === undefined)
45
+ return undefined;
46
+ if (!pausing) {
47
+ badRequest(res, "forSeconds only applies when pausing (paused: true)");
48
+ return null;
49
+ }
50
+ const ms = boundedSecondsMs(res, body, "forSeconds", " — pause without it for a standing pause");
51
+ return ms === null || ms === undefined ? ms : Date.now() + ms;
52
+ }
53
+ /** The optional `reason` of /api/pause: undefined when absent or blank, the raw string when
54
+ * given with `paused: true` (pauseTeam folds and caps it), null after sending the 400. */
55
+ function pauseReasonField(res, body, pausing) {
56
+ const reason = body.reason;
57
+ if (reason === undefined)
58
+ return undefined;
59
+ if (!pausing) {
60
+ badRequest(res, "reason only applies when pausing (paused: true)");
61
+ return null;
62
+ }
63
+ if (typeof reason !== "string") {
64
+ badRequest(res, `reason must be a string${gotSuffix(reason)}`);
65
+ return null;
66
+ }
67
+ return reason.trim() === "" ? undefined : reason;
68
+ }
69
+ /** Handle POST /api/pause: the dashboard's pause control — the same operator gate
70
+ * `tumwater pause [--for <duration>]` and `resume` write, via team/team-state.ts's shared writers
71
+ * (pauseTeam/resumeTeam) so the CLI and the GUI cannot drift on the marker's format or
72
+ * idempotence. Same body discipline as /api/prompt and /api/budget (readJsonObject → 400
73
+ * malformed/non-object, 413 oversized); the target state is explicit (`paused: true|false`)
74
+ * rather than a toggle, so a retried request is idempotent. An optional `forSeconds` makes it
75
+ * a timed pause (the marker's `until`, capped like the CLI's `--for`); it only accompanies
76
+ * `paused: true`, as does an optional `reason` (the CLI's `pause --reason`, stored folded and
77
+ * capped by pauseTeam; the 200 body echoes what was stored). */
78
+ export async function handlePause(req, res, root) {
79
+ const body = await readPostBody(req, res, '{"paused": true, "forSeconds": 3600}');
80
+ if (!body)
81
+ return;
82
+ const value = requirePausedFlag(res, body);
83
+ if (value === null)
84
+ return;
85
+ const untilMs = pauseForSecondsMs(res, body, value);
86
+ if (untilMs === null)
87
+ return;
88
+ const reason = pauseReasonField(res, body, value);
89
+ if (reason === null)
90
+ return;
91
+ if (value)
92
+ pauseTeam(root, untilMs, reason);
93
+ else
94
+ resumeTeam(root);
95
+ const stored = normalizePauseReason(reason);
96
+ sendJson(res, 200, { ok: true, paused: value, ...(untilMs === undefined ? {} : { until: untilMs }), ...(stored ? { reason: stored } : {}) });
97
+ }
98
+ /** The `inSeconds` rule of /api/wake: undefined when absent, the delay in milliseconds when
99
+ * valid, null after sending the 400 (a positive finite number within the CLI's 90-day cap).
100
+ * A duration, not a deadline: requestWake takes `inMs` and reads its own clock. */
101
+ export function wakeInSecondsMs(res, body) {
102
+ return boundedSecondsMs(res, body, "inSeconds");
103
+ }
104
+ /** Handle POST /api/wake: the dashboard's per-row wake control — the same marker-writing
105
+ * core `tumwater wake` calls (requestWake), so the CLI and the GUI cannot drift on the
106
+ * state-file edits or the marker. `{}`/a missing role targets every configured role (the
107
+ * CLI's all-roles default); a given role validates exactly like /api/transcript. Same body
108
+ * discipline as /api/pause (readJsonObject → 400 malformed/non-object, 413 oversized). An
109
+ * optional `inSeconds` schedules the wake for later, like `tumwater wake --in` (same
110
+ * 90-day cap). */
111
+ export async function handleWake(req, res, root) {
112
+ const body = await readRoleBody(req, res, '{"role": "feature"}', root, true);
113
+ if (!body)
114
+ return;
115
+ const inMs = wakeInSecondsMs(res, body);
116
+ if (inMs === null)
117
+ return;
118
+ sendJson(res, 200, {
119
+ ok: true,
120
+ message: requestWake(root, body.role === undefined ? validRoleIds(root) : [body.role], inMs),
121
+ });
122
+ }
123
+ /** Handle POST /api/wake-cancel: the dashboard's cancel of pending scheduled wakes — the same
124
+ * core `tumwater wake --cancel` calls (cancelWakes). A missing role cancels every pending wake;
125
+ * a given role validates like /api/wake and drops only its share. */
126
+ export async function handleWakeCancel(req, res, root) {
127
+ const body = await readRoleBody(req, res, '{"role": "feature"}', root, true);
128
+ if (!body)
129
+ return;
130
+ sendJson(res, 200, { ok: true, message: cancelWakes(root, body.role) });
131
+ }
132
+ /** Resume a row: lift every marker key that pauses this loop id (its own, its base role's, and
133
+ * instance 1's `<role>-1`), since the row shows Resume whenever `loopPaused` holds and the key
134
+ * actually in the marker may differ from the row's id. Reports whether any key was lifted. */
135
+ function resumeLoopKeys(root, loopId) {
136
+ const keys = new Set([loopId, baseRoleOf(loopId), ...(INSTANCE_ROLES.has(loopId) ? [`${loopId}-1`] : [])]);
137
+ let changed = false;
138
+ for (const key of keys)
139
+ changed = resumeRole(root, key) || changed;
140
+ return changed;
141
+ }
142
+ /** Handle POST /api/pause-role: the dashboard's per-row pause/resume control — the same
143
+ * marker-writing core `tumwater pause --role` / `resume --role` call (pauseRole/resumeRole),
144
+ * so the CLI and the GUI cannot drift on the marker's format or idempotence. The target state
145
+ * is explicit (`paused: true|false`) like /api/pause, so a retried request is idempotent; the
146
+ * team-wide pause composes freely — its gate is checked first at scheduling, and the role
147
+ * marker still records the operator's per-row intent. Same body discipline as /api/wake
148
+ * (readJsonObject → 400 malformed/non-object, 413 oversized; the role validates through the
149
+ * shared rejectBadRole wording). */
150
+ export async function handlePauseRole(req, res, root) {
151
+ const body = await readRoleBody(req, res, '{"role": "feature", "paused": true}', root);
152
+ if (!body)
153
+ return;
154
+ const value = requirePausedFlag(res, body);
155
+ if (value === null)
156
+ return;
157
+ const untilMs = pauseForSecondsMs(res, body, value);
158
+ if (untilMs === null)
159
+ return;
160
+ const changed = value
161
+ ? pauseRole(root, body.role, untilMs)
162
+ : resumeLoopKeys(root, body.role);
163
+ sendJson(res, 200, { ok: true, changed, paused: value, ...(untilMs === undefined ? {} : { until: untilMs }) });
164
+ }
@@ -6,6 +6,22 @@ export function sendJson(res, status, body) {
6
6
  res.writeHead(status, { "content-type": "application/json" });
7
7
  res.end(JSON.stringify(body));
8
8
  }
9
+ /** Send a 400 Bad Request carrying a single client-error message — the one shape every /api
10
+ * handler's validation/authoring failure replies with ({ error: "..." }), so the status and
11
+ * the `{ error }` wrapper live here instead of being open-coded at each call site. */
12
+ export function badRequest(res, message) {
13
+ sendJson(res, 400, { error: message });
14
+ }
15
+ /** Send 200 with the {"ok":true,"status":"gone"} body every file-addressed handler that answers
16
+ * for a vanished entry (the loop already dequeued it) replies with — data for the UI's flash
17
+ * line, not a server fault — so the status and the `{ ok, status }` wrapper live here instead
18
+ * of being open-coded at each call site. */
19
+ export function okGone(res) {
20
+ sendJson(res, 200, OK_GONE);
21
+ }
22
+ /** The {"ok":true,"status":"gone"} body okGone sends, also exposed for the couple of handlers
23
+ * that wrap it as one branch of a larger 200 reply (prompt-cancel/prompt-edit). */
24
+ export const OK_GONE = { ok: true, status: "gone" };
9
25
  /** Max request body for the /api POST endpoints, in wire bytes. Sized for the prompt
10
26
  * endpoints' image attachments: 4 images × 5 MiB × 4/3 base64 expansion ≈ 27 MiB must fit one
11
27
  * POST, with headroom — the cap exists to bound memory on a local dashboard, not to be tight.
@@ -130,7 +146,7 @@ export async function readJsonObject(req, res, example) {
130
146
  // because it sends a different status.
131
147
  const parsed = parseJsonObject(body);
132
148
  if (!parsed) {
133
- sendJson(res, 400, { error: `body must be a JSON object like ${example}` });
149
+ badRequest(res, `body must be a JSON object like ${example}`);
134
150
  return null;
135
151
  }
136
152
  return parsed;