tumwater 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (446) hide show
  1. package/README.md +27 -13
  2. package/dist/build-info.json +2 -2
  3. package/dist/src/agent/agent-args.js +8 -9
  4. package/dist/src/agent/agent-backend.js +8 -6
  5. package/dist/src/agent/backend-traits.js +42 -25
  6. package/dist/src/agent/claude/claude-account.js +71 -204
  7. package/dist/src/agent/claude/claude-args.js +9 -6
  8. package/dist/src/agent/claude/claude-backend.js +10 -10
  9. package/dist/src/agent/claude/claude-env.js +1 -1
  10. package/dist/src/agent/claude/claude-stream.js +4 -3
  11. package/dist/src/agent/claude/claude-write-guard.js +49 -0
  12. package/dist/src/agent/claude/write-guard-hook.js +13 -0
  13. package/dist/src/agent/codex/codex-account.js +119 -0
  14. package/dist/src/agent/codex/codex-args.js +181 -0
  15. package/dist/src/agent/codex/codex-backend.js +279 -0
  16. package/dist/src/agent/codex/codex-catalog.js +97 -0
  17. package/dist/src/agent/codex/codex-env.js +18 -0
  18. package/dist/src/agent/codex/codex-hook-trust.js +246 -0
  19. package/dist/src/agent/codex/codex-patch-paths.js +36 -0
  20. package/dist/src/agent/codex/codex-rollout.js +179 -0
  21. package/dist/src/agent/codex/codex-stream.js +402 -0
  22. package/dist/src/agent/codex/codex-write-guard.js +86 -0
  23. package/dist/src/agent/codex/write-guard-hook.js +12 -0
  24. package/dist/src/agent/image-input.js +29 -24
  25. package/dist/src/agent/notes-shim.js +11 -12
  26. package/dist/src/agent/opencode/opencode-args.js +0 -1
  27. package/dist/src/agent/opencode/opencode-backend.js +12 -9
  28. package/dist/src/agent/opencode/opencode-catalog.js +55 -29
  29. package/dist/src/agent/opencode/opencode-config.js +19 -14
  30. package/dist/src/agent/opencode/opencode-env.js +0 -1
  31. package/dist/src/agent/opencode/opencode-session-id.js +1 -2
  32. package/dist/src/agent/opencode/opencode-stream.js +1 -1
  33. package/dist/src/agent/opencode/write-guard-plugin/index.js +48 -0
  34. package/dist/src/agent/role-notes-write.js +5 -6
  35. package/dist/src/agent/run-agent.js +465 -448
  36. package/dist/src/agent/run-settle.js +214 -0
  37. package/dist/src/agent/session-ledger.js +86 -16
  38. package/dist/src/agent/subscription-account.js +220 -0
  39. package/dist/src/agent/write-guard.js +148 -0
  40. package/dist/src/backlog/backlog-archive.js +246 -55
  41. package/dist/src/backlog/backlog-cli-view.js +280 -0
  42. package/dist/src/backlog/backlog-cli.js +85 -274
  43. package/dist/src/backlog/backlog-commit.js +4 -5
  44. package/dist/src/backlog/backlog-eligibility.js +175 -242
  45. package/dist/src/backlog/backlog-index.js +149 -0
  46. package/dist/src/backlog/backlog-md.js +54 -6
  47. package/dist/src/backlog/backlog-parts.js +254 -0
  48. package/dist/src/backlog/backlog-release.js +117 -0
  49. package/dist/src/backlog/backlog-render.js +27 -0
  50. package/dist/src/backlog/backlog-structure.js +99 -156
  51. package/dist/src/backlog/backlog-write.js +40 -127
  52. package/dist/src/backlog/backlog.js +21 -6
  53. package/dist/src/backlog/question-answer.js +21 -15
  54. package/dist/src/baseline/main-baseline.js +89 -20
  55. package/dist/src/baseline/main-culprit.js +94 -0
  56. package/dist/src/baseline/main-guardrails.js +209 -0
  57. package/dist/src/baseline/main-red.js +139 -32
  58. package/dist/src/baseline/main-revert.js +133 -0
  59. package/dist/src/baseline/main-watch.js +184 -0
  60. package/dist/src/brief/brief-edit.js +9 -11
  61. package/dist/src/brief/brief.js +31 -51
  62. package/dist/src/budget/budget.js +30 -20
  63. package/dist/src/budget/fallback-breaker.js +26 -27
  64. package/dist/src/build/build-check-detect.js +23 -15
  65. package/dist/src/build/build-check-events.js +25 -6
  66. package/dist/src/build/build-check-report.js +49 -9
  67. package/dist/src/build/build-check-scoped.js +18 -11
  68. package/dist/src/build/build-check.js +2 -1
  69. package/dist/src/build/build-info.js +2 -3
  70. package/dist/src/build/build-stage.js +9 -14
  71. package/dist/src/build/dep-install.js +1 -2
  72. package/dist/src/change/change-data.js +123 -31
  73. package/dist/src/change/change-render.js +46 -5
  74. package/dist/src/cli/brief-command.js +3 -4
  75. package/dist/src/cli/cli-args.js +118 -33
  76. package/dist/src/cli/cli-command-args.js +46 -18
  77. package/dist/src/cli/cli-editor.js +2 -1
  78. package/dist/src/cli/cli-flag-specs.js +150 -45
  79. package/dist/src/cli/cli-init.js +51 -0
  80. package/dist/src/cli/cli-marker-commands.js +4 -4
  81. package/dist/src/cli/cli-query-commands.js +308 -590
  82. package/dist/src/cli/cli-repo-gate.js +11 -1
  83. package/dist/src/cli/cli-role-command.js +420 -0
  84. package/dist/src/cli/cli-start-summary.js +160 -0
  85. package/dist/src/cli/{cli-run.js → cli-start.js} +165 -237
  86. package/dist/src/cli/completion.js +149 -22
  87. package/dist/src/cli/config-commands.js +41 -18
  88. package/dist/src/cli/follow-view.js +75 -0
  89. package/dist/src/cli/help.js +189 -73
  90. package/dist/src/cli/log-commands.js +148 -82
  91. package/dist/src/cli/question-commands.js +205 -10
  92. package/dist/src/cli.js +106 -43
  93. package/dist/src/collections.js +26 -10
  94. package/dist/src/concurrency/check-permit.js +20 -3
  95. package/dist/src/concurrency/lock.js +17 -13
  96. package/dist/src/concurrency/provider-lease.js +44 -8
  97. package/dist/src/concurrency/semaphore.js +102 -26
  98. package/dist/src/config/config-aliases.js +0 -1
  99. package/dist/src/config/config-editable-keys.js +13 -1
  100. package/dist/src/config/config-fallbacks.js +93 -56
  101. package/dist/src/config/config-field-checks.js +40 -10
  102. package/dist/src/config/config-keys.js +30 -8
  103. package/dist/src/config/config-live.js +63 -24
  104. package/dist/src/config/config-request.js +137 -0
  105. package/dist/src/config/config-schema.js +31 -24
  106. package/dist/src/config/config-selector-checks.js +123 -0
  107. package/dist/src/config/config-validation-agents.js +80 -0
  108. package/dist/src/config/config-validation-model.js +116 -0
  109. package/dist/src/config/config-validation.js +141 -199
  110. package/dist/src/config/config-views.js +127 -53
  111. package/dist/src/config/config-write-core.js +135 -0
  112. package/dist/src/config/config-write.js +131 -264
  113. package/dist/src/config/config.js +48 -38
  114. package/dist/src/config/model-selector.js +34 -34
  115. package/dist/src/doctor/doctor-backlog-bugs.js +287 -0
  116. package/dist/src/doctor/doctor-backlog-plans.js +263 -0
  117. package/dist/src/doctor/doctor-backlog.js +100 -496
  118. package/dist/src/doctor/doctor-check-names.js +81 -0
  119. package/dist/src/doctor/doctor-checks.js +52 -579
  120. package/dist/src/doctor/doctor-claude-checks.js +145 -0
  121. package/dist/src/doctor/doctor-codex-checks.js +279 -0
  122. package/dist/src/doctor/doctor-contention-checks.js +380 -0
  123. package/dist/src/doctor/doctor-holds.js +82 -66
  124. package/dist/src/doctor/doctor-install.js +2 -1
  125. package/dist/src/doctor/doctor-model-checks.js +236 -402
  126. package/dist/src/doctor/doctor-notify.js +3 -4
  127. package/dist/src/doctor/doctor-opencode-checks.js +162 -0
  128. package/dist/src/doctor/doctor-orphans.js +182 -63
  129. package/dist/src/doctor/doctor-project-checks.js +303 -0
  130. package/dist/src/doctor/doctor.js +121 -77
  131. package/dist/src/doctor/image-probe.js +21 -8
  132. package/dist/src/doctor/outcome.js +3 -2
  133. package/dist/src/doctor/probe.js +7 -11
  134. package/dist/src/events/event-format.js +181 -86
  135. package/dist/src/events/event-labels.js +12 -5
  136. package/dist/src/events/event-legacy.js +39 -15
  137. package/dist/src/events/event-read.js +39 -39
  138. package/dist/src/events/event-tally.js +16 -0
  139. package/dist/src/events/event-window.js +60 -24
  140. package/dist/src/events/events.js +30 -13
  141. package/dist/src/events/notify.js +7 -7
  142. package/dist/src/failure/failure-cluster.js +12 -12
  143. package/dist/src/failure/failure-data.js +56 -56
  144. package/dist/src/failure/failure-render.js +31 -23
  145. package/dist/src/failure/failure-state-change.js +17 -12
  146. package/dist/src/failure/time-spend.js +99 -35
  147. package/dist/src/files/files.js +111 -35
  148. package/dist/src/files/json-files.js +3 -3
  149. package/dist/src/files/json-object.js +28 -7
  150. package/dist/src/files/stat-cache.js +2 -2
  151. package/dist/src/files/tail.js +1 -1
  152. package/dist/src/gates/bootstrap-gates.js +4 -5
  153. package/dist/src/gates/budget-gates.js +177 -84
  154. package/dist/src/gates/disk-gate.js +1 -1
  155. package/dist/src/gates/gate-polls.js +108 -58
  156. package/dist/src/gates/maintenance-quota.js +10 -13
  157. package/dist/src/gates/pause-gates.js +28 -10
  158. package/dist/src/gates/readiness.js +7 -3
  159. package/dist/src/gates/role-cap-gates.js +8 -8
  160. package/dist/src/gates/startup-gate.js +94 -32
  161. package/dist/src/gates/streak-gate.js +54 -30
  162. package/dist/src/git/commit-message.js +77 -46
  163. package/dist/src/git/git-diff.js +16 -10
  164. package/dist/src/git/git-run.js +79 -38
  165. package/dist/src/git/git.js +36 -34
  166. package/dist/src/git/slot-salvage.js +253 -0
  167. package/dist/src/git/slots-state.js +6 -6
  168. package/dist/src/git/worktree-pool.js +132 -38
  169. package/dist/src/git/worktree-use.js +4 -11
  170. package/dist/src/git/worktree.js +94 -37
  171. package/dist/src/gui/gui-admin-endpoints.js +96 -0
  172. package/dist/src/gui/gui-args.js +79 -8
  173. package/dist/src/gui/gui-backlog-commands.js +42 -18
  174. package/dist/src/gui/gui-backlog-endpoints.js +155 -0
  175. package/dist/src/gui/gui-command.js +8 -13
  176. package/dist/src/gui/gui-config-commands.js +94 -72
  177. package/dist/src/gui/gui-endpoint-commands.js +4 -3
  178. package/dist/src/gui/gui-endpoint-common.js +4 -4
  179. package/dist/src/gui/gui-endpoints.js +11 -380
  180. package/dist/src/gui/gui-history-endpoints.js +301 -0
  181. package/dist/src/gui/gui-operator-commands.js +108 -13
  182. package/dist/src/gui/gui-prompt-commands.js +11 -14
  183. package/dist/src/gui/gui-report-endpoints.js +90 -0
  184. package/dist/src/gui/gui-server.js +11 -4
  185. package/dist/src/gui/gui-team-commands.js +7 -7
  186. package/dist/src/gui/http-body.js +1 -1
  187. package/dist/src/history/history-data.js +122 -234
  188. package/dist/src/history/history-queued.js +147 -0
  189. package/dist/src/history/history-render.js +42 -22
  190. package/dist/src/history/history.js +193 -49
  191. package/dist/src/inbox/inbox-attachments.js +29 -30
  192. package/dist/src/inbox/inbox-cancel.js +10 -11
  193. package/dist/src/inbox/inbox-edit.js +9 -9
  194. package/dist/src/inbox/inbox-submit.js +18 -18
  195. package/dist/src/inbox/inbox-top.js +25 -9
  196. package/dist/src/inbox/inbox.js +42 -43
  197. package/dist/src/inbox/pending-prompt.js +26 -26
  198. package/dist/src/inbox/prompt-commands.js +62 -49
  199. package/dist/src/inbox/prompt-not-before.js +2 -2
  200. package/dist/src/init/init-templates.js +1 -2
  201. package/dist/src/init/init.js +28 -23
  202. package/dist/src/merge/backlog-conflicts.js +215 -35
  203. package/dist/src/merge/merge-batch.js +14 -8
  204. package/dist/src/merge/merge-cancel.js +53 -0
  205. package/dist/src/merge/merge-check-failures.js +44 -22
  206. package/dist/src/merge/merge-core.js +81 -32
  207. package/dist/src/merge/merge-diff.js +5 -4
  208. package/dist/src/merge/merge-drain.js +150 -11
  209. package/dist/src/merge/merge-git.js +177 -38
  210. package/dist/src/merge/merge-pipeline.js +15 -3
  211. package/dist/src/merge/merge-post-review.js +90 -0
  212. package/dist/src/merge/merge-queue-render.js +49 -11
  213. package/dist/src/merge/merge-queue.js +31 -15
  214. package/dist/src/merge/merge-requeue.js +102 -0
  215. package/dist/src/merge/merge-slot.js +61 -16
  216. package/dist/src/merge/merge-stack.js +33 -11
  217. package/dist/src/merge/merge-to-main.js +86 -27
  218. package/dist/src/merge/merge-top.js +69 -0
  219. package/dist/src/merge/merge-verify.js +12 -10
  220. package/dist/src/notes-cli.js +30 -15
  221. package/dist/src/operator/operator-commands.js +192 -81
  222. package/dist/src/operator/operator-intent.js +69 -42
  223. package/dist/src/operator/operator-requests.js +35 -40
  224. package/dist/src/operator/retire.js +5 -6
  225. package/dist/src/operator/revert.js +142 -34
  226. package/dist/src/orchestrator/orchestrator-launch.js +60 -52
  227. package/dist/src/orchestrator/orchestrator-poll.js +320 -0
  228. package/dist/src/orchestrator/orchestrator-scheduling.js +68 -369
  229. package/dist/src/orchestrator/orchestrator.js +128 -369
  230. package/dist/src/orchestrator/retention.js +42 -8
  231. package/dist/src/orchestrator/scheduling-admit.js +116 -0
  232. package/dist/src/orchestrator/scheduling-scope.js +201 -0
  233. package/dist/src/orchestrator/scheduling-skip.js +185 -0
  234. package/dist/src/orchestrator/startup-migration-steps.js +8 -7
  235. package/dist/src/orchestrator/startup-migrations.js +11 -7
  236. package/dist/src/paths.js +116 -49
  237. package/dist/src/pi/command-shape.js +12 -1
  238. package/dist/src/pi/pi-args.js +6 -4
  239. package/dist/src/pi/pi-backend.js +6 -6
  240. package/dist/src/pi/pi-bin.js +54 -63
  241. package/dist/src/pi/pi-listed-models.js +53 -0
  242. package/dist/src/pi/pi-models.js +63 -30
  243. package/dist/src/pi/pi-run-result.js +1 -1
  244. package/dist/src/pi/pi-settings.js +1 -1
  245. package/dist/src/pi/pi-stream.js +6 -6
  246. package/dist/src/pi/pi-watchdogs.js +18 -19
  247. package/dist/src/pi-extension/bounded-output.js +5 -3
  248. package/dist/src/pi-extension/context-budget.js +2 -2
  249. package/dist/src/pi-extension/context-shake.js +1 -1
  250. package/dist/src/pi-extension/context-window.js +0 -1
  251. package/dist/src/pi-extension/full-output.js +3 -10
  252. package/dist/src/pi-extension/strip-thinking.js +0 -1
  253. package/dist/src/pi-extension/trim-system-prompt.js +3 -4
  254. package/dist/src/pi-extension/write-guard.js +37 -0
  255. package/dist/src/process/launch-services.js +1 -2
  256. package/dist/src/process/node-bin.js +16 -0
  257. package/dist/src/process/process-group.js +8 -18
  258. package/dist/src/process/process.js +41 -2
  259. package/dist/src/process/run-marker.js +18 -6
  260. package/dist/src/process/supervisor.js +22 -21
  261. package/dist/src/prompt/dialect.js +17 -2
  262. package/dist/src/prompt/gate-notes.js +203 -0
  263. package/dist/src/prompt/gate-prompts.js +121 -211
  264. package/dist/src/prompt/notes-backlog.js +181 -0
  265. package/dist/src/prompt/principles.js +1 -2
  266. package/dist/src/prompt/prompt-followup.js +19 -14
  267. package/dist/src/prompt/prompt.js +129 -204
  268. package/dist/src/redeploy/redeploy-policy.js +24 -19
  269. package/dist/src/redeploy/redeploy-probes.js +1 -1
  270. package/dist/src/redeploy/redeploy.js +2 -3
  271. package/dist/src/redeploy/redeployer.js +20 -20
  272. package/dist/src/redeploy/self-reload.js +2 -1
  273. package/dist/src/report/report-data.js +161 -77
  274. package/dist/src/report/report-fold.js +129 -52
  275. package/dist/src/report/report-main-red.js +55 -0
  276. package/dist/src/report/report-render.js +133 -64
  277. package/dist/src/report/report-types.js +1 -0
  278. package/dist/src/report/report.js +26 -9
  279. package/dist/src/request-timeouts.js +2 -2
  280. package/dist/src/review/known-flakes.js +100 -1
  281. package/dist/src/review/post-merge-review.js +106 -0
  282. package/dist/src/review/protected-edits.js +4 -5
  283. package/dist/src/review/review-followup.js +8 -8
  284. package/dist/src/review/review-precheck.js +66 -23
  285. package/dist/src/review/review-run.js +395 -0
  286. package/dist/src/review/review-verdict.js +2 -2
  287. package/dist/src/review/review.js +155 -360
  288. package/dist/src/review/secret-leaks.js +36 -16
  289. package/dist/src/review/suite-rerun.js +0 -1
  290. package/dist/src/roles/legacy-role-migration.js +80 -7
  291. package/dist/src/roles/role-catalog.js +69 -12
  292. package/dist/src/roles/role-guidance.js +55 -20
  293. package/dist/src/roles/role-labels.js +1 -2
  294. package/dist/src/roles/role-render.js +67 -50
  295. package/dist/src/roles/role-view.js +59 -47
  296. package/dist/src/roles/roles.js +31 -31
  297. package/dist/src/roles/runner-ids.js +97 -0
  298. package/dist/src/run/acceptance-greps.js +119 -0
  299. package/dist/src/run/feedback-filings.js +51 -0
  300. package/dist/src/{tick → run}/qa-coverage.js +9 -27
  301. package/dist/src/{tick/tick-apply.js → run/run-apply.js} +95 -94
  302. package/dist/src/run/run-detail-data.js +179 -0
  303. package/dist/src/run/run-detail-render.js +98 -0
  304. package/dist/src/run/run-detail.js +168 -0
  305. package/dist/src/{tick/tick-finalize.js → run/run-finalize.js} +72 -60
  306. package/dist/src/{tick/tick-outcome.js → run/run-outcome.js} +6 -7
  307. package/dist/src/{tick/tick-prompt.js → run/run-prompt.js} +108 -73
  308. package/dist/src/{tick/tick-resume.js → run/run-resume.js} +55 -26
  309. package/dist/src/{tick/tick-stage.js → run/run-stage.js} +108 -50
  310. package/dist/src/{tick/tick-timing.js → run/run-timing.js} +43 -41
  311. package/dist/src/{tick/tick-usage.js → run/run-usage.js} +45 -37
  312. package/dist/src/{tick/tick-verdict.js → run/run-verdict.js} +91 -56
  313. package/dist/src/run/selfcheck-digest.js +40 -0
  314. package/dist/src/run/stage-backlog-checks.js +249 -0
  315. package/dist/src/run/stage-check.js +172 -0
  316. package/dist/src/run/stage-path-checks.js +730 -0
  317. package/dist/src/{loop → runner}/leftover.js +80 -25
  318. package/dist/src/{loop → runner}/model-fallback.js +12 -12
  319. package/dist/src/runner/revision.js +202 -0
  320. package/dist/src/{loop/loop-fallback.js → runner/runner-fallback.js} +8 -8
  321. package/dist/src/runner/runner-outcome.js +107 -0
  322. package/dist/src/{loop/loop-pi.js → runner/runner-pi.js} +140 -131
  323. package/dist/src/{loop/loop-recovery.js → runner/runner-recovery.js} +21 -15
  324. package/dist/src/{loop/loop-tick.js → runner/runner-run.js} +218 -240
  325. package/dist/src/{loop/loop-state.js → runner/runner-state-load.js} +61 -114
  326. package/dist/src/runner/runner-state.js +102 -0
  327. package/dist/src/{loop/loop.js → runner/runner.js} +123 -120
  328. package/dist/src/scheduling/backoff.js +38 -38
  329. package/dist/src/scheduling/claims.js +35 -22
  330. package/dist/src/scheduling/once-round.js +22 -22
  331. package/dist/src/scheduling/quiet-hours.js +5 -6
  332. package/dist/src/scheduling/scheduling.js +74 -64
  333. package/dist/src/scheduling/work-merged-cache.js +2 -2
  334. package/dist/src/status/status-budget.js +55 -47
  335. package/dist/src/status/status-data.js +43 -16
  336. package/dist/src/status/status-inbox.js +2 -2
  337. package/dist/src/status/status-install.js +0 -1
  338. package/dist/src/status/status-polls.js +28 -28
  339. package/dist/src/status/status-roles.js +39 -13
  340. package/dist/src/team/error-storm.js +8 -8
  341. package/dist/src/team/failure-spread.js +2 -2
  342. package/dist/src/team/orchestrator-info.js +15 -9
  343. package/dist/src/team/reclaim.js +5 -6
  344. package/dist/src/team/team-hold.js +7 -7
  345. package/dist/src/team/team-polls.js +91 -50
  346. package/dist/src/team/team-state.js +8 -2
  347. package/dist/src/text/datetime.js +63 -17
  348. package/dist/src/text/format.js +48 -11
  349. package/dist/src/text/markdown.js +8 -5
  350. package/dist/src/text/phrases.js +57 -23
  351. package/dist/src/text/suggest.js +1 -1
  352. package/dist/src/text/text.js +67 -13
  353. package/dist/src/ui/badges.js +65 -37
  354. package/dist/src/ui/gui/gui-client-backlog.js +292 -0
  355. package/dist/src/ui/gui/gui-client-boot.js +32 -26
  356. package/dist/src/ui/gui/gui-client-composer.js +8 -9
  357. package/dist/src/ui/gui/gui-client-drawer-config.js +212 -0
  358. package/dist/src/ui/gui/gui-client-drawer.js +126 -285
  359. package/dist/src/ui/gui/gui-client-feed.js +271 -0
  360. package/dist/src/ui/gui/gui-client-history.js +434 -67
  361. package/dist/src/ui/gui/gui-client-labels.js +1 -2
  362. package/dist/src/ui/gui/gui-client-markdown.js +1 -2
  363. package/dist/src/ui/gui/gui-client-model.js +27 -36
  364. package/dist/src/ui/gui/gui-client-operator.js +55 -51
  365. package/dist/src/ui/gui/gui-client-pending.js +227 -23
  366. package/dist/src/ui/gui/gui-client-report-chart.js +464 -0
  367. package/dist/src/ui/gui/gui-client-report.js +87 -427
  368. package/dist/src/ui/gui/{gui-client-loops.js → gui-client-roles.js} +43 -44
  369. package/dist/src/ui/gui/gui-client-settings.js +56 -4
  370. package/dist/src/ui/gui/gui-client-team.js +25 -465
  371. package/dist/src/ui/gui/gui-client.js +27 -25
  372. package/dist/src/ui/gui/gui-icons.js +0 -2
  373. package/dist/src/ui/gui/gui-page.js +9 -7
  374. package/dist/src/ui/gui/gui-styles.js +34 -34
  375. package/dist/src/ui/progress-data.js +9 -11
  376. package/dist/src/ui/{tick-progress-model.js → run-progress-model.js} +31 -31
  377. package/dist/src/ui/status-model.js +133 -125
  378. package/dist/src/ui/status-payload.js +136 -49
  379. package/dist/src/ui/status-render.js +101 -82
  380. package/dist/src/ui/team-alerts.js +248 -57
  381. package/dist/src/ui/tone.js +5 -6
  382. package/dist/src/ui/transcript-run.js +58 -0
  383. package/dist/src/ui/transcript-tail.js +28 -20
  384. package/dist/src/ui/transcript.js +63 -23
  385. package/dist/src/ui/tui/tui-app.js +0 -1
  386. package/dist/src/ui/tui/tui-backlog.js +51 -14
  387. package/dist/src/ui/tui/tui-editor-answer.js +27 -31
  388. package/dist/src/ui/tui/tui-editor-attach.js +3 -5
  389. package/dist/src/ui/tui/tui-editor-backends.js +132 -0
  390. package/dist/src/ui/tui/tui-editor-brief.js +10 -12
  391. package/dist/src/ui/tui/tui-editor-budget.js +35 -31
  392. package/dist/src/ui/tui/tui-editor-claude-reserve.js +24 -0
  393. package/dist/src/ui/tui/tui-editor-concurrency.js +24 -0
  394. package/dist/src/ui/tui/tui-editor-config.js +86 -0
  395. package/dist/src/ui/tui/tui-editor-entry.js +36 -47
  396. package/dist/src/ui/tui/tui-editor-fallback.js +31 -0
  397. package/dist/src/ui/tui/tui-editor-instances.js +37 -0
  398. package/dist/src/ui/tui/tui-editor-instructions.js +136 -0
  399. package/dist/src/ui/tui/tui-editor-interval.js +11 -9
  400. package/dist/src/ui/tui/tui-editor-model.js +29 -0
  401. package/dist/src/ui/tui/tui-editor-notes.js +3 -5
  402. package/dist/src/ui/tui/tui-editor-notify.js +25 -0
  403. package/dist/src/ui/tui/tui-editor-pause.js +48 -59
  404. package/dist/src/ui/tui/tui-editor-prompt.js +11 -16
  405. package/dist/src/ui/tui/tui-editor-provider-caps.js +27 -0
  406. package/dist/src/ui/tui/tui-editor-queued.js +5 -9
  407. package/dist/src/ui/tui/tui-editor-quiet.js +99 -35
  408. package/dist/src/ui/tui/tui-editor-retire.js +4 -6
  409. package/dist/src/ui/tui/tui-editor-role-key.js +76 -0
  410. package/dist/src/ui/tui/tui-editor-role-model.js +28 -0
  411. package/dist/src/ui/tui/tui-editor-search.js +45 -33
  412. package/dist/src/ui/tui/tui-editor-session.js +60 -34
  413. package/dist/src/ui/tui/tui-editor-slots.js +23 -0
  414. package/dist/src/ui/tui/tui-editors.js +35 -2
  415. package/dist/src/ui/tui/tui-frame.js +56 -11
  416. package/dist/src/ui/tui/tui-history.js +8 -9
  417. package/dist/src/ui/tui/tui-input.js +13 -11
  418. package/dist/src/ui/tui/tui-keymap.js +0 -1
  419. package/dist/src/ui/tui/tui-keys.js +24 -9
  420. package/dist/src/ui/tui/tui-pane-activity.js +14 -11
  421. package/dist/src/ui/tui/tui-pane-backlog.js +171 -126
  422. package/dist/src/ui/tui/tui-pane-brief.js +0 -1
  423. package/dist/src/ui/tui/tui-pane-ctx.js +2 -2
  424. package/dist/src/ui/tui/tui-pane-health.js +58 -8
  425. package/dist/src/ui/tui/tui-pane-history.js +402 -82
  426. package/dist/src/ui/tui/tui-pane-pending.js +308 -13
  427. package/dist/src/ui/tui/tui-pane-queued.js +71 -55
  428. package/dist/src/ui/tui/tui-pane-report.js +46 -14
  429. package/dist/src/ui/tui/tui-pane-session.js +25 -6
  430. package/dist/src/ui/tui/tui-pane-transcript.js +111 -37
  431. package/dist/src/ui/tui/tui-pane.js +80 -23
  432. package/dist/src/ui/tui/tui-panes.js +100 -30
  433. package/dist/src/ui/tui/tui.js +14 -14
  434. package/dist/src/verdict/fix-claim.js +82 -32
  435. package/dist/src/verdict/no-change.js +4 -4
  436. package/dist/src/verdict/refusal.js +40 -16
  437. package/dist/src/verdict/reply-contract.js +42 -31
  438. package/dist/src/version.js +0 -1
  439. package/package.json +2 -1
  440. package/dist/src/loop/revision.js +0 -70
  441. package/dist/src/roles/loop-ids.js +0 -77
  442. package/dist/src/tick/stage-check.js +0 -419
  443. package/dist/src/tick/telemetry-digest.js +0 -25
  444. package/dist/src/tick/tick-detail-data.js +0 -108
  445. package/dist/src/tick/tick-detail.js +0 -163
  446. package/dist/src/ui/gui/gui-client-sound.js +0 -53
@@ -0,0 +1,96 @@
1
+ /**
2
+ * The dashboard's GET settings/admin handlers (src/gui/gui-server.ts routes to them):
3
+ * prompt-text, doctor, config, retire-list, and brief. Each answers its request and touches no
4
+ * socket beyond its own `res`; the domain work lives one layer down (doctor/doctor.js,
5
+ * config/config.js, operator/retire.js), so this module only adapts HTTP onto it. Barrelled
6
+ * into gui/gui-endpoints.ts beside the other per-family GET endpoint modules.
7
+ */
8
+ import { runDoctor } from "../doctor/doctor.js";
9
+ import { knownRoleIds, loadConfigSafe } from "../config/config.js";
10
+ import { runnerEnabled } from "../roles/runner-ids.js";
11
+ import { artifactPhrase, previewRetire } from "../operator/retire.js";
12
+ import { EDITABLE_CONFIG_KEYS } from "../config/config-editable-keys.js";
13
+ import { readBrief } from "../brief/brief.js";
14
+ import { readQueuedPromptBody } from "../inbox/inbox-edit.js";
15
+ import { rejectBadQueueFileName, rejectBadRole } from "./gui-args.js";
16
+ import { DIRECTOR_ROLE } from "../roles/roles.js";
17
+ import { okGone, sendJson } from "./http-body.js";
18
+ /** Handle GET /api/prompt-text?file=<name>[&role=<id>]: one queued prompt's editable body —
19
+ * the text an edit replaces, for the dashboard's per-row edit affordance (PLANS.md
20
+ * 2026-10-11). The prompt is addressed by its queue-file basename (the /api/prompt-cancel
21
+ * address), so a stale poll's snapshot can never read the wrong entry; the not-before deferral
22
+ * marker and the `[image attached: …]` reference block are stripped, both plumbing the edit
23
+ * carries over unchanged. The role is optional (absent reads the director's queue) and
24
+ * validates exactly like /api/transcript; a file name failing the shared
25
+ * rejectBadQueueFileName guard is a user-input error answered 400 with nothing read. A
26
+ * vanished file answers 200 { ok: true, status: "gone" } — the role already dequeued
27
+ * it, which is data for the UI, not a server fault. Reads one file directly, so it works
28
+ * whether or not the team is running. */
29
+ export function handlePromptText(q, res, root) {
30
+ const roleParam = q.get("role");
31
+ if (roleParam !== null && rejectBadRole(root, res, roleParam))
32
+ return;
33
+ const role = roleParam ?? DIRECTOR_ROLE;
34
+ const file = q.get("file");
35
+ if (rejectBadQueueFileName(res, file))
36
+ return;
37
+ const text = readQueuedPromptBody(root, role, file);
38
+ if (text === null) {
39
+ okGone(res);
40
+ return;
41
+ }
42
+ sendJson(res, 200, { ok: true, text });
43
+ }
44
+ /** Handle GET /api/doctor: the same DoctorReport `tumwater doctor --json` prints, so the
45
+ * Settings view's Health check card and the CLI cannot disagree. Read-only — the checks write
46
+ * nothing (runDoctor's own contract) — and it works whether or not the team is running. `run`
47
+ * exists so tests can skip the host probes; it never probes images ("never") — the explicit
48
+ * re-probe is POST /api/doctor-reprobe; a throw reaches the server's outer 500. */
49
+ export async function handleDoctor(res, root, run = (r) => runDoctor(r, undefined, undefined, "never")) {
50
+ sendJson(res, 200, await run(root));
51
+ }
52
+ /** Handle GET /api/config: the Settings view's resolved values for exactly
53
+ * EDITABLE_CONFIG_KEYS — read through the same load path `tumwater config get` uses
54
+ * (loadConfigSafe, defaults merged in), so the page shows what the team would actually
55
+ * load. A broken or invalid tumwater.json answers 500 with validateConfig's message instead
56
+ * of serving a half-empty panel. */
57
+ export function handleConfig(res, root) {
58
+ const { config, error } = loadConfigSafe(root);
59
+ if (config === undefined) {
60
+ sendJson(res, 500, { error });
61
+ return;
62
+ }
63
+ const record = config;
64
+ const out = {};
65
+ for (const key of EDITABLE_CONFIG_KEYS)
66
+ out[key] = record[key] ?? null;
67
+ sendJson(res, 200, out);
68
+ }
69
+ /** Handle GET /api/retire: the Settings view's "Disabled roles" card — every configured,
70
+ * disabled, non-director role, with what `tumwater retire` would remove (previewRetire, so the
71
+ * rails and wording are the CLI's) and the rails' objections. `wouldRemove` is empty when there
72
+ * is nothing to retire (a role disabled with a clean checkout), so the card can also re-enable
73
+ * a role via POST /api/role-enable. Read-only (previewRetire's contract) and works whether or
74
+ * not the team runs; a throw reaches the server's outer 500, an unreadable tumwater.json
75
+ * answers 500 like /api/config. */
76
+ export async function handleRetireList(res, root) {
77
+ const { config, error } = loadConfigSafe(root);
78
+ if (config === undefined) {
79
+ sendJson(res, 500, { error });
80
+ return;
81
+ }
82
+ const roles = [];
83
+ for (const role of knownRoleIds(config).filter((id) => id !== DIRECTOR_ROLE && !runnerEnabled(config, id))) {
84
+ const { wouldRemove, objections } = await previewRetire(root, role);
85
+ roles.push({ role, wouldRemove, phrases: wouldRemove.map((a) => artifactPhrase(a, role)), objections });
86
+ }
87
+ sendJson(res, 200, { roles });
88
+ }
89
+ /** Handle GET /api/brief: the brief file's name and the current brief text (what
90
+ * `tumwater brief --json` prints), or 404 when no brief carries a prompt block. */
91
+ export function handleBrief(res, root) {
92
+ const brief = readBrief(root);
93
+ if (brief === null)
94
+ return sendJson(res, 404, { error: "no project brief with a prompt block found — run `tumwater init` first" });
95
+ sendJson(res, 200, { file: brief.file, prompt: brief.prompt });
96
+ }
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * The request-argument validators behind the dashboard's /api endpoints (gui/gui-endpoints.ts's
3
- * GET handlers and gui/gui-endpoint-commands.ts's POST handlers call them): loop-targeting role
3
+ * GET handlers and gui/gui-endpoint-commands.ts's POST handlers call them): role-targeting role
4
4
  * validation, integer query parsing, the report
5
5
  * endpoints' days window, and the body-field checks the paired operator endpoints
6
6
  * (/api/prompt and /api/prompt-role, /api/pause and /api/pause-role) must answer with
@@ -14,10 +14,10 @@ import { REPORT_DEFAULT_DAYS, REPORT_MAX_DAYS } from "../events/event-window.js"
14
14
  import { promptLengthProblem } from "../inbox/inbox-submit.js";
15
15
  import { queueFileNameProblem } from "../inbox/inbox-cancel.js";
16
16
  import { DIRECTOR_ROLE } from "../roles/roles.js";
17
- import { gotSuffix, parseNonNegativeInt, parsePositiveInt } from "../text/text.js";
17
+ import { gotSuffix, parseNonNegativeInt, parsePositiveInt, splitCommaListUnique } from "../text/text.js";
18
18
  import { typoSuffix } from "../text/suggest.js";
19
19
  import { badRequest, sendJson } from "./http-body.js";
20
- /** The role ids a loop-targeting endpoint accepts: when tumwater.json parses, catalog +
20
+ /** The role ids a role-targeting endpoint accepts: when tumwater.json parses, catalog +
21
21
  * customRoles (knownRoleIds); a transiently broken file falls back to the built-in catalog
22
22
  * rather than refusing every id — the rule config.knownRoleIdsCached owns. Shared by
23
23
  * /api/transcript and the two operator endpoints so their validation and 400 wording cannot
@@ -25,7 +25,7 @@ import { badRequest, sendJson } from "./http-body.js";
25
25
  export function validRoleIds(root) {
26
26
  return knownRoleIdsCached(root);
27
27
  }
28
- /** Validate one request's loop-targeting role against validRoleIds, sending the shared 400 on
28
+ /** Validate one request's role-targeting role against validRoleIds, sending the shared 400 on
29
29
  * a miss and reporting whether the request should stop: an absent id reads "role required
30
30
  * (valid ids: …)" — unless `allowMissing`, the wake endpoint's `{}` → all-roles default,
31
31
  * which only a truly absent `undefined` may ride; an explicit null is always rejected — and a
@@ -50,6 +50,24 @@ export function rejectBadRole(root, res, role, allowMissing = false) {
50
50
  }
51
51
  return false;
52
52
  }
53
+ /** Parse the GET endpoints' `?role=` comma-separated list (each id trimmed, deduplicated, and
54
+ * validated by rejectBadRole; an empty list or unknown id → 400). Returns undefined when
55
+ * `role` is absent and null once a 400 is sent. Shared by /api/diff and the report endpoints'
56
+ * reportScope so the role-list rule cannot drift between them. */
57
+ export function roleListQuery(q, res, root) {
58
+ const raw = q.get("role");
59
+ if (raw === null)
60
+ return undefined;
61
+ const roles = splitCommaListUnique(raw);
62
+ if (roles.length === 0) {
63
+ rejectBadRole(root, res, raw);
64
+ return null;
65
+ }
66
+ for (const r of roles)
67
+ if (rejectBadRole(root, res, r))
68
+ return null;
69
+ return roles;
70
+ }
53
71
  /** Validate one request's file-addressed queue name (a queued prompt's basename) through
54
72
  * inbox-cancel.ts's queueFileNameProblem, sending its reason as the shared 400 and reporting
55
73
  * whether the request should stop — the file twin of rejectBadRole above. The three
@@ -87,6 +105,11 @@ export function intQuery(q, res, name, kind, fallback) {
87
105
  }
88
106
  return parsed;
89
107
  }
108
+ /** Clamp a parsed count into 1..max: the bound every GUI count query (`n`, `days`) applies after
109
+ * its parse, so a request never asks for zero or more than the endpoint's cap. */
110
+ export function clampCount(n, max) {
111
+ return Math.min(max, Math.max(1, n));
112
+ }
90
113
  /** The window both report endpoints serve, from ?days=N on the request. One exact rule,
91
114
  * shared so /api/report and /api/failures cannot drift: missing or non-decimal →
92
115
  * REPORT_DEFAULT_DAYS, out-of-range clamped to 1..REPORT_MAX_DAYS (the same bounds the CLI's
@@ -97,7 +120,7 @@ export function intQuery(q, res, name, kind, fallback) {
97
120
  * coerced value — raw Number.parseInt would read "1e3" as 1, "0x10" as 0, and "-5" as -5. */
98
121
  export function windowDays(q) {
99
122
  const n = parseNonNegativeInt(q.get("days") ?? "");
100
- return n === null ? REPORT_DEFAULT_DAYS : Math.min(REPORT_MAX_DAYS, Math.max(1, n));
123
+ return n === null ? REPORT_DEFAULT_DAYS : clampCount(n, REPORT_MAX_DAYS);
101
124
  }
102
125
  /** Pull the prompt text out of a prompt endpoint's body — the shared validator for
103
126
  * /api/prompt and /api/prompt-role, which must reject a non-string, blank, or over-long text
@@ -107,7 +130,7 @@ export function windowDays(q) {
107
130
  export function requirePromptText(res, body, role = DIRECTOR_ROLE) {
108
131
  const text = body.text;
109
132
  if (typeof text !== "string") {
110
- sendJson(res, 400, { error: `text must be a string${text === undefined ? "" : gotSuffix(text)}` });
133
+ sendJson(res, 400, { error: `text must be a string${gotSuffix(text)}` });
111
134
  return null;
112
135
  }
113
136
  if (!text.trim()) {
@@ -140,6 +163,29 @@ export function sinceMsQuery(q, res, maxMs, overCap) {
140
163
  }
141
164
  return sinceMs;
142
165
  }
166
+ /** Parse the report, failures, history and logs endpoints' optional `?untilMs=` window end — how
167
+ * long ago a `sinceMs` window stops, the twin of the CLI's `--until`. Absent → undefined (the
168
+ * window ends now). Present without a `sinceMs` → 400 (the CLI's "--until requires --since"),
169
+ * so the count and day shapes never ignore it silently; otherwise the value rides intQuery's
170
+ * positive-integer rule, and one not shorter than `sinceMs` is a 400 (the CLI's "must be
171
+ * shorter than --since"; sinceMs is already capped, so this bounds untilMs too). Returns the
172
+ * parsed length, undefined when absent, or null once the 400 is sent. */
173
+ export function untilMsQuery(q, res, sinceMs) {
174
+ if (q.get("untilMs") === null)
175
+ return undefined;
176
+ if (sinceMs === undefined) {
177
+ badRequest(res, "untilMs requires sinceMs (it ends a sinceMs window that long ago)");
178
+ return null;
179
+ }
180
+ const untilMs = intQuery(q, res, "untilMs", "positive");
181
+ if (untilMs === null)
182
+ return null;
183
+ if (untilMs >= sinceMs) {
184
+ badRequest(res, `untilMs (${untilMs}) must be shorter than sinceMs (${sinceMs})`);
185
+ return null;
186
+ }
187
+ return untilMs;
188
+ }
143
189
  /** Pull the required curated config key out of a config endpoint's body — the shared validator
144
190
  * for /api/config-set and /api/config-unset, which must reject a missing or uncurated key with
145
191
  * the same 400 wording: the panel's reach stays exactly EDITABLE_CONFIG_KEYS, and the browser and
@@ -149,11 +195,36 @@ export function sinceMsQuery(q, res, maxMs, overCap) {
149
195
  export function requireConfigKey(res, body) {
150
196
  const key = body.key;
151
197
  if (typeof key !== "string" || !EDITABLE_CONFIG_KEYS.includes(key)) {
152
- badRequest(res, `key must be one of ${EDITABLE_CONFIG_KEYS.join(", ")}${key === undefined ? "" : gotSuffix(key)}${typeof key === "string" ? typoSuffix(key, EDITABLE_CONFIG_KEYS) : ""}`);
198
+ badRequest(res, `key must be one of ${EDITABLE_CONFIG_KEYS.join(", ")}${gotSuffix(key)}${typeof key === "string" ? typoSuffix(key, EDITABLE_CONFIG_KEYS) : ""}`);
153
199
  return null;
154
200
  }
155
201
  return key;
156
202
  }
203
+ /** Pull the required, non-blank string `role` out of a config POST body — the shared 400
204
+ * (`role required`) for the role-cap, min-run-interval, instances, model, instructions,
205
+ * role-enable/disable, and quiet-hours handlers in gui-config-commands.ts. Returns the role
206
+ * as sent (untrimmed, not yet validated against the role list), or null once the 400 is sent. */
207
+ export function requireRoleField(res, body) {
208
+ const role = body.role;
209
+ if (typeof role !== "string" || role.trim() === "") {
210
+ badRequest(res, "role required");
211
+ return null;
212
+ }
213
+ return role;
214
+ }
215
+ /** Pull a required string field out of a POST body — the shared 400 for the editors whose body
216
+ * carries a full replacement text that may be "" to clear it (role instructions, quiet-hours
217
+ * window, role notes, the brief, and the model handler's optional provider/model/thinking
218
+ * fields). `meaning` completes the message after the em dash. Returns the string as sent, or
219
+ * null once the 400 is sent. */
220
+ export function requireStringField(res, body, key, meaning) {
221
+ const value = body[key];
222
+ if (typeof value !== "string") {
223
+ badRequest(res, `"${key}" must be a string — ${meaning}${gotSuffix(value)}`);
224
+ return null;
225
+ }
226
+ return value;
227
+ }
157
228
  /** Pull the required `paused` boolean out of a pause endpoint's body — the shared validator
158
229
  * for /api/pause and /api/pause-role, which must reject a missing or non-boolean target state
159
230
  * with the same 400 wording: the target state is explicit (`paused: true|false`), so a retried
@@ -162,7 +233,7 @@ export function requireConfigKey(res, body) {
162
233
  export function requirePausedFlag(res, body) {
163
234
  const value = body.paused;
164
235
  if (typeof value !== "boolean") {
165
- sendJson(res, 400, { error: `paused must be a boolean${value === undefined ? "" : gotSuffix(value)}` });
236
+ sendJson(res, 400, { error: `paused must be a boolean${gotSuffix(value)}` });
166
237
  return null;
167
238
  }
168
239
  return value;
@@ -1,4 +1,3 @@
1
- // vocabulary: plain
2
1
  /**
3
2
  * The dashboard's POST backlog/QUESTIONS command handlers (barrelled into
4
3
  * gui/gui-endpoint-commands.ts, which routes to them from src/gui/gui-server.ts):
@@ -8,9 +7,14 @@
8
7
  * HTTP onto the domain work one layer down (backlog-write, question-answer, backlog-commit)
9
8
  * and touches no socket beyond its own `res`.
10
9
  */
10
+ import { backlogHolds, cancelledClaimNote, stillHeldNote } from "../backlog/backlog-eligibility.js";
11
+ import { entryKey, plansReleasedBy, releasedPartsNote } from "../backlog/backlog-parts.js";
12
+ import { plannedPlanEntries } from "../backlog/backlog.js";
13
+ import { allLiveClaims } from "../runner/runner-state-load.js";
11
14
  import { commitOperatorBacklogEdit } from "../backlog/backlog-commit.js";
12
15
  import { stampedSubject } from "../git/commit-message.js";
13
- import { fileEntryWithBody, normalizeEntryBody, OPERATOR_FILE_KINDS, tryCancelEntry, tryEditEntry, tryMoveEntryToTop, tryReleaseEntry, bodyStructureProblem } from "../backlog/backlog-write.js";
16
+ import { fileEntryWithBody, normalizeEntryBody, OPERATOR_FILE_KINDS, tryCancelEntry, tryEditEntry, bodyStructureProblem } from "../backlog/backlog-write.js";
17
+ import { tryMoveEntryToTop, tryReleaseEntry } from "../backlog/backlog-release.js";
14
18
  import { tryAnswerQuestion, UNCLOSED_FENCE_MESSAGE } from "../backlog/question-answer.js";
15
19
  import { promptLengthProblem } from "../inbox/inbox-submit.js";
16
20
  import { requestWake, submitRolePromptAndWake, wakeQuestionAsker } from "../operator/operator-intent.js";
@@ -23,7 +27,7 @@ import { readPostBody } from "./gui-endpoint-common.js";
23
27
  function checkedBacklogKind(res, body) {
24
28
  const kind = body.kind;
25
29
  if (kind !== "bug" && kind !== "plan") {
26
- badRequest(res, `kind must be "bug" or "plan"${kind === undefined ? "" : gotSuffix(kind)}`);
30
+ badRequest(res, `kind must be "bug" or "plan"${gotSuffix(kind)}`);
27
31
  return null;
28
32
  }
29
33
  return kind;
@@ -36,11 +40,11 @@ function checkedBacklogKind(res, body) {
36
40
  function checkedEntryPositionAndTitle(res, body) {
37
41
  const { position, title } = body;
38
42
  if (typeof position !== "number" || !Number.isInteger(position) || position < 1) {
39
- badRequest(res, `position must be a positive integer${position === undefined ? "" : gotSuffix(position)}`);
43
+ badRequest(res, `position must be a positive integer${gotSuffix(position)}`);
40
44
  return null;
41
45
  }
42
46
  if (typeof title !== "string" || title === "") {
43
- badRequest(res, `title must be a non-empty string${title === undefined ? "" : gotSuffix(title)}`);
47
+ badRequest(res, `title must be a non-empty string${gotSuffix(title)}`);
44
48
  return null;
45
49
  }
46
50
  return { position, title };
@@ -105,7 +109,7 @@ export async function handleQuestionAnswer(req, res, root) {
105
109
  }
106
110
  /** Handle POST /api/backlog-file: the Plans/Bugs tabs' New control — the same entry and wake
107
111
  * `tumwater bug`/`tumwater plan` produce (fileEntryWithBody, then the shared wake text to the
108
- * bugfix/feature loop). Body `{"kind": "bug"|"plan", "title": "...", "body": "optional
112
+ * bugfix/feature role). Body `{"kind": "bug"|"plan", "title": "...", "body": "optional
109
113
  * markdown"}`. The empty-title and wake-length checks run here, before anything is written:
110
114
  * the body is normalized and checked by bodyStructureProblem too, and an over-long wake
111
115
  * would throw after the entry merged. */
@@ -147,7 +151,8 @@ export async function handleBacklogFile(req, res, root) {
147
151
  * `tumwater plan|bug --cancel <n>` through the non-exiting tryCancelEntry. Body `{"kind":
148
152
  * "bug"|"plan", "position": <1-based>, "title": "<heading>"}`; a missing position or a
149
153
  * different heading there answers 200 { ok: true, status: "gone" } (the list moved since the
150
- * poll). No wake and no inbox prompt: removing work must not wake a loop. */
154
+ * poll). A cancelled plan that planned parts required adds `releasedNote` naming them, as
155
+ * `plan --cancel` prints. No wake and no inbox prompt: removing work must not wake a role. */
151
156
  export async function handleBacklogCancel(req, res, root) {
152
157
  const body = await readPostBody(req, res, '{"kind": "bug", "position": 1, "title": "<heading>"}');
153
158
  if (!body)
@@ -159,21 +164,30 @@ export async function handleBacklogCancel(req, res, root) {
159
164
  if (!pair)
160
165
  return;
161
166
  const { position, title } = pair;
167
+ const claims = allLiveClaims(root);
162
168
  const outcome = outcomeOrBadRequest(res, () => tryCancelEntry(root, kind, position, title));
163
169
  if (outcome === null)
164
170
  return;
165
171
  if (outcome.status === "cancelled") {
172
+ const workingNote = cancelledClaimNote(claims.get(entryKey(outcome.title)) ?? null);
173
+ const releasedNote = kind === "plan" ? releasedPartsNote(plansReleasedBy(plannedPlanEntries(root), outcome.title)) : "";
166
174
  const committed = await commitOperatorBacklogEdit(root, outcome.file, stampedSubject("operator", `Cancel ${kind}: ${outcome.title}`)); // vocabulary-ok: the commit prefix stays tumwater(operator):
167
- sendJson(res, 200, { ok: true, status: "cancelled", file: outcome.file, title: outcome.title, committed });
175
+ sendJson(res, 200, { ok: true, status: "cancelled", file: outcome.file, title: outcome.title, committed, ...(workingNote === "" ? {} : { workingNote }), ...(releasedNote === "" ? {} : { releasedNote }) });
168
176
  }
169
177
  else {
170
178
  okGone(res);
171
179
  }
172
180
  }
181
+ /** The hold on the kind's entry at a 1-based position (backlogHolds), `null` when eligible. */
182
+ function heldAt(root, kind, position) {
183
+ const holds = backlogHolds(root);
184
+ return (kind === "bug" ? holds.bugs : holds.plans)[position - 1] ?? null;
185
+ }
173
186
  /** Handle POST /api/backlog-release: the entry drawer's release — the same lock and splice as
174
187
  * `tumwater plan|bug --release <n>` through the non-exiting tryReleaseEntry. Body as
175
- * backlog-cancel's. A released entry is newly eligible, so the kind's role is woken; `gone`
176
- * and `moved` answer okGone, and an entry with neither a Refused nor a Waiting-for-the-next-
188
+ * backlog-cancel's. A released entry that is not still held is newly eligible, so the kind's
189
+ * role is woken (a held one wakes no role and carries `heldNote`); `gone` and `moved` answer
190
+ * okGone, and an entry with neither a Refused nor a Waiting-for-the-next-
177
191
  * failure note answers status "not_refused" with no write and no wake. */
178
192
  export async function handleBacklogRelease(req, res, root) {
179
193
  const body = await readPostBody(req, res, '{"kind": "bug", "position": 1, "title": "<heading>"}');
@@ -191,8 +205,10 @@ export async function handleBacklogRelease(req, res, root) {
191
205
  return;
192
206
  if (outcome.status === "released") {
193
207
  const committed = await commitOperatorBacklogEdit(root, outcome.file, stampedSubject("operator", `Release ${kind}: ${outcome.title}`)); // vocabulary-ok: the commit prefix stays tumwater(operator):
194
- requestWake(root, [OPERATOR_FILE_KINDS[kind].role]);
195
- sendJson(res, 200, { ok: true, status: "released", file: outcome.file, title: outcome.title, committed });
208
+ const hold = heldAt(root, kind, position);
209
+ if (hold === null)
210
+ requestWake(root, [OPERATOR_FILE_KINDS[kind].role]);
211
+ sendJson(res, 200, { ok: true, status: "released", file: outcome.file, title: outcome.title, committed, ...(hold === null ? {} : { heldNote: stillHeldNote(hold) }) });
196
212
  }
197
213
  else if (outcome.status === "not_refused") {
198
214
  sendJson(res, 200, { ok: true, status: "not_refused", title: outcome.title });
@@ -203,8 +219,9 @@ export async function handleBacklogRelease(req, res, root) {
203
219
  }
204
220
  /** Handle POST /api/backlog-top: the entry drawer's Move to top — the same splice as
205
221
  * `tumwater plan|bug --top <n>` through the non-exiting tryMoveEntryToTop. Body as
206
- * backlog-cancel's. Reordering makes no entry newly eligible, so no role is woken; position 1
207
- * answers status "already_first" with no write, and `gone` and `moved` answer okGone. */
222
+ * backlog-cancel's. Reordering makes no entry newly eligible, so no role is woken (a held entry
223
+ * carries `heldNote`); position 1 answers status "already_first" with no write, and `gone` and
224
+ * `moved` answer okGone. */
208
225
  export async function handleBacklogTop(req, res, root) {
209
226
  const body = await readPostBody(req, res, '{"kind": "bug", "position": 2, "title": "<heading>"}');
210
227
  if (!body)
@@ -221,7 +238,8 @@ export async function handleBacklogTop(req, res, root) {
221
238
  return;
222
239
  if (outcome.status === "moved_to_top") {
223
240
  const committed = await commitOperatorBacklogEdit(root, outcome.file, stampedSubject("operator", `Prioritize ${kind}: ${outcome.title}`)); // vocabulary-ok: the commit prefix stays tumwater(operator):
224
- sendJson(res, 200, { ok: true, status: "moved_to_top", file: outcome.file, title: outcome.title, committed });
241
+ const hold = heldAt(root, kind, 1);
242
+ sendJson(res, 200, { ok: true, status: "moved_to_top", file: outcome.file, title: outcome.title, committed, ...(hold === null ? {} : { heldNote: stillHeldNote(hold) }) });
225
243
  }
226
244
  else if (outcome.status === "already_first") {
227
245
  sendJson(res, 200, { ok: true, status: "already_first", title: outcome.title });
@@ -233,8 +251,10 @@ export async function handleBacklogTop(req, res, root) {
233
251
  /** Handle POST /api/backlog-edit: the entry drawer's edit — tryEditEntry's title-checked,
234
252
  * in-place rewrite under the state lock. Body `{"kind", "position", "title", "newTitle",
235
253
  * "newBody"}`; a missing position or a different heading there answers 200 { ok: true, status:
236
- * "gone" }. The body keeps its line structure. No wake and no inbox prompt: whichever tick picks
237
- * the entry up refines it. */
254
+ * "gone" }; text identical to the entry's own answers 200 { ok: true, status: "unchanged", file,
255
+ * title } with nothing written or committed. An edited entry that is still held adds `heldNote`
256
+ * (stillHeldNote). The body keeps its line structure. No wake and no inbox prompt: whichever run
257
+ * picks the entry up refines it. */
238
258
  export async function handleBacklogEdit(req, res, root) {
239
259
  const body = await readPostBody(req, res, '{"kind": "bug", "position": 1, "title": "<heading>", "newTitle": "...", "newBody": "..."}');
240
260
  if (!body)
@@ -265,7 +285,11 @@ export async function handleBacklogEdit(req, res, root) {
265
285
  return;
266
286
  if (outcome.status === "edited") {
267
287
  const committed = await commitOperatorBacklogEdit(root, outcome.file, stampedSubject("operator", `Edit ${kind}: ${outcome.title}`)); // vocabulary-ok: the commit prefix stays tumwater(operator):
268
- sendJson(res, 200, { ok: true, status: "edited", file: outcome.file, title: outcome.title, committed });
288
+ const hold = heldAt(root, kind, position);
289
+ sendJson(res, 200, { ok: true, status: "edited", file: outcome.file, title: outcome.title, committed, ...(hold === null ? {} : { heldNote: stillHeldNote(hold) }) });
290
+ }
291
+ else if (outcome.status === "unchanged") {
292
+ sendJson(res, 200, { ok: true, status: "unchanged", file: outcome.file, title: outcome.title });
269
293
  }
270
294
  else {
271
295
  okGone(res);
@@ -0,0 +1,155 @@
1
+ /**
2
+ * The dashboard's GET backlog and unmerged-change handlers (src/gui/gui-server.ts routes to
3
+ * them): backlog, backlog-match, and diff. Each answers its request and touches no socket
4
+ * beyond its own `res`; the domain work lives one layer down (backlog/backlog.ts,
5
+ * inbox/inbox.ts, change/change-data.ts), so this module only adapts HTTP onto it. Barrelled
6
+ * into gui/gui-endpoints.ts beside the other per-family GET endpoint modules.
7
+ */
8
+ import { operatorHeldEntries, refusalNote, waitingNote } from "../backlog/backlog-eligibility.js";
9
+ import { answeredQuestionEntries, entryMatches, donePlanEntries, fixedBugEntries, openBugEntries, openQuestionEntries, plannedPlanEntries } from "../backlog/backlog.js";
10
+ import { collectChangeOverlaps, scopeChangeOverlaps, scopeChangeOverlapsToPath, collectTeamChanges, collectRoleChange, normalizeDiffPath } from "../change/change-data.js";
11
+ import { listWideScope, queuedListRows } from "../inbox/inbox.js";
12
+ import { intQuery, rejectBadRole, roleListQuery } from "./gui-args.js";
13
+ import { badRequest, sendJson } from "./http-body.js";
14
+ import { typoSuffix } from "../text/suggest.js";
15
+ /** The /api/backlog `file` values and the open-section reader each addresses — one table so
16
+ * the on-demand dispatch and the "valid values" list in both 400s cannot drift apart. A Map,
17
+ * not an object literal, so no `file` spelling (`constructor`, `toString`, `__proto__`) can
18
+ * reach an inherited member through the lookup. Insertion order is the list order. */
19
+ const BACKLOG_FILE_READERS = new Map([
20
+ ["plans", plannedPlanEntries],
21
+ ["bugs", openBugEntries],
22
+ ["questions", openQuestionEntries],
23
+ ["done", donePlanEntries],
24
+ ["fixed", fixedBugEntries],
25
+ ["answered", (root) => answeredQuestionEntries(root).reverse()],
26
+ ]);
27
+ const BACKLOG_FILE_NAMES = [...BACKLOG_FILE_READERS.keys()];
28
+ const BACKLOG_FILE_VALUES = BACKLOG_FILE_NAMES.join(", ");
29
+ /** Handle GET /api/backlog?file=<plans|bugs|questions|done|fixed|answered>&index=N: one
30
+ * backlog entry's full text ({title, body}), fetched on demand so multi-KB bodies (long repros,
31
+ * whole plans) never ride the 1-second /api/status poll. index addresses the Nth entry of that
32
+ * file's section in the same order statusPayload lists its titles — plans: PLANS.md ## Planned,
33
+ * bugs: BUGS.md ## Open, questions: QUESTIONS.md ## Open, done: PLANS.md ## Done and fixed:
34
+ * BUGS.md ## Fixed (both newest first, the order of the dashboard's recent rows) and answered:
35
+ * QUESTIONS.md ## Answered, newest first (the file is appended to, so the reverse of file
36
+ * order, unlike done/fixed) — zero-based.
37
+ * Unknown/missing file, missing or bad index, and out-of-range index
38
+ * → 400 JSON error via sendJson.
39
+ * On a held-for-operator plan (file === "plans" and the entry in
40
+ * operatorHeldEntries' selection), the 200 payload adds `heldForOperator: true` so the drawer
41
+ * shows its Release button there too; every other entry's payload carries no such key.
42
+ * The query arrives pre-parsed (gui/gui-server.ts's parseRequestTarget), like every
43
+ * GET-data handler here. */
44
+ export function handleBacklog(q, res, root) {
45
+ const file = q.get("file");
46
+ if (file === null) {
47
+ badRequest(res, `file required (valid values: ${BACKLOG_FILE_VALUES})`);
48
+ return;
49
+ }
50
+ const reader = BACKLOG_FILE_READERS.get(file);
51
+ if (!reader) {
52
+ badRequest(res, `unknown file ${JSON.stringify(file)} (valid values: ${BACKLOG_FILE_VALUES})${typoSuffix(file, BACKLOG_FILE_NAMES)}`);
53
+ return;
54
+ }
55
+ const entries = reader(root);
56
+ const index = intQuery(q, res, "index", "non-negative");
57
+ if (index === null)
58
+ return;
59
+ const entry = entries[index];
60
+ if (!entry) {
61
+ badRequest(res, `index ${index} out of range (${file} has ${entries.length} entries)`);
62
+ return;
63
+ }
64
+ sendJson(res, 200, {
65
+ title: entry.title,
66
+ body: entry.body,
67
+ refused: refusalNote(entry.body) !== null,
68
+ waiting: waitingNote(entry.body) !== null,
69
+ ...(file === "plans" && operatorHeldEntries(root).some((e) => e.index === index) ? { heldForOperator: true } : {}),
70
+ });
71
+ }
72
+ /** Handle GET /api/backlog-match?grep=<text>: for each /api/backlog `file`, the ascending
73
+ * indexes (the ones /api/backlog?file=…&index=N addresses) whose entry heading or body contains
74
+ * the trimmed text, case-insensitively — the match `tumwater backlog --grep` and the TUI's `/`
75
+ * use — so the dashboard's Search can find text bodies hold without bodies riding the status
76
+ * poll. A `queued` key adds the queued messages whose full text (not only the 80-character
77
+ * preview) contains the text, as `{ role, file }` with the queue-file basename, director first —
78
+ * the match `tumwater prompt --list --grep` uses. Missing or blank grep → 400. */
79
+ export function handleBacklogMatch(q, res, root) {
80
+ const needle = (q.get("grep") ?? "").trim();
81
+ if (!needle) {
82
+ badRequest(res, "grep required");
83
+ return;
84
+ }
85
+ const out = {};
86
+ for (const [file, reader] of BACKLOG_FILE_READERS) {
87
+ out[file] = reader(root).flatMap((entry, i) => (entryMatches(entry, needle) ? [i] : []));
88
+ }
89
+ const lower = needle.toLowerCase();
90
+ out.queued = queuedListRows(root, null, listWideScope(root))
91
+ .filter((row) => row.text.toLowerCase().includes(lower))
92
+ .map((row) => ({ role: row.role, file: row.file }));
93
+ sendJson(res, 200, out);
94
+ }
95
+ /** Handle GET /api/diff[?role=<id>]: the unmerged-change view the CLI prints from the same
96
+ * collectors — no `role` → collectTeamChanges (exactly `tumwater diff --json`'s document:
97
+ * `{ mainBranch, roles }`, no per-role patch halves); `?role=<id>` → collectRoleChange (the
98
+ * full per-role view with `diff`/`uncommittedDiff`, `tumwater diff --role <id> --json`). The
99
+ * role is a target, not a filter, so it validates through rejectBadRole exactly like
100
+ * /api/transcript and /api/run (unknown/missing → 400 naming the valid ids) — the same ids
101
+ * `parseRoleFlag` accepts on the CLI, including disabled and user-defined roles. The
102
+ * collectors degrade absent worktrees and missing base branches to their `state` instead of
103
+ * throwing, so a fresh or half-built repo still answers 200. Reads git plumbing directly, so
104
+ * it works whether or not the team is running. `?overlaps=1` serves collectChangeOverlaps
105
+ * (exactly `tumwater diff --overlaps --json`'s `{ mainBranch, state, overlaps }`); with `role` (a
106
+ * comma-separated list, each id validated) it keeps the files a listed role holds, exactly
107
+ * `tumwater diff --overlaps --role <ids> --json`; `&path=<path>` keeps the files at or under that
108
+ * path, exactly `tumwater diff --overlaps --path <path> --json`. `?role=<id>&stat=1` serves both
109
+ * halves as the `git diff --stat` summary, exactly `tumwater diff --role <id> --stat --json`. */
110
+ export async function handleDiff(q, res, root) {
111
+ const rawPath = q.get("path");
112
+ const stat = q.get("stat") !== null;
113
+ if (q.get("overlaps") !== null) {
114
+ if (stat) {
115
+ badRequest(res, "overlaps lists paths, not patches; drop stat");
116
+ return;
117
+ }
118
+ const opath = rawPath === null ? undefined : normalizeDiffPath(rawPath);
119
+ if (opath === null) {
120
+ badRequest(res, "path takes a repo-relative path such as src/cli.ts (as overlaps lists it)");
121
+ return;
122
+ }
123
+ const roles = roleListQuery(q, res, root);
124
+ if (roles === null)
125
+ return;
126
+ let view = await collectChangeOverlaps(root);
127
+ if (roles !== undefined)
128
+ view = scopeChangeOverlaps(view, roles);
129
+ if (opath !== undefined)
130
+ view = scopeChangeOverlapsToPath(view, opath);
131
+ sendJson(res, 200, view);
132
+ return;
133
+ }
134
+ const role = q.get("role");
135
+ if (role === null) {
136
+ if (stat) {
137
+ badRequest(res, "stat needs role (the team roster carries no patch)");
138
+ return;
139
+ }
140
+ if (rawPath !== null) {
141
+ badRequest(res, "path needs role (the team roster carries no patch)");
142
+ return;
143
+ }
144
+ sendJson(res, 200, await collectTeamChanges(root));
145
+ return;
146
+ }
147
+ if (rejectBadRole(root, res, role))
148
+ return;
149
+ const path = rawPath === null ? undefined : normalizeDiffPath(rawPath);
150
+ if (path === null) {
151
+ badRequest(res, "path takes a repo-relative path such as src/cli.ts (as overlaps lists it)");
152
+ return;
153
+ }
154
+ sendJson(res, 200, await collectRoleChange(root, role, { ...(path === undefined ? {} : { path }), ...(stat ? { stat: true } : {}) }));
155
+ }
@@ -6,8 +6,7 @@ import os from "node:os";
6
6
  import { startGui } from "./gui-server.js";
7
7
  import { errCode } from "../errno.js";
8
8
  import { fail, say } from "../cli/cli-output.js";
9
- import { flagValue, parsePortFlag } from "../cli/cli-args.js";
10
- import { TOKEN_VALUE_ERROR } from "../cli/cli-flag-specs.js";
9
+ import { flagValue, parsePortFlag, tokenValueProblem } from "../cli/cli-args.js";
11
10
  /** External IPv4 addresses of this machine's network interfaces, for printing the URLs a
12
11
  * `gui --all-interfaces` server is reachable at. IPv6 and internal (loopback) addresses are
13
12
  * skipped: the loopback URL is printed separately, and bracketed IPv6 URLs are rarely what
@@ -43,17 +42,13 @@ export async function cmdGui(root, args, seams = {}) {
43
42
  const port = portRaw !== null ? parsePortFlag(portRaw) : 7180;
44
43
  const allInterfaces = args.includes("--all-interfaces");
45
44
  // A valueless or empty --token is a CLI error, not an open server: an operator who
46
- // asked for protection must never silently get none. A flag-looking value is the
47
- // same mistake in disguise — a valued flag claims the next token even when it is a
48
- // known flag, so `--token --all-interfaces` would serve with the literal secret
49
- // "--all-interfaces" (--all-interfaces still takes effect, since it is read straight
50
- // from args) while the real secret sits unreached after another flag.
51
- const tokenRaw = flagValue(args, "--token");
52
- const token = tokenRaw ?? "";
53
- if (tokenRaw !== null && !token)
54
- fail(TOKEN_VALUE_ERROR);
55
- if (tokenRaw !== null && token.startsWith("--"))
56
- fail(`--token got the flag-looking value "${token}" instead of a secret — write the secret as its own argument (e.g. \`tumwater gui --token s3cret --all-interfaces\`)`);
45
+ // asked for protection must never silently get none. The validation and its wording
46
+ // live in tokenValueProblem, shared with `start --gui` so the two callers cannot
47
+ // drift; its flag-looking-value case names `tumwater gui` here.
48
+ const problem = tokenValueProblem(args, "tumwater gui");
49
+ if (problem)
50
+ fail(problem);
51
+ const token = flagValue(args, "--token") ?? "";
57
52
  try {
58
53
  await start(root, port, allInterfaces, token);
59
54
  }