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
@@ -1,671 +1,18 @@
1
1
  /**
2
- * The dashboard's POST operator endpoint handlers (src/gui/gui-server.ts routes to them): prompt,
3
- * prompt-role, prompt-cancel, prompt-edit, budget, config-set, config-unset, pause, wake, restart, abort,
4
- * pause-role —
5
- * everything that writes state or queues work — plus the shared request-body helpers
6
- * (readPostBody, readRoleBody, checkedPromptImages, checkedPromptFields) each mutating handler
7
- * guards its inputs with. The GET data endpoints (transcript, backlog, report, failures, history,
8
- * tick, diff, config) stay in gui/gui-endpoints.ts; each handler answers its request and touches
9
- * no socket beyond its own `res`,
10
- * and the domain work lives one layer down (inbox.ts, inbox-submit.ts, config-write.ts,
11
- * fleet/fleet-state.ts, operator/operator-intent.ts) — this module only adapts HTTP onto it.
2
+ * The dashboard's POST operator endpoint namespace (src/gui/gui-server.ts routes to these
3
+ * handlers): one file per domain, each adapting HTTP onto the domain work one layer down, all
4
+ * gathered here. The GET data endpoints (transcript, backlog, report, failures, history, tick,
5
+ * diff, config) stay in gui/gui-endpoints.ts; the shared request-body helpers
6
+ * (readPostBody, readRoleBody, sendRequestResult) live in gui/gui-endpoint-common.ts. The
7
+ * per-domain command modules, each a thin HTTP adapter touching no socket beyond its own `res`:
8
+ * prompt queueing/cancel/edit (gui/gui-prompt-commands), backlog entry file/cancel/edit +
9
+ * question-answer (gui/gui-backlog-commands), config set/unset + budget + notify-test
10
+ * (gui/gui-config-commands), team pause/role-pause/wake (gui/gui-team-commands),
11
+ * and operator lifecycle restart/reclaim/abort/reset-counters/stop-schedule/role-notes/
12
+ * retire/brief-edit (gui/gui-operator-commands).
12
13
  */
13
- import { fileEntryWithBody, normalizeEntryBody, OPERATOR_FILE_KINDS, tryCancelEntry, tryEditEntry, bodyStructureProblem } from "../backlog/backlog-write.js";
14
- import { promptPreview } from "../inbox/inbox.js";
15
- import { cancelQueuedFile } from "../inbox/inbox-cancel.js";
16
- import { editQueuedFile } from "../inbox/inbox-edit.js";
17
- import { promptLengthProblem, submitPrompt } from "../inbox/inbox-submit.js";
18
- import { promptImagesProblem } from "../inbox/inbox-attachments.js";
19
- import { tryAnswerQuestion, UNCLOSED_FENCE_MESSAGE } from "../cli/question-commands.js";
20
- import { parseDurationMs } from "../cli/cli-args.js";
21
- import { checkDailyBudgetUsd, setConfigKey, setDailyBudgetUsd, unsetConfigKey } from "../config/config-write.js";
22
- import { normalizePauseReason, pauseFleet, pauseRole, resumeFleet, resumeRole } from "../fleet/fleet-state.js";
23
- import { PAUSE_FOR_MAX_MS, cancelWakes, requestAbort, requestResetCounters, requestRestart, requestWake, runManualReclaim, submitRolePromptAndWake } from "../operator/operator-intent.js";
24
- import { DIRECTOR_ROLE } from "../roles/roles.js";
25
- import { saveRoleNote } from "../agent/role-notes-write.js";
26
- import { roleNotesPath } from "../paths.js";
27
- import { rejectBadQueueFileName, rejectBadRole, requireConfigKey, requirePausedFlag, requirePromptText, validRoleIds } from "./gui-args.js";
28
- import { readJsonObject, sendJson } from "./http-body.js";
29
- import { collapseWhitespace, errorMessage, gotSuffix } from "../text/text.js";
30
- /** Every POST handler's shared opening: readJsonObject reads the body and — when it is
31
- * oversized, malformed, or not a JSON object — has already written the 413/400 itself. Its
32
- * null return therefore means the response is sent and the handler must stop without touching
33
- * it again; each handler used to restate that contract in its own inline comment, and this is
34
- * the one home for it. Handlers keep their own `if (!body) return` bail so the stop stays
35
- * visible in their flow. */
36
- async function readPostBody(req, res, example) {
37
- const body = await readJsonObject(req, res, example);
38
- if (body === null)
39
- return null; // 4xx already sent — oversized or not a JSON object
40
- return body;
41
- }
42
- /** The role-targeting POST handlers' shared prologue: readPostBody (400/413 sent on a null
43
- * return) plus rejectBadRole's shared role validation (400 sent on a null return), so the
44
- * wake/abort/prompt-role/pause-role family states the body-discipline-and-role-check pairing
45
- * once instead of two guard lines per handler. Its null return means the response is already
46
- * written and the handler must stop; callers keep their own `if (!body) return` bail. Exactly
47
- * seven call sites — handlePromptRole, handleWake, handleWakeCancel and handleResetCounters (allowMissing, the
48
- * {} → all-roles default), handleAbort, handlePauseRole, and handleRoleNotes; handlePromptCancel deliberately does not ride it, since
49
- * its role is optional and validates only when present. */
50
- async function readRoleBody(req, res, example, root, allowMissing = false) {
51
- const body = await readPostBody(req, res, example);
52
- if (body === null)
53
- return null; // 4xx already sent — oversized, malformed, or not an object
54
- if (rejectBadRole(root, res, body.role, allowMissing))
55
- return null; // 400 already sent
56
- return body;
57
- }
58
- /** Handle POST /api/prompt: queue a director prompt. An over-long prompt
59
- * (DIRECTOR_PROMPT_MAX_CHARS) is a user-input error, not a server fault: the shared length
60
- * rule answers 400 — an unexpected submit failure (EACCES) still reaches the server's outer
61
- * catch as the 500 its gui-server-transport test pins. An optional images array (the composer's
62
- * dropped/pasted attachments) is validated before anything is queued: a bad image — wrong
63
- * extension, too many, undecodable, oversized — answers 400 naming the rule, with no queue
64
- * file and no image written. */
65
- export async function handlePrompt(req, res, root) {
66
- const body = await readPostBody(req, res, '{"text": "..."}');
67
- if (!body)
68
- return;
69
- const fields = checkedPromptFields(res, body);
70
- if (!fields)
71
- return;
72
- submitPrompt(root, fields.text, fields.images, fields.notBeforeMs);
73
- sendJson(res, 200, { ok: true });
74
- }
75
- /** The images field of a prompt endpoint's body, validated before anything touches the disk.
76
- * Three outcomes, deliberately distinct: `undefined` — no images field, a text-only submit,
77
- * continue without attachments; an array — the field was present and valid, attach it (a
78
- * present-but-empty array is fine and saves nothing); `null` — the field was present and
79
- * invalid, the 400 naming the rule is already sent, stop. The absent and invalid cases must
80
- * not share a value: a text-only POST is the common path and must proceed. */
81
- function checkedPromptImages(res, body) {
82
- if (body.images === undefined)
83
- return undefined;
84
- const problem = promptImagesProblem(body.images);
85
- if (problem) {
86
- sendJson(res, 400, { error: problem });
87
- return null;
88
- }
89
- return body.images;
90
- }
91
- /** The defer field of a prompt endpoint's body, validated before anything touches the disk.
92
- * Three outcomes, deliberately distinct like checkedPromptImages: `undefined` — no defer
93
- * field, a prompt that runs as soon as its turn comes; a number — the epoch ms the prompt is
94
- * deliverable from (`Date.now() + ms`); `null` — the field was present but not a duration
95
- * this grammar accepts, or its duration is over the 90d cap, and the 400 naming the rule is
96
- * already sent, so stop. The grammar is cli-args.ts's parseDurationMs — the same one
97
- * `tumwater prompt --at` and `pause --for` accept — so a deferral spelled here computes the
98
- * same `tumwater:not-before` marker the CLI writes. */
99
- function checkedPromptDefer(res, body) {
100
- if (body.defer === undefined)
101
- return undefined;
102
- const ms = typeof body.defer === "string" ? parseDurationMs(body.defer) : null;
103
- if (ms === null) {
104
- sendJson(res, 400, { error: "defer needs a duration like 45s, 90m, 1h30m, or 2d" });
105
- return null;
106
- }
107
- if (ms > PAUSE_FOR_MAX_MS) {
108
- sendJson(res, 400, { error: "defer is capped at 90d" });
109
- return null;
110
- }
111
- return Date.now() + ms;
112
- }
113
- /** The prompt endpoints' shared body tail: requirePromptText (400 on a null return) then
114
- * checkedPromptImages then checkedPromptDefer (400 on a null return from either), so
115
- * /api/prompt and /api/prompt-role read the same three fields in the same order and a body
116
- * wrong in several still answers the text problem first. Null means a 400 is already sent;
117
- * callers keep their own `if (!fields) return` bail. */
118
- function checkedPromptFields(res, body, role = DIRECTOR_ROLE) {
119
- const text = requirePromptText(res, body, role);
120
- if (text === null)
121
- return null;
122
- const images = checkedPromptImages(res, body);
123
- if (images === null)
124
- return null;
125
- const notBeforeMs = checkedPromptDefer(res, body);
126
- if (notBeforeMs === null)
127
- return null;
128
- return { text, images, notBeforeMs };
129
- }
130
- /** Handle POST /api/prompt-role: queue a prompt for one loop — the dashboard's per-row
131
- * prompt affordance submits here, calling the same path `tumwater prompt --role <id>` uses
132
- * (submitRolePrompt enqueues into that loop's own queue and logs under it, then a single-role
133
- * wake brings the live loop in within one poll; the wake message rides back for the flash).
134
- * The role validates exactly like /api/transcript (the shared rejectBadRole wording), and the
135
- * body discipline is /api/prompt's: readJsonObject → 400 malformed/non-object, 413 oversized,
136
- * text a non-empty string within the shared length rule → 400 otherwise — plus the same
137
- * optional images array and its validate-before-anything-is-written discipline. */
138
- export async function handlePromptRole(req, res, root) {
139
- const body = await readRoleBody(req, res, '{"role": "feature", "text": "..."}', root);
140
- if (!body)
141
- return;
142
- const role = body.role;
143
- const fields = checkedPromptFields(res, body, role);
144
- if (!fields)
145
- return;
146
- sendJson(res, 200, { ok: true, message: submitRolePromptAndWake(root, role, fields.text, fields.images, fields.notBeforeMs) });
147
- }
148
- /** Handle POST /api/prompt-cancel: retract one queued prompt addressed by its queue file —
149
- * the dashboard's per-row cancel affordance, the file-addressed twin of the CLI's
150
- * position-addressed `prompt --cancel`. The role is optional (absent cancels from the
151
- * director's queue) and validates exactly like /api/transcript; a file name failing the
152
- * shared rejectBadQueueFileName guard is a user-input error answered 400 with
153
- * nothing touched on disk. A vanished file answers 200 { status: "gone" } — the loop already
154
- * dequeued the prompt, which is data for the flash line, not a server fault. Same body
155
- * discipline as /api/prompt (readJsonObject → 400 malformed/non-object, 413 oversized). */
156
- export async function handlePromptCancel(req, res, root) {
157
- const body = await readPostBody(req, res, '{"role": "feature", "file": "<stamp>-<seq>-<pid>.md"}');
158
- if (!body)
159
- return;
160
- if (body.role !== undefined && rejectBadRole(root, res, body.role))
161
- return;
162
- const role = body.role === undefined ? DIRECTOR_ROLE : body.role;
163
- if (rejectBadQueueFileName(res, body.file))
164
- return;
165
- const outcome = cancelQueuedFile(root, role, body.file);
166
- sendJson(res, 200, outcome.status === "cancelled"
167
- ? { ok: true, status: "cancelled", preview: promptPreview(outcome.text) }
168
- : { ok: true, status: "gone" });
169
- }
170
- /** Handle POST /api/prompt-edit: rewrite one queued prompt addressed by its queue file — the
171
- * dashboard's per-row edit affordance, the file-addressed twin of the CLI's position-addressed
172
- * `prompt --edit`. The role is optional (absent edits the director's queue) and validates
173
- * exactly like /api/prompt-cancel; a file name failing the shared rejectBadQueueFileName
174
- * guard is a user-input error answered 400 with nothing touched on disk. Text
175
- * and images validate through the same checkedPromptFields /api/prompt and /api/prompt-role
176
- * use; editQueuedFile throws for a bad name or an over-cap text — both user-input errors
177
- * answered 400 (a filesystem failure reads as 400 like handleConfigSet's failed write). A
178
- * vanished file answers 200 { ok: true, status: "gone" } — the loop already dequeued the
179
- * prompt, which is data for the flash line, not a server fault; an edit answers 200 with the
180
- * old and new operator-facing text the client flashes. Same body discipline as /api/prompt
181
- * (readJsonObject → 400 malformed/non-object, 413 oversized). */
182
- export async function handlePromptEdit(req, res, root) {
183
- const body = await readPostBody(req, res, '{"role": "qa", "file": "<stamp>.md", "text": "..."}');
184
- if (!body)
185
- return;
186
- if (body.role !== undefined && rejectBadRole(root, res, body.role))
187
- return;
188
- const role = body.role === undefined ? DIRECTOR_ROLE : body.role;
189
- if (rejectBadQueueFileName(res, body.file))
190
- return;
191
- const fields = checkedPromptFields(res, body, role);
192
- if (!fields)
193
- return;
194
- let outcome;
195
- try {
196
- outcome = editQueuedFile(root, role, body.file, fields.text, fields.images);
197
- }
198
- catch (err) {
199
- sendJson(res, 400, { error: errorMessage(err) });
200
- return;
201
- }
202
- sendJson(res, 200, outcome.status === "edited"
203
- ? { ok: true, status: "edited", oldText: outcome.oldText, newText: outcome.newText }
204
- : { ok: true, status: "gone" });
205
- }
206
- /** Handle POST /api/question-answer: record the operator's answer to the Nth open question
207
- * exactly as `tumwater questions answer` does, through the non-exiting tryAnswerQuestion core.
208
- * The body carries the `title` the operator saw at `position`; a different title there, or no
209
- * such position, answers 200 { ok: true, status: "gone" } (the outbox moved on — data for the
210
- * flash line, like /api/prompt-cancel). The decision keeps its lines (`questions answer --file`'s
211
- * rule): a `## `/`### ` heading line or an unclosed fence in it answers 400 (bodyStructureProblem)
212
- * with nothing written. An unclosed fence in `## Open` answers 409; a blank
213
- * decision, a non-positive-integer position, or a missing title answers 400, and so does a
214
- * filesystem failure (handleConfigSet's precedent). */
215
- export async function handleQuestionAnswer(req, res, root) {
216
- const body = await readPostBody(req, res, '{"position": 1, "title": "<heading>", "decision": "..."}');
217
- if (!body)
218
- return;
219
- const { position, title, decision } = body;
220
- if (typeof position !== "number" || !Number.isInteger(position) || position < 1) {
221
- sendJson(res, 400, { error: "position must be a positive integer" });
222
- return;
223
- }
224
- if (typeof title !== "string" || title === "") {
225
- sendJson(res, 400, { error: "title must be a non-empty string" });
226
- return;
227
- }
228
- if (typeof decision !== "string" || decision.trim() === "") {
229
- sendJson(res, 400, { error: 'questions answer needs a decision: questions answer <n> "<decision>"' });
230
- return;
231
- }
232
- const keptDecision = normalizeEntryBody(decision);
233
- const problem = bodyStructureProblem(keptDecision);
234
- if (problem !== null) {
235
- sendJson(res, 400, { error: problem });
236
- return;
237
- }
238
- let outcome;
239
- try {
240
- outcome = tryAnswerQuestion(root, position, keptDecision, title, true);
241
- }
242
- catch (err) {
243
- sendJson(res, 400, { error: errorMessage(err) });
244
- return;
245
- }
246
- if (outcome.status === "answered") {
247
- sendJson(res, 200, { ok: true, status: "answered", question: outcome.title, decision: outcome.decision });
248
- }
249
- else if (outcome.status === "fenced") {
250
- sendJson(res, 409, { ok: false, error: UNCLOSED_FENCE_MESSAGE });
251
- }
252
- else {
253
- sendJson(res, 200, { ok: true, status: "gone" });
254
- }
255
- }
256
- /** Handle POST /api/budget: the dashboard's budget-badge editor saves the daily cost cap —
257
- * same body discipline as /api/prompt (readJsonObject), and the same shared validation rule +
258
- * atomic setter the TUI's Ctrl+B uses, so both surfaces write tumwater.json identically and
259
- * the running orchestrator picks the change up on its next ~2 s poll. */
260
- export async function handleBudget(req, res, root) {
261
- const body = await readPostBody(req, res, '{"maxDailyCostUsd": 25}');
262
- if (!body)
263
- return;
264
- const value = body.maxDailyCostUsd;
265
- if (value === undefined) {
266
- sendJson(res, 400, { error: "maxDailyCostUsd required" });
267
- return;
268
- }
269
- // The shared rule (finite ≥ 0; 0 disables): missing/non-finite/negative → 400 with
270
- // the offending value named.
271
- const problem = checkDailyBudgetUsd(value);
272
- if (problem) {
273
- sendJson(res, 400, { error: problem });
274
- return;
275
- }
276
- const result = setDailyBudgetUsd(root, value);
277
- if (!result.ok) {
278
- // The value was valid — this is a server-side failure (broken tumwater.json or
279
- // disk), not the client's fault.
280
- sendJson(res, 500, { error: result.error });
281
- return;
282
- }
283
- sendJson(res, 200, { ok: true, maxDailyCostUsd: value });
284
- }
285
- /** Handle POST /api/config-set: the Settings view's per-key Save — one curated key set to
286
- * one JSON value, through setConfigKey so the browser cannot drift from the CLI's rules
287
- * (unknown-key refusal, the per-key validators' messages, whole-candidate validateConfig,
288
- * atomic write the running fleet picks up live). A key outside EDITABLE_CONFIG_KEYS is
289
- * refused 400 with the key named — a known-but-not-curated key (customLoops) included, so
290
- * the panel's reach stays exactly EDITABLE_CONFIG_KEYS. The body's value is re-encoded with
291
- * JSON.stringify before setConfigKey parses it back, so `set model gpt-5`'s
292
- * JSON-or-literal rule is bypassed harmlessly: the browser already sent parsed JSON.
293
- * Same body discipline as /api/budget (readPostBody → 400 malformed/non-object, 413
294
- * oversized). */
295
- export async function handleConfigSet(req, res, root) {
296
- const body = await readPostBody(req, res, '{"key": "model", "value": "gpt-5"}');
297
- if (!body)
298
- return;
299
- const key = requireConfigKey(res, body);
300
- if (key === null)
301
- return;
302
- if (body.value === undefined) {
303
- sendJson(res, 400, { error: `value required for ${key}` });
304
- return;
305
- }
306
- // setConfigKey takes the raw CLI text; a JSON round-trip of an already-parsed value is
307
- // exact (JSON.parse(JSON.stringify(v)) === v), so the shared write path applies unchanged.
308
- const result = setConfigKey(root, key, JSON.stringify(body.value));
309
- if (!result.ok) {
310
- sendJson(res, 400, { error: `${key}: ${result.error}` });
311
- return;
312
- }
313
- sendJson(res, 200, { ok: true, key, value: result.value, oldValue: result.oldValue });
314
- }
315
- /** Handle POST /api/config-unset: the Settings view's per-key Clear — one curated key removed,
316
- * through unsetConfigKey so the browser cannot drift from `tumwater config unset`'s rules
317
- * (unknown-key refusal, whole-candidate validateConfig, atomic write the running fleet picks
318
- * up live). The same EDITABLE_CONFIG_KEYS gate as /api/config-set, so the panel's reach stays
319
- * exactly the curated set; a key outside it is refused 400 with the key named. Same body
320
- * discipline (readPostBody → 400 malformed/non-object, 413 oversized). The 200 body omits
321
- * `oldValue` when the file never held the key (JSON.stringify drops an undefined), so the
322
- * client can tell a retraction from a no-op; a file value of `null` still serializes and is a
323
- * real retraction. */
324
- export async function handleConfigUnset(req, res, root) {
325
- const body = await readPostBody(req, res, '{"key": "model"}');
326
- if (!body)
327
- return;
328
- const key = requireConfigKey(res, body);
329
- if (key === null)
330
- return;
331
- const result = unsetConfigKey(root, key);
332
- if (!result.ok) {
333
- sendJson(res, 400, { error: `${key}: ${result.error}` });
334
- return;
335
- }
336
- sendJson(res, 200, { ok: true, key, oldValue: result.oldValue });
337
- }
338
- /** The shared `forSeconds` rule of /api/pause and /api/pause-role: undefined when absent, the
339
- * deadline in epoch milliseconds when valid, null after sending the 400 (only with
340
- * `paused: true`, a positive finite number, within the CLI's 90-day cap). */
341
- function pauseForSecondsMs(res, body, pausing) {
342
- const forSeconds = body.forSeconds;
343
- if (forSeconds === undefined)
344
- return undefined;
345
- if (!pausing) {
346
- sendJson(res, 400, { error: "forSeconds only applies when pausing (paused: true)" });
347
- return null;
348
- }
349
- if (typeof forSeconds !== "number" || !Number.isFinite(forSeconds) || forSeconds <= 0) {
350
- sendJson(res, 400, { error: `forSeconds must be a positive number of seconds${gotSuffix(forSeconds)}` });
351
- return null;
352
- }
353
- if (forSeconds * 1000 > PAUSE_FOR_MAX_MS) {
354
- sendJson(res, 400, { error: `forSeconds is capped at ${PAUSE_FOR_MAX_MS / 1000} seconds (90 days)${gotSuffix(forSeconds)} — pause without it for a standing pause` });
355
- return null;
356
- }
357
- return Date.now() + Math.round(forSeconds * 1000);
358
- }
359
- /** The optional `reason` of /api/pause: undefined when absent or blank, the raw string when
360
- * given with `paused: true` (pauseFleet folds and caps it), null after sending the 400. */
361
- function pauseReasonField(res, body, pausing) {
362
- const reason = body.reason;
363
- if (reason === undefined)
364
- return undefined;
365
- if (!pausing) {
366
- sendJson(res, 400, { error: "reason only applies when pausing (paused: true)" });
367
- return null;
368
- }
369
- if (typeof reason !== "string") {
370
- sendJson(res, 400, { error: `reason must be a string${gotSuffix(reason)}` });
371
- return null;
372
- }
373
- return reason.trim() === "" ? undefined : reason;
374
- }
375
- /** Handle POST /api/pause: the dashboard's pause control — the same operator gate
376
- * `tumwater pause [--for <duration>]` and `resume` write, via fleet/fleet-state.ts's shared writers
377
- * (pauseFleet/resumeFleet) so the CLI and the GUI cannot drift on the marker's format or
378
- * idempotence. Same body discipline as /api/prompt and /api/budget (readJsonObject → 400
379
- * malformed/non-object, 413 oversized); the target state is explicit (`paused: true|false`)
380
- * rather than a toggle, so a retried request is idempotent. An optional `forSeconds` makes it
381
- * a timed pause (the marker's `until`, capped like the CLI's `--for`); it only accompanies
382
- * `paused: true`, as does an optional `reason` (the CLI's `pause --reason`, stored folded and
383
- * capped by pauseFleet; the 200 body echoes what was stored). */
384
- export async function handlePause(req, res, root) {
385
- const body = await readPostBody(req, res, '{"paused": true, "forSeconds": 3600}');
386
- if (!body)
387
- return;
388
- const value = requirePausedFlag(res, body);
389
- if (value === null)
390
- return;
391
- const untilMs = pauseForSecondsMs(res, body, value);
392
- if (untilMs === null)
393
- return;
394
- const reason = pauseReasonField(res, body, value);
395
- if (reason === null)
396
- return;
397
- if (value)
398
- pauseFleet(root, untilMs, reason);
399
- else
400
- resumeFleet(root);
401
- const stored = normalizePauseReason(reason);
402
- sendJson(res, 200, { ok: true, paused: value, ...(untilMs === undefined ? {} : { until: untilMs }), ...(stored ? { reason: stored } : {}) });
403
- }
404
- /** The `inSeconds` rule of /api/wake: undefined when absent, the delay in milliseconds when
405
- * valid, null after sending the 400 (a positive finite number within the CLI's 90-day cap).
406
- * A duration, not a deadline: requestWake takes `inMs` and reads its own clock. */
407
- function wakeInSecondsMs(res, body) {
408
- const inSeconds = body.inSeconds;
409
- if (inSeconds === undefined)
410
- return undefined;
411
- if (typeof inSeconds !== "number" || !Number.isFinite(inSeconds) || inSeconds <= 0) {
412
- sendJson(res, 400, { error: `inSeconds must be a positive number of seconds${gotSuffix(inSeconds)}` });
413
- return null;
414
- }
415
- if (inSeconds * 1000 > PAUSE_FOR_MAX_MS) {
416
- sendJson(res, 400, { error: `inSeconds is capped at ${PAUSE_FOR_MAX_MS / 1000} seconds (90 days)${gotSuffix(inSeconds)}` });
417
- return null;
418
- }
419
- return Math.round(inSeconds * 1000);
420
- }
421
- /** Handle POST /api/wake: the dashboard's per-row wake control — the same marker-writing
422
- * core `tumwater wake` calls (requestWake), so the CLI and the GUI cannot drift on the
423
- * state-file edits or the marker. `{}`/a missing role targets every configured role (the
424
- * CLI's all-roles default); a given role validates exactly like /api/transcript. Same body
425
- * discipline as /api/pause (readJsonObject → 400 malformed/non-object, 413 oversized). An
426
- * optional `inSeconds` schedules the wake for later, like `tumwater wake --in` (same 90-day cap). */
427
- export async function handleWake(req, res, root) {
428
- const body = await readRoleBody(req, res, '{"role": "feature"}', root, true);
429
- if (!body)
430
- return;
431
- const inMs = wakeInSecondsMs(res, body);
432
- if (inMs === null)
433
- return;
434
- sendJson(res, 200, {
435
- ok: true,
436
- message: requestWake(root, body.role === undefined ? validRoleIds(root) : [body.role], inMs),
437
- });
438
- }
439
- /** Handle POST /api/wake-cancel: the dashboard's cancel of pending scheduled wakes — the same
440
- * core `tumwater wake --cancel` calls (cancelWakes). A missing role cancels every pending wake;
441
- * a given role validates like /api/wake and drops only its share. */
442
- export async function handleWakeCancel(req, res, root) {
443
- const body = await readRoleBody(req, res, '{"role": "feature"}', root, true);
444
- if (!body)
445
- return;
446
- sendJson(res, 200, { ok: true, message: cancelWakes(root, body.role) });
447
- }
448
- /** Handle POST /api/reset-counters: the dashboard's counter reset — the same marker-writing
449
- * core `tumwater reset-counters` calls (requestResetCounters). Same body discipline and
450
- * `{}` → every configured role default as /api/wake. */
451
- export async function handleResetCounters(req, res, root) {
452
- const body = await readRoleBody(req, res, '{"role": "feature"}', root, true);
453
- if (!body)
454
- return;
455
- sendJson(res, 200, {
456
- ok: true,
457
- message: requestResetCounters(root, body.role === undefined ? validRoleIds(root) : [body.role]),
458
- });
459
- }
460
- /** Handle POST /api/backlog-file: the Plans/Bugs tabs' New control — the same entry and wake
461
- * `tumwater bug`/`tumwater plan` produce (fileEntryWithBody, then the shared wake text to the
462
- * bugfix/feature loop). Body `{"kind": "bug"|"plan", "title": "...", "body": "optional markdown"}`. The empty-title and
463
- * wake-length checks run here, before anything is written: the body is normalized and checked by bodyStructureProblem too, and an over-long wake would throw after the entry landed. */
464
- export async function handleBacklogFile(req, res, root) {
465
- const body = await readPostBody(req, res, '{"kind": "plan", "title": "...", "body": "optional markdown"}');
466
- if (!body)
467
- return;
468
- const kind = body.kind;
469
- if (kind !== "bug" && kind !== "plan") {
470
- sendJson(res, 400, { error: 'kind must be "bug" or "plan"' });
471
- return;
472
- }
473
- const rawTitle = typeof body.title === "string" ? body.title : "";
474
- if (collapseWhitespace(rawTitle) === "") {
475
- sendJson(res, 400, { error: "title required" });
476
- return;
477
- }
478
- const { role, wakeText } = OPERATOR_FILE_KINDS[kind];
479
- const problem = promptLengthProblem(wakeText(collapseWhitespace(rawTitle)), role);
480
- if (problem) {
481
- sendJson(res, 400, { error: problem });
482
- return;
483
- }
484
- const rawBody = body.body;
485
- if (rawBody !== undefined && typeof rawBody !== "string") {
486
- sendJson(res, 400, { error: "body must be a string" });
487
- return;
488
- }
489
- const entryBody = normalizeEntryBody(rawBody ?? "");
490
- const bodyProblem = bodyStructureProblem(entryBody);
491
- if (bodyProblem) {
492
- sendJson(res, 400, { error: bodyProblem });
493
- return;
494
- }
495
- const filed = fileEntryWithBody(root, kind, rawTitle, entryBody);
496
- submitRolePromptAndWake(root, role, wakeText(filed.title));
497
- sendJson(res, 200, { ok: true, file: filed.file, title: filed.title });
498
- }
499
- /** Handle POST /api/backlog-cancel: the entry drawer's cancel — the same lock and splice as
500
- * `tumwater plan|bug --cancel <n>` through the non-exiting tryCancelEntry. Body `{"kind":
501
- * "bug"|"plan", "position": <1-based>, "title": "<heading>"}`; a missing position or a
502
- * different heading there answers 200 { ok: true, status: "gone" } (the list moved since the
503
- * poll). No wake and no inbox prompt: removing work must not wake a loop. */
504
- export async function handleBacklogCancel(req, res, root) {
505
- const body = await readPostBody(req, res, '{"kind": "bug", "position": 1, "title": "<heading>"}');
506
- if (!body)
507
- return;
508
- const { kind, position, title } = body;
509
- if (kind !== "bug" && kind !== "plan") {
510
- sendJson(res, 400, { error: 'kind must be "bug" or "plan"' });
511
- return;
512
- }
513
- if (typeof position !== "number" || !Number.isInteger(position) || position < 1) {
514
- sendJson(res, 400, { error: "position must be a positive integer" });
515
- return;
516
- }
517
- if (typeof title !== "string" || title === "") {
518
- sendJson(res, 400, { error: "title must be a non-empty string" });
519
- return;
520
- }
521
- let outcome;
522
- try {
523
- outcome = tryCancelEntry(root, kind, position, title);
524
- }
525
- catch (err) {
526
- sendJson(res, 400, { error: errorMessage(err) });
527
- return;
528
- }
529
- if (outcome.status === "cancelled") {
530
- sendJson(res, 200, { ok: true, status: "cancelled", file: outcome.file, title: outcome.title });
531
- }
532
- else {
533
- sendJson(res, 200, { ok: true, status: "gone" });
534
- }
535
- }
536
- /** Handle POST /api/backlog-edit: the entry drawer's edit — tryEditEntry's title-checked,
537
- * in-place rewrite under the state lock. Body `{"kind", "position", "title", "newTitle",
538
- * "newBody"}`; a missing position or a different heading there answers 200 { ok: true, status:
539
- * "gone" }. The body keeps its line structure. No wake and no inbox prompt: whichever tick picks
540
- * the entry up refines it. */
541
- export async function handleBacklogEdit(req, res, root) {
542
- const body = await readPostBody(req, res, '{"kind": "bug", "position": 1, "title": "<heading>", "newTitle": "...", "newBody": "..."}');
543
- if (!body)
544
- return;
545
- const { kind, position, title, newTitle, newBody } = body;
546
- if (kind !== "bug" && kind !== "plan") {
547
- sendJson(res, 400, { error: 'kind must be "bug" or "plan"' });
548
- return;
549
- }
550
- if (typeof position !== "number" || !Number.isInteger(position) || position < 1) {
551
- sendJson(res, 400, { error: "position must be a positive integer" });
552
- return;
553
- }
554
- if (typeof title !== "string" || title === "") {
555
- sendJson(res, 400, { error: "title must be a non-empty string" });
556
- return;
557
- }
558
- if (typeof newTitle !== "string" || collapseWhitespace(newTitle) === "") {
559
- sendJson(res, 400, { error: "newTitle required" });
560
- return;
561
- }
562
- if (typeof newBody !== "string") {
563
- sendJson(res, 400, { error: "newBody must be a string" });
564
- return;
565
- }
566
- const problem = bodyStructureProblem(newBody.replace(/\r\n?/g, "\n"));
567
- if (problem !== null) {
568
- sendJson(res, 400, { error: problem });
569
- return;
570
- }
571
- let outcome;
572
- try {
573
- outcome = tryEditEntry(root, kind, position, title, newTitle, newBody);
574
- }
575
- catch (err) {
576
- sendJson(res, 400, { error: errorMessage(err) });
577
- return;
578
- }
579
- if (outcome.status === "edited") {
580
- sendJson(res, 200, { ok: true, status: "edited", file: outcome.file, title: outcome.title });
581
- }
582
- else {
583
- sendJson(res, 200, { ok: true, status: "gone" });
584
- }
585
- }
586
- /** Reply for a boolean-claiming marker request (RequestResult): success maps to 200 with its
587
- * message, refusal to 409 — the request is valid but nothing can act on it, the same conflict
588
- * reading requestAbort's not-live error uses. /api/restart and /api/abort share it. */
589
- function sendRequestResult(res, result) {
590
- if (!result.ok) {
591
- sendJson(res, 409, { error: result.error });
592
- return;
593
- }
594
- sendJson(res, 200, { ok: true, message: result.message });
595
- }
596
- /** Handle POST /api/restart: the dashboard's build-stale alert's refresh button — the same
597
- * marker-writing core a CLI surface would call (requestRestart), so the wording and the
598
- * not-pending refusal cannot drift. The not-pending error comes back 409 like /api/abort's
599
- * not-live one: the request is valid but nothing can act on it. Same body discipline as
600
- * /api/wake (readJsonObject → 400 malformed/non-object, 413 oversized; the body itself is
601
- * ignored — there are no options to force a restart). */
602
- export async function handleRestart(req, res, root) {
603
- const body = await readPostBody(req, res, "{}");
604
- if (!body)
605
- return;
606
- sendRequestResult(res, requestRestart(root));
607
- }
608
- /** Handle POST /api/reclaim: the dashboard disk row's reclaim button — the same manual pass
609
- * `tumwater reclaim` runs (runManualReclaim), answering its confirmation verbatim. Same body
610
- * discipline as /api/restart (the body is ignored); a throw reaches the server's outer 500. */
611
- export async function handleReclaim(req, res, root) {
612
- const body = await readPostBody(req, res, "{}");
613
- if (!body)
614
- return;
615
- sendJson(res, 200, { ok: true, message: await runManualReclaim(root) });
616
- }
617
- /** Handle POST /api/abort: the dashboard's per-row abort control — the same marker-writing
618
- * core `tumwater abort` calls (requestAbort). The role is required and validated like
619
- * /api/transcript; requestAbort's not-live error comes back 409 — the marker is valid but
620
- * nothing can consume it, a conflict rather than a client 400. The director variant's
621
- * message (the discarded-prompt note) rides through verbatim. */
622
- export async function handleAbort(req, res, root) {
623
- const body = await readRoleBody(req, res, '{"role": "feature"}', root);
624
- if (!body)
625
- return;
626
- sendRequestResult(res, requestAbort(root, body.role));
627
- }
628
- /** Handle POST /api/pause-role: the dashboard's per-row pause/resume control — the same
629
- * marker-writing core `tumwater pause --role` / `resume --role` call (pauseRole/resumeRole),
630
- * so the CLI and the GUI cannot drift on the marker's format or idempotence. The target state
631
- * is explicit (`paused: true|false`) like /api/pause, so a retried request is idempotent; the
632
- * fleet-wide pause composes freely — its gate is checked first at scheduling, and the role
633
- * marker still records the operator's per-row intent. Same body discipline as /api/wake
634
- * (readJsonObject → 400 malformed/non-object, 413 oversized; the role validates through the
635
- * shared rejectBadRole wording). */
636
- export async function handlePauseRole(req, res, root) {
637
- const body = await readRoleBody(req, res, '{"role": "feature", "paused": true}', root);
638
- if (!body)
639
- return;
640
- const value = requirePausedFlag(res, body);
641
- if (value === null)
642
- return;
643
- const untilMs = pauseForSecondsMs(res, body, value);
644
- if (untilMs === null)
645
- return;
646
- const changed = value
647
- ? pauseRole(root, body.role, untilMs)
648
- : resumeRole(root, body.role);
649
- sendJson(res, 200, { ok: true, changed, paused: value, ...(untilMs === undefined ? {} : { until: untilMs }) });
650
- }
651
- /** Handle POST /api/role-notes: the loop drawer's notebook Edit/Clear — the same saveRoleNote
652
- * `tumwater role <id> --notes-file` / `--clear-notes` call, so the 4 KB cap and its wording
653
- * cannot drift. `"text": ""` clears. The director has no notebook (400, the CLI's wording); a
654
- * non-string text is a 400 before anything is written; an over-cap note is a 400 carrying
655
- * saveRoleNote's byte-count message. */
656
- export async function handleRoleNotes(req, res, root) {
657
- const body = await readRoleBody(req, res, '{"role": "feature", "text": "…"}', root);
658
- if (!body)
659
- return;
660
- const role = body.role;
661
- if (role === DIRECTOR_ROLE)
662
- return sendJson(res, 400, { error: "the director has no notebook" });
663
- if (typeof body.text !== "string")
664
- return sendJson(res, 400, { error: `"text" must be a string — the full replacement note, or "" to clear it${gotSuffix(body.text)}` });
665
- try {
666
- sendJson(res, 200, { ok: true, message: saveRoleNote(roleNotesPath(root, role), body.text) });
667
- }
668
- catch (err) {
669
- sendJson(res, 400, { error: errorMessage(err) });
670
- }
671
- }
14
+ export { handlePrompt, handlePromptRole, handlePromptCancel, handlePromptEdit, handlePromptTop } from "./gui-prompt-commands.js";
15
+ export { handleQuestionAnswer, handleBacklogFile, handleBacklogCancel, handleBacklogRelease, handleBacklogTop, handleBacklogEdit } from "./gui-backlog-commands.js";
16
+ export { handleConfigSet, handleConfigUnset, handleBudget, handleRoleCap, handleRoleEnable, handleRoleDisable, handleRoleInstances, handleRoleInterval, handleRoleInstructions, handleRoleModel, handleRoleQuietHours, handleNotifyTest } from "./gui-config-commands.js";
17
+ export { handlePause, handlePauseRole, handleWake, handleWakeCancel } from "./gui-team-commands.js";
18
+ export { handleResetCounters, handleRestart, handleReclaim, handleAbort, handleStopSchedule, handleRoleNotes, handleRetire, handleBriefEdit } from "./gui-operator-commands.js";