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
@@ -10,16 +10,16 @@
10
10
  import { setConfigKey, setDailyBudgetUsd, setRoleModelFields, unsetConfigKey } from "../config/config-write.js";
11
11
  import { checkDailyBudgetUsd, checkMinRunInterval } from "../config/config-keys.js";
12
12
  import { testNotifyHook } from "../cli/notify-command.js";
13
- import { rejectBadRole, requireConfigKey } from "./gui-args.js";
14
- import { baseRoleOf } from "../roles/loop-ids.js";
13
+ import { rejectBadRole, requireConfigKey, requireRoleField, requireStringField } from "./gui-args.js";
14
+ import { baseRoleOf } from "../roles/runner-ids.js";
15
15
  import { DIRECTOR_ROLE } from "../roles/roles.js";
16
16
  import { badRequest, sendJson } from "./http-body.js";
17
- import { gotSuffix } from "../text/text.js";
18
17
  import { readPostBody } from "./gui-endpoint-common.js";
19
18
  /** Handle POST /api/budget: the dashboard's budget-badge editor saves the daily cost cap —
20
19
  * same body discipline as /api/prompt (readJsonObject), and the same shared validation rule +
21
20
  * atomic setter the TUI's Ctrl+B uses, so both surfaces write tumwater.json identically and
22
- * the running orchestrator picks the change up on its next ~2 s poll. */
21
+ * the running orchestrator picks the change up on its next ~2 s poll. A cap already in place
22
+ * answers `unchanged: true` and writes nothing. */
23
23
  export async function handleBudget(req, res, root) {
24
24
  const body = await readPostBody(req, res, '{"maxDailyCostUsd": 25}');
25
25
  if (!body)
@@ -43,7 +43,7 @@ export async function handleBudget(req, res, root) {
43
43
  sendJson(res, 500, { error: result.error });
44
44
  return;
45
45
  }
46
- sendJson(res, 200, { ok: true, maxDailyCostUsd: value });
46
+ sendJson(res, 200, { ok: true, maxDailyCostUsd: value, ...unchangedFlag(result) });
47
47
  }
48
48
  /** Handle POST /api/role-cap: the dashboard role drawer's editor saves one role's daily cost
49
49
  * cap — the control the amber role-cap alert remedies. Same body discipline as /api/budget
@@ -52,18 +52,16 @@ export async function handleBudget(req, res, root) {
52
52
  * through the CLI's own setConfigKey dotted path so the per-key validator and whole-candidate
53
53
  * validateConfig (which rejects an unknown role id) own the final truth. The director has no
54
54
  * cap (role-view.ts's guard exempts it), so it is refused up front with wording that mirrors
55
- * handleRoleNotes. A bare `feature-2` loop id maps to its base role's cap (baseRoleOf), since
55
+ * handleRoleNotes. A bare `feature-2` role id maps to its base role's cap (baseRoleOf), since
56
56
  * the cap is about the role. A write failure is configKeyError's 400 exactly as handleConfigSet
57
- * reports it. */
57
+ * reports it. A no-op write answers `unchanged: true`. */
58
58
  export async function handleRoleCap(req, res, root) {
59
59
  const body = await readPostBody(req, res, '{"role": "feature", "maxDailyCostUsd": 2}');
60
60
  if (!body)
61
61
  return;
62
- const role = body.role;
63
- if (typeof role !== "string" || role.trim() === "") {
64
- badRequest(res, "role required");
62
+ const role = requireRoleField(res, body);
63
+ if (role === null)
65
64
  return;
66
- }
67
65
  if (role === DIRECTOR_ROLE) {
68
66
  badRequest(res, "the director has no daily cap");
69
67
  return;
@@ -86,7 +84,7 @@ export async function handleRoleCap(req, res, root) {
86
84
  const result = setConfigKey(root, "maxDailyCostUsdPerRole." + baseRole, JSON.stringify(value));
87
85
  if (!result.ok)
88
86
  return configKeyError(res, "maxDailyCostUsdPerRole." + baseRole, result);
89
- sendJson(res, 200, { ok: true, role: baseRole, maxDailyCostUsd: value });
87
+ sendJson(res, 200, { ok: true, role: baseRole, maxDailyCostUsd: value, ...unchangedFlag(result) });
90
88
  }
91
89
  /** Handle POST /api/role-interval: the role drawer's min run interval editor sets or clears
92
90
  * one role's override of the team-wide minRunIntervalSeconds — the knob that makes one role
@@ -95,19 +93,17 @@ export async function handleRoleCap(req, res, root) {
95
93
  * value clears the key through unsetConfigKey (so the role falls back to the team-wide
96
94
  * interval), otherwise the write rides the CLI's own setConfigKey dotted path so
97
95
  * checkMinRunInterval and whole-candidate validateConfig (which rejects an unknown role id)
98
- * own the final truth. The director is accepted here — it ticks on the same clock, unlike
99
- * the cap's director exemption. A bare `feature-2` loop id maps to its base role's override
96
+ * own the final truth. The director is accepted here — it runs on the same clock, unlike
97
+ * the cap's director exemption. A bare `feature-2` role id maps to its base role's override
100
98
  * (baseRoleOf), since the interval is about the role. A write failure is configKeyError's 400
101
- * exactly as handleRoleCap reports it. */
99
+ * exactly as handleRoleCap reports it. A no-op set or clear says so in its message. */
102
100
  export async function handleRoleInterval(req, res, root) {
103
101
  const body = await readPostBody(req, res, '{"role": "feature", "minRunIntervalSeconds": 300}');
104
102
  if (!body)
105
103
  return;
106
- const role = body.role;
107
- if (typeof role !== "string" || role.trim() === "") {
108
- badRequest(res, "role required");
104
+ const role = requireRoleField(res, body);
105
+ if (role === null)
109
106
  return;
110
- }
111
107
  const value = body.minRunIntervalSeconds;
112
108
  if (value === undefined) {
113
109
  badRequest(res, "minRunIntervalSeconds required");
@@ -126,7 +122,10 @@ export async function handleRoleInterval(req, res, root) {
126
122
  const clearResult = unsetConfigKey(root, `roles.${base}.minRunIntervalSeconds`);
127
123
  if (!clearResult.ok)
128
124
  return configKeyError(res, `roles.${base}.minRunIntervalSeconds`, clearResult);
129
- sendJson(res, 200, { ok: true, role: base, message: `${base}: min run interval cleared` });
125
+ const message = clearResult.oldValue === undefined
126
+ ? `${base}: min run interval is not set in tumwater.json, so nothing was cleared`
127
+ : `${base}: min run interval cleared`;
128
+ sendJson(res, 200, { ok: true, role: base, message });
130
129
  return;
131
130
  }
132
131
  // The shared rule (finite 0..MAX_DURATION_SECONDS), naming the per-role dotted key so a
@@ -141,10 +140,13 @@ export async function handleRoleInterval(req, res, root) {
141
140
  const result = setConfigKey(root, `roles.${base}.minRunIntervalSeconds`, JSON.stringify(value));
142
141
  if (!result.ok)
143
142
  return configKeyError(res, `roles.${base}.minRunIntervalSeconds`, result);
144
- sendJson(res, 200, { ok: true, role: base, minRunIntervalSeconds: value, message: `${base}: min run interval set to ${value}s` });
143
+ const message = result.unchanged
144
+ ? `${base}: min run interval is already ${value}s, so tumwater.json was not written`
145
+ : `${base}: min run interval set to ${value}s`;
146
+ sendJson(res, 200, { ok: true, role: base, minRunIntervalSeconds: value, message });
145
147
  }
146
148
  /** Handle POST /api/role-instances: the role drawer's instance-count editor sets or clears one
147
- * role's parallel-runner count — the count that lets `feature-2`…`feature-N` tick. Same body
149
+ * role's parallel-runner count — the count that lets `feature-2`…`feature-N` run. Same body
148
150
  * discipline as /api/role-interval (readPostBody → 400 malformed/non-object, 413 oversized); a
149
151
  * null, "" or whitespace-only value clears the key through unsetConfigKey (so the role falls
150
152
  * back to the default 1), otherwise the write rides the CLI's own setConfigKey dotted path so
@@ -152,18 +154,16 @@ export async function handleRoleInterval(req, res, root) {
152
154
  * (unlike handleRoleCap's director exemption): setConfigKey runs the candidate through
153
155
  * validateConfig, whose INSTANCE_COUNT rejects 0, 9, 1.5 and `abc` and whose INSTANCE_ROLES
154
156
  * branch rejects an instances key under any role but feature and bugfix (the director and
155
- * custom roles included). A bare `feature-2` loop id maps to its base role's count (baseRoleOf),
157
+ * custom roles included). A bare `feature-2` role id maps to its base role's count (baseRoleOf),
156
158
  * since the count is about the role. A write failure is configKeyError's 400 exactly as
157
- * handleRoleInterval reports it. */
159
+ * handleRoleInterval reports it. A no-op set or clear says so in its message. */
158
160
  export async function handleRoleInstances(req, res, root) {
159
161
  const body = await readPostBody(req, res, '{"role": "feature", "instances": 3}');
160
162
  if (!body)
161
163
  return;
162
- const role = body.role;
163
- if (typeof role !== "string" || role.trim() === "") {
164
- badRequest(res, "role required");
164
+ const role = requireRoleField(res, body);
165
+ if (role === null)
165
166
  return;
166
- }
167
167
  const value = body.instances;
168
168
  if (value === undefined) {
169
169
  badRequest(res, "instances required");
@@ -182,7 +182,10 @@ export async function handleRoleInstances(req, res, root) {
182
182
  const clearResult = unsetConfigKey(root, `roles.${base}.instances`);
183
183
  if (!clearResult.ok)
184
184
  return configKeyError(res, `roles.${base}.instances`, clearResult);
185
- sendJson(res, 200, { ok: true, role: base, message: `${base}: instances cleared` });
185
+ const message = clearResult.oldValue === undefined
186
+ ? `${base}: instances is not set in tumwater.json, so nothing was cleared`
187
+ : `${base}: instances cleared`;
188
+ sendJson(res, 200, { ok: true, role: base, message });
186
189
  return;
187
190
  }
188
191
  // setConfigKey takes the raw CLI text; a JSON round-trip of an already-parsed value is
@@ -191,7 +194,10 @@ export async function handleRoleInstances(req, res, root) {
191
194
  const result = setConfigKey(root, `roles.${base}.instances`, JSON.stringify(value));
192
195
  if (!result.ok)
193
196
  return configKeyError(res, `roles.${base}.instances`, result);
194
- sendJson(res, 200, { ok: true, role: base, instances: value, message: `${base}: instances set to ${value}` });
197
+ const message = result.unchanged
198
+ ? `${base}: instances is already ${value}, so tumwater.json was not written`
199
+ : `${base}: instances set to ${value}`;
200
+ sendJson(res, 200, { ok: true, role: base, instances: value, message });
195
201
  }
196
202
  /** Handle POST /api/role-model: the role drawer's Model overrides editor sets or clears one
197
203
  * role's provider/model/thinking triple (roles.<base>.provider/.model/.thinking) in one
@@ -200,31 +206,27 @@ export async function handleRoleInstances(req, res, root) {
200
206
  * already write it). Same body discipline as /api/role-interval (readPostBody → 400
201
207
  * malformed/non-object, 413 oversized); a missing/blank role is a 400 `role required`, each
202
208
  * of the three must be a string when present (400 naming the field otherwise), and at least
203
- * one must be present (400 `provider, model or thinking required`). A bare `feature-2` loop
209
+ * one must be present (400 `provider, model or thinking required`). A bare `feature-2` role
204
210
  * id maps to its base role's keys (baseRoleOf). The director is accepted — configForRole
205
211
  * resolves roles.director.* like any role's, matching the CLI's accepted triple for it.
206
212
  * Values are trimmed; `""` clears that field (never stored — checkStringField rejects an
207
213
  * empty string). The write rides setRoleModelFields, whose validateConfig names
208
214
  * roles.<base>.<field> on a bad value and refuses an unknown role, reported as a 400 exactly
209
- * as handleRoleInterval reports a write failure. */
215
+ * as handleRoleInterval reports a write failure. A no-op write says so in its message. */
210
216
  export async function handleRoleModel(req, res, root) {
211
- const body = await readPostBody(req, res, '{"role": "feature", "provider": "claude", "model": "claude-opus-5", "thinking": "high"}');
217
+ const body = await readPostBody(req, res, '{"role": "feature", "provider": "huggingface", "model": "claude-opus-5", "thinking": "high"}');
212
218
  if (!body)
213
219
  return;
214
- const role = body.role;
215
- if (typeof role !== "string" || role.trim() === "") {
216
- badRequest(res, "role required");
220
+ const role = requireRoleField(res, body);
221
+ if (role === null)
217
222
  return;
218
- }
219
223
  const fields = {};
220
224
  for (const key of ["provider", "model", "thinking"]) {
221
- const value = body[key];
222
- if (value === undefined)
225
+ if (body[key] === undefined)
223
226
  continue;
224
- if (typeof value !== "string") {
225
- badRequest(res, `"${key}" must be a string — a model override, or "" to clear it${gotSuffix(value)}`);
227
+ const value = requireStringField(res, body, key, 'a model override, or "" to clear it');
228
+ if (value === null)
226
229
  return;
227
- }
228
230
  fields[key] = value.trim();
229
231
  }
230
232
  if (!("provider" in fields) && !("model" in fields) && !("thinking" in fields)) {
@@ -235,12 +237,15 @@ export async function handleRoleModel(req, res, root) {
235
237
  const result = setRoleModelFields(root, base, fields);
236
238
  if (!result.ok)
237
239
  return badRequest(res, result.error);
238
- sendJson(res, 200, { ok: true, role: base, message: `${base}: model updated` });
240
+ const message = result.unchanged
241
+ ? `${base}: model overrides unchanged, so tumwater.json was not written`
242
+ : `${base}: model updated`;
243
+ sendJson(res, 200, { ok: true, role: base, message });
239
244
  }
240
245
  /** Handle POST /api/role-instructions: the role drawer's standing-instructions editor — the
241
246
  * `Instructions override` row's Edit/Save. Same body discipline as /api/role-cap (readPostBody
242
247
  * → 400 malformed/non-object, 413 oversized); an empty role id is a 400, and a bare `feature-2`
243
- * loop id maps to its base role's override (baseRoleOf), the key tick-prompt reads — so an
248
+ * role id maps to its base role's override (baseRoleOf), the key run-prompt reads — so an
244
249
  * instance edits the instructions its runs carry (readRoleBody's shared guard is not used
245
250
  * because it rejects instance ids, and the unknown-role check is validateConfig's, below).
246
251
  * The director is editable here (its override reaches its runs), unlike handleRoleNotes'
@@ -248,16 +253,17 @@ export async function handleRoleModel(req, res, root) {
248
253
  * CLI's --clear-instructions); otherwise the write goes through the CLI's own setConfigKey
249
254
  * with the same JSON.stringify wrapper --instructions-file uses, so validateConfig owns the
250
255
  * 4096-char cap and the unknown-role check, and a write failure is configKeyError's 400
251
- * naming the dotted key. */
256
+ * naming the dotted key. A no-op set or clear says so in its message. */
252
257
  export async function handleRoleInstructions(req, res, root) {
253
258
  const body = await readPostBody(req, res, '{"role": "feature", "text": "…"}');
254
259
  if (!body)
255
260
  return;
256
- const role = body.role;
257
- if (typeof role !== "string" || role.trim() === "")
258
- return badRequest(res, "role required");
259
- if (typeof body.text !== "string")
260
- return badRequest(res, `"text" must be a string — the full replacement instructions, or "" to clear it${gotSuffix(body.text)}`);
261
+ const role = requireRoleField(res, body);
262
+ if (role === null)
263
+ return;
264
+ const text = requireStringField(res, body, "text", `the full replacement instructions, or "" to clear it`);
265
+ if (text === null)
266
+ return;
261
267
  const base = baseRoleOf(role);
262
268
  // An empty or whitespace-only buffer clears the override, like the CLI's --clear-instructions;
263
269
  // otherwise the same JSON.stringify wrapper --instructions-file uses (setConfigKey takes raw
@@ -265,7 +271,7 @@ export async function handleRoleInstructions(req, res, root) {
265
271
  // 4096-char cap and the unknown-role check in writeConfigMutation. Unlike quiet hours the
266
272
  // clear arm carries no role guard (the director may clear, and the unknown-role clear gap is
267
273
  // pre-existing).
268
- writeRoleJsonKey(res, root, base, `roles.${base}.instructions`, "instructions", body.text, (v) => JSON.stringify(v), (v) => `${base}: instructions saved (${v.length} chars)`);
274
+ writeRoleJsonKey(res, root, base, `roles.${base}.instructions`, "instructions", text, (v) => JSON.stringify(v), (v) => `${base}: instructions saved (${v.length} chars)`);
269
275
  }
270
276
  /** The shared write behind POST /api/role-enable and POST /api/role-disable: the Settings
271
277
  * card's Enable button and the role drawer's two-click Disable both write
@@ -273,18 +279,19 @@ export async function handleRoleInstructions(req, res, root) {
273
279
  * yields the boolean checkBoolean expects), and validateConfig's unknown-role check refuses an
274
280
  * unknown id as configKeyError's 400 before anything is written. Same body discipline as
275
281
  * /api/role-cap (readPostBody → 400 malformed/non-object, 413 oversized); a missing,
276
- * non-string, or blank role is a 400 `role required`, and a bare `feature-2` loop id maps to
282
+ * non-string, or blank role is a 400 `role required`, and a bare `feature-2` role id maps to
277
283
  * its base role's key (baseRoleOf), so an instance disables/enables the role it belongs to.
278
- * The director cannot be disabled (with it disabled loopIds omits it and no loop dequeues the
284
+ * The director cannot be disabled (with it disabled runnerIds omits it and no role dequeues the
279
285
  * operator's inbox), so a disable is refused explicitly; enabling the director stays harmless
280
- * and mirrors the CLI's --enable. */
286
+ * and mirrors the CLI's --enable. A role already in the requested state answers `already
287
+ * enabled`/`already disabled` and writes nothing. */
281
288
  async function setRoleEnabled(req, res, root, enabled) {
282
289
  const body = await readPostBody(req, res, '{"role": "selfcheck"}');
283
290
  if (!body)
284
291
  return;
285
- const role = body.role;
286
- if (typeof role !== "string" || role.trim() === "")
287
- return badRequest(res, "role required");
292
+ const role = requireRoleField(res, body);
293
+ if (role === null)
294
+ return;
288
295
  const base = baseRoleOf(role);
289
296
  if (!enabled && base === DIRECTOR_ROLE)
290
297
  return badRequest(res, "the director cannot be disabled");
@@ -294,7 +301,8 @@ async function setRoleEnabled(req, res, root, enabled) {
294
301
  const result = setConfigKey(root, key, enabled ? "true" : "false");
295
302
  if (!result.ok)
296
303
  return configKeyError(res, key, result);
297
- sendJson(res, 200, { ok: true, role: base, message: `${base}: ${enabled ? "enabled" : "disabled"}` });
304
+ const state = enabled ? "enabled" : "disabled";
305
+ sendJson(res, 200, { ok: true, role: base, message: result.unchanged ? `${base}: already ${state}` : `${base}: ${state}` });
298
306
  }
299
307
  /** Handle POST /api/role-enable: the Settings view's "Disabled roles" card Enable button —
300
308
  * re-enables one disabled role (the GUI half of the CLI's `tumwater role <id> --enable`). Body
@@ -313,7 +321,7 @@ export async function handleRoleDisable(req, res, root) {
313
321
  * role's daily quiet-hours window — the control the `Quiet hours` row it already shows leads
314
322
  * to (the counterpart of the in-flight `tumwater role <id> --quiet-hours` CLI write). Same body
315
323
  * discipline as /api/role-cap (readPostBody → 400 malformed/non-object, 413 oversized); a
316
- * missing/blank role is a 400, and a bare `feature-2` loop id maps to its base role's key
324
+ * missing/blank role is a 400, and a bare `feature-2` role id maps to its base role's key
317
325
  * (baseRoleOf), so an instance edits the window its runs read. The director is refused up front:
318
326
  * `quietHoursPerRole.director` is inert (roleQuietHold and doctor both exempt the director). An
319
327
  * empty/whitespace-only window clears the key (unsetConfigKey, like the CLI's --clear-quiet-hours);
@@ -321,18 +329,20 @@ export async function handleRoleDisable(req, res, root) {
321
329
  * map path skips validateConfig — the guard the set branch gets free from validateConfig's
322
330
  * unknown-role refusal; otherwise the write goes through setConfigKey with the same
323
331
  * JSON.stringify wrapper, so checkQuietHours (naming quietHoursPerRole.<base>) and validateConfig
324
- * own the truth, and a write failure is configKeyError's 400 naming the dotted key. */
332
+ * own the truth, and a write failure is configKeyError's 400 naming the dotted key. A no-op set
333
+ * or clear says so in its message. */
325
334
  export async function handleRoleQuietHours(req, res, root) {
326
335
  const body = await readPostBody(req, res, '{"role": "feature", "window": "23:00-07:00"}');
327
336
  if (!body)
328
337
  return;
329
- const role = body.role;
330
- if (typeof role !== "string" || role.trim() === "")
331
- return badRequest(res, "role required");
338
+ const role = requireRoleField(res, body);
339
+ if (role === null)
340
+ return;
332
341
  if (role === DIRECTOR_ROLE)
333
342
  return badRequest(res, "the director has no quiet hours");
334
- if (typeof body.window !== "string")
335
- return badRequest(res, `"window" must be a string — a window like 23:00-07:00, or "" to clear it${gotSuffix(body.window)}`);
343
+ const window = requireStringField(res, body, "window", `a window like 23:00-07:00, or "" to clear it`);
344
+ if (window === null)
345
+ return;
336
346
  const base = baseRoleOf(role);
337
347
  // An empty or whitespace-only window clears the hold, like the CLI's --clear-quiet-hours;
338
348
  // the clear arm carries rejectBadRole because unsetConfigKey's no-op map path (a role the
@@ -340,7 +350,7 @@ export async function handleRoleQuietHours(req, res, root) {
340
350
  // branch's unknown-role refusal would never fire on a clear and a bogus id would answer 200
341
351
  // "cleared" (mirrors the CLI's knownRoleIds guard). The set arm runs setConfigKey raw, its
342
352
  // map branch checking the dotted key as the label and validateConfig owning the unknown role.
343
- writeRoleJsonKey(res, root, base, `quietHoursPerRole.${base}`, "quiet hours", body.window, (v) => JSON.stringify(v), (v) => `${base}: quiet hours set to ${v}`, () => rejectBadRole(root, res, role));
353
+ writeRoleJsonKey(res, root, base, `quietHoursPerRole.${base}`, "quiet hours", window, (v) => JSON.stringify(v), (v) => `${base}: quiet hours set to ${v}`, () => rejectBadRole(root, res, role));
344
354
  }
345
355
  /** The shared 400 for a failed config mutation — the set and unset write paths both nest the
346
356
  * write's error under the key it names, so the Settings view can tell which field failed and a
@@ -349,6 +359,12 @@ export async function handleRoleQuietHours(req, res, root) {
349
359
  function configKeyError(res, key, result) {
350
360
  badRequest(res, `${key}: ${result.error}`);
351
361
  }
362
+ /** The `unchanged: true` field a no-op config write adds to its 200 reply, or nothing when
363
+ * the write changed the file — spread into the body by the budget, role-cap, and config-set
364
+ * handlers. */
365
+ function unchangedFlag(result) {
366
+ return result.unchanged ? { unchanged: true } : {};
367
+ }
352
368
  /** The set-or-clear write the two per-role JSON-config-key handlers (quiet hours, standing
353
369
  * instructions) share: an empty/whitespace `raw` unsets `key` (after `guardClear`, a per-call
354
370
  * check the clear arm runs first — quiet hours' unknown-role guard; instructions has none),
@@ -363,15 +379,21 @@ function writeRoleJsonKey(res, root, base, key, noun, raw, serialize, setMessage
363
379
  const result = unsetConfigKey(root, key);
364
380
  if (!result.ok)
365
381
  configKeyError(res, key, result);
366
- else
367
- sendJson(res, 200, { ok: true, role: base, message: `${base}: ${noun} cleared` });
382
+ else {
383
+ const message = result.oldValue === undefined
384
+ ? `${base}: ${noun} is not set in tumwater.json, so nothing was cleared`
385
+ : `${base}: ${noun} cleared`;
386
+ sendJson(res, 200, { ok: true, role: base, message });
387
+ }
368
388
  return;
369
389
  }
370
390
  const result = setConfigKey(root, key, serialize(raw));
371
391
  if (!result.ok)
372
392
  configKeyError(res, key, result);
373
- else
374
- sendJson(res, 200, { ok: true, role: base, message: setMessage(raw) });
393
+ else {
394
+ const message = result.unchanged ? `${base}: ${noun} unchanged, so tumwater.json was not written` : setMessage(raw);
395
+ sendJson(res, 200, { ok: true, role: base, message });
396
+ }
375
397
  }
376
398
  /** Handle POST /api/config-set: the Settings view's per-key Save — one curated key set to
377
399
  * one JSON value, through setConfigKey so the browser cannot drift from the CLI's rules
@@ -382,7 +404,7 @@ function writeRoleJsonKey(res, root, base, key, noun, raw, serialize, setMessage
382
404
  * JSON.stringify before setConfigKey parses it back, so `set model gpt-5`'s
383
405
  * JSON-or-literal rule is bypassed harmlessly: the browser already sent parsed JSON.
384
406
  * Same body discipline as /api/budget (readPostBody → 400 malformed/non-object, 413
385
- * oversized). */
407
+ * oversized). A value already in place answers `unchanged: true`. */
386
408
  export async function handleConfigSet(req, res, root) {
387
409
  const body = await readPostBody(req, res, '{"key": "model", "value": "gpt-5"}');
388
410
  if (!body)
@@ -399,7 +421,7 @@ export async function handleConfigSet(req, res, root) {
399
421
  const result = setConfigKey(root, key, JSON.stringify(body.value));
400
422
  if (!result.ok)
401
423
  return configKeyError(res, key, result);
402
- sendJson(res, 200, { ok: true, key, value: result.value, oldValue: result.oldValue });
424
+ sendJson(res, 200, { ok: true, key, value: result.value, oldValue: result.oldValue, ...unchangedFlag(result) });
403
425
  }
404
426
  /** Handle POST /api/config-unset: the Settings view's per-key Clear — one curated key removed,
405
427
  * through unsetConfigKey so the browser cannot drift from `tumwater config unset`'s rules
@@ -1,8 +1,9 @@
1
1
  /**
2
2
  * The dashboard's POST operator endpoint namespace (src/gui/gui-server.ts routes to these
3
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
4
+ * gathered here. The GET data endpoints are barrelled into gui/gui-endpoints.ts from one module
5
+ * per family (gui-history-endpoints, gui-backlog-endpoints, gui-report-endpoints,
6
+ * gui-admin-endpoints); the shared request-body helpers
6
7
  * (readPostBody, readRoleBody, sendRequestResult) live in gui/gui-endpoint-common.ts. The
7
8
  * per-domain command modules, each a thin HTTP adapter touching no socket beyond its own `res`:
8
9
  * prompt queueing/cancel/edit (gui/gui-prompt-commands), backlog entry file/cancel/edit +
@@ -15,4 +16,4 @@ export { handlePrompt, handlePromptRole, handlePromptCancel, handlePromptEdit, h
15
16
  export { handleQuestionAnswer, handleBacklogFile, handleBacklogCancel, handleBacklogRelease, handleBacklogTop, handleBacklogEdit } from "./gui-backlog-commands.js";
16
17
  export { handleConfigSet, handleConfigUnset, handleBudget, handleRoleCap, handleRoleEnable, handleRoleDisable, handleRoleInstances, handleRoleInterval, handleRoleInstructions, handleRoleModel, handleRoleQuietHours, handleNotifyTest } from "./gui-config-commands.js";
17
18
  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";
19
+ export { handleResetCounters, handleRestart, handleReclaim, handleKillOrphans, handleDoctorReprobe, handleAbort, handleStopSchedule, handleRoleNotes, handleRetire, handleRevert, handleQueueCancel, handleBriefEdit } from "./gui-operator-commands.js";
@@ -25,10 +25,10 @@ export async function readPostBody(req, res, example) {
25
25
  * wake/abort/prompt-role/pause-role family states the body-discipline-and-role-check pairing
26
26
  * once instead of two guard lines per handler. Its null return means the response is already
27
27
  * written and the handler must stop; callers keep their own `if (!body) return` bail. Exactly
28
- * seven call sites — handlePromptRole, handleWake, handleWakeCancel and handleResetCounters
29
- * (allowMissing, the {} → all-roles default), handleAbort, handlePauseRole, and
30
- * handleRoleNotes; handlePromptCancel deliberately does not ride it, since its role is
31
- * optional and validates only when present. */
28
+ * eight call sites — handlePromptRole, handleWake, handleWakeCancel and handleResetCounters
29
+ * (allowMissing, the {} → all-roles default), handleAbort, handlePauseRole,
30
+ * handleRoleNotes, and handleRetire; handlePromptCancel deliberately does not ride it, since
31
+ * its role is optional and validates only when present. */
32
32
  export async function readRoleBody(req, res, example, root, allowMissing = false) {
33
33
  const body = await readPostBody(req, res, example);
34
34
  if (body === null)