tumwater 0.2.0 → 0.4.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 (464) 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 +55 -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 +6 -4
  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 +404 -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 +281 -55
  41. package/dist/src/backlog/backlog-cli-view.js +255 -0
  42. package/dist/src/backlog/backlog-cli.js +91 -335
  43. package/dist/src/backlog/backlog-commit.js +20 -13
  44. package/dist/src/backlog/backlog-eligibility.js +193 -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 +264 -0
  48. package/dist/src/backlog/backlog-release.js +134 -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-guardrails.js +136 -0
  56. package/dist/src/baseline/main-red.js +153 -36
  57. package/dist/src/baseline/main-watch.js +232 -0
  58. package/dist/src/brief/brief-edit.js +12 -12
  59. package/dist/src/brief/brief.js +31 -51
  60. package/dist/src/budget/budget-fallback-view.js +288 -0
  61. package/dist/src/budget/budget-wiring.js +158 -0
  62. package/dist/src/budget/budget.js +31 -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 +65 -3
  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 +181 -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 +183 -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 +291 -598
  82. package/dist/src/cli/cli-repo-gate.js +11 -1
  83. package/dist/src/cli/cli-review-commands.js +113 -0
  84. package/dist/src/cli/cli-role-command.js +525 -0
  85. package/dist/src/cli/cli-start-summary.js +167 -0
  86. package/dist/src/cli/{cli-run.js → cli-start.js} +193 -237
  87. package/dist/src/cli/completion.js +169 -22
  88. package/dist/src/cli/config-commands.js +58 -26
  89. package/dist/src/cli/follow-view.js +75 -0
  90. package/dist/src/cli/help.js +258 -82
  91. package/dist/src/cli/log-commands.js +164 -85
  92. package/dist/src/cli/question-commands.js +211 -10
  93. package/dist/src/cli.js +173 -45
  94. package/dist/src/collections.js +56 -13
  95. package/dist/src/concurrency/check-permit.js +20 -3
  96. package/dist/src/concurrency/lock.js +17 -13
  97. package/dist/src/concurrency/provider-lease.js +44 -8
  98. package/dist/src/concurrency/semaphore.js +102 -26
  99. package/dist/src/config/config-aliases.js +0 -1
  100. package/dist/src/config/config-editable-keys.js +16 -1
  101. package/dist/src/config/config-fallbacks.js +94 -56
  102. package/dist/src/config/config-field-checks.js +40 -10
  103. package/dist/src/config/config-keys.js +30 -8
  104. package/dist/src/config/config-live.js +63 -24
  105. package/dist/src/config/config-request.js +137 -0
  106. package/dist/src/config/config-schema.js +31 -24
  107. package/dist/src/config/config-selector-checks.js +123 -0
  108. package/dist/src/config/config-validation-agents.js +80 -0
  109. package/dist/src/config/config-validation-model.js +116 -0
  110. package/dist/src/config/config-validation.js +169 -222
  111. package/dist/src/config/config-views.js +136 -54
  112. package/dist/src/config/config-write-core.js +135 -0
  113. package/dist/src/config/config-write.js +164 -265
  114. package/dist/src/config/config.js +53 -38
  115. package/dist/src/config/model-selector.js +34 -34
  116. package/dist/src/doctor/doctor-backlog-bugs.js +330 -0
  117. package/dist/src/doctor/doctor-backlog-plans.js +398 -0
  118. package/dist/src/doctor/doctor-backlog.js +249 -512
  119. package/dist/src/doctor/doctor-check-names.js +85 -0
  120. package/dist/src/doctor/doctor-checks.js +61 -593
  121. package/dist/src/doctor/doctor-claude-checks.js +142 -0
  122. package/dist/src/doctor/doctor-codex-checks.js +276 -0
  123. package/dist/src/doctor/doctor-contention-checks.js +506 -0
  124. package/dist/src/doctor/doctor-fallback-checks.js +130 -0
  125. package/dist/src/doctor/doctor-holds.js +80 -66
  126. package/dist/src/doctor/doctor-install.js +2 -1
  127. package/dist/src/doctor/doctor-model-checks.js +216 -537
  128. package/dist/src/doctor/doctor-notify.js +9 -5
  129. package/dist/src/doctor/doctor-opencode-checks.js +164 -0
  130. package/dist/src/doctor/doctor-orphans.js +182 -63
  131. package/dist/src/doctor/doctor-pi-checks.js +62 -0
  132. package/dist/src/doctor/doctor-project-checks.js +397 -0
  133. package/dist/src/doctor/doctor.js +126 -77
  134. package/dist/src/doctor/image-probe.js +21 -8
  135. package/dist/src/doctor/outcome.js +3 -2
  136. package/dist/src/doctor/probe.js +21 -12
  137. package/dist/src/events/event-format.js +185 -89
  138. package/dist/src/events/event-labels.js +11 -5
  139. package/dist/src/events/event-legacy.js +39 -15
  140. package/dist/src/events/event-read.js +52 -39
  141. package/dist/src/events/event-tally.js +16 -0
  142. package/dist/src/events/event-window.js +80 -26
  143. package/dist/src/events/events.js +30 -13
  144. package/dist/src/events/notify.js +7 -7
  145. package/dist/src/failure/failure-cluster.js +12 -12
  146. package/dist/src/failure/failure-data.js +65 -60
  147. package/dist/src/failure/failure-render.js +29 -21
  148. package/dist/src/failure/failure-state-change.js +17 -12
  149. package/dist/src/failure/time-spend.js +35 -35
  150. package/dist/src/files/file-queue.js +17 -0
  151. package/dist/src/files/files.js +111 -35
  152. package/dist/src/files/json-files.js +3 -3
  153. package/dist/src/files/json-object.js +28 -7
  154. package/dist/src/files/stat-cache.js +2 -2
  155. package/dist/src/files/tail.js +1 -1
  156. package/dist/src/gates/bootstrap-gates.js +4 -5
  157. package/dist/src/gates/budget-gates.js +88 -225
  158. package/dist/src/gates/disk-gate.js +1 -1
  159. package/dist/src/gates/gate-polls.js +101 -143
  160. package/dist/src/gates/maintenance-quota.js +37 -30
  161. package/dist/src/gates/pause-gates.js +28 -10
  162. package/dist/src/gates/readiness.js +7 -3
  163. package/dist/src/gates/role-cap-gates.js +8 -8
  164. package/dist/src/gates/startup-gate.js +94 -32
  165. package/dist/src/gates/streak-gate.js +54 -30
  166. package/dist/src/git/commit-message.js +89 -46
  167. package/dist/src/git/git-diff.js +16 -10
  168. package/dist/src/git/git-run.js +79 -38
  169. package/dist/src/git/git.js +54 -34
  170. package/dist/src/git/slot-salvage.js +253 -0
  171. package/dist/src/git/slots-state.js +6 -6
  172. package/dist/src/git/worktree-pool.js +132 -38
  173. package/dist/src/git/worktree-use.js +4 -11
  174. package/dist/src/git/worktree.js +94 -37
  175. package/dist/src/gui/gui-admin-endpoints.js +96 -0
  176. package/dist/src/gui/gui-args.js +79 -8
  177. package/dist/src/gui/gui-backlog-commands.js +43 -19
  178. package/dist/src/gui/gui-backlog-endpoints.js +156 -0
  179. package/dist/src/gui/gui-command.js +26 -13
  180. package/dist/src/gui/gui-config-commands.js +168 -80
  181. package/dist/src/gui/gui-endpoint-commands.js +5 -4
  182. package/dist/src/gui/gui-endpoint-common.js +4 -4
  183. package/dist/src/gui/gui-endpoints.js +11 -380
  184. package/dist/src/gui/gui-history-endpoints.js +301 -0
  185. package/dist/src/gui/gui-operator-commands.js +235 -13
  186. package/dist/src/gui/gui-prompt-commands.js +26 -20
  187. package/dist/src/gui/gui-report-endpoints.js +97 -0
  188. package/dist/src/gui/gui-server.js +26 -5
  189. package/dist/src/gui/gui-team-commands.js +7 -7
  190. package/dist/src/gui/http-body.js +1 -1
  191. package/dist/src/history/history-data.js +122 -234
  192. package/dist/src/history/history-queued.js +155 -0
  193. package/dist/src/history/history-render.js +42 -22
  194. package/dist/src/history/history.js +190 -49
  195. package/dist/src/inbox/inbox-attachments.js +29 -30
  196. package/dist/src/inbox/inbox-cancel.js +10 -11
  197. package/dist/src/inbox/inbox-edit.js +9 -9
  198. package/dist/src/inbox/inbox-submit.js +18 -18
  199. package/dist/src/inbox/inbox-top.js +28 -17
  200. package/dist/src/inbox/inbox.js +42 -43
  201. package/dist/src/inbox/pending-prompt.js +26 -26
  202. package/dist/src/inbox/prompt-commands.js +62 -49
  203. package/dist/src/inbox/prompt-not-before.js +2 -2
  204. package/dist/src/init/init-templates.js +1 -2
  205. package/dist/src/init/init.js +28 -23
  206. package/dist/src/merge/backlog-conflicts.js +215 -35
  207. package/dist/src/merge/merge-batch.js +14 -8
  208. package/dist/src/merge/merge-cancel.js +53 -0
  209. package/dist/src/merge/merge-check-failures.js +44 -22
  210. package/dist/src/merge/merge-core.js +58 -32
  211. package/dist/src/merge/merge-diff.js +5 -4
  212. package/dist/src/merge/merge-drain.js +45 -19
  213. package/dist/src/merge/merge-git.js +177 -38
  214. package/dist/src/merge/merge-pipeline.js +15 -3
  215. package/dist/src/merge/merge-post-review.js +214 -0
  216. package/dist/src/merge/merge-queue-render.js +48 -11
  217. package/dist/src/merge/merge-queue.js +52 -15
  218. package/dist/src/merge/merge-requeue.js +103 -0
  219. package/dist/src/merge/merge-slot.js +61 -16
  220. package/dist/src/merge/merge-stack.js +27 -11
  221. package/dist/src/merge/merge-to-main.js +77 -26
  222. package/dist/src/merge/merge-top.js +83 -0
  223. package/dist/src/merge/merge-verify.js +75 -18
  224. package/dist/src/notes-cli.js +35 -15
  225. package/dist/src/operator/operator-commands.js +192 -81
  226. package/dist/src/operator/operator-intent.js +69 -42
  227. package/dist/src/operator/operator-requests.js +35 -40
  228. package/dist/src/operator/retire.js +5 -6
  229. package/dist/src/operator/revert.js +142 -34
  230. package/dist/src/orchestrator/orchestrator-launch.js +60 -52
  231. package/dist/src/orchestrator/orchestrator-poll.js +352 -0
  232. package/dist/src/orchestrator/orchestrator-scheduling.js +71 -370
  233. package/dist/src/orchestrator/orchestrator.js +129 -369
  234. package/dist/src/orchestrator/retention.js +42 -8
  235. package/dist/src/orchestrator/scheduling-admit.js +116 -0
  236. package/dist/src/orchestrator/scheduling-scope.js +245 -0
  237. package/dist/src/orchestrator/scheduling-skip.js +195 -0
  238. package/dist/src/orchestrator/startup-migration-steps.js +8 -7
  239. package/dist/src/orchestrator/startup-migrations.js +11 -7
  240. package/dist/src/paths.js +126 -49
  241. package/dist/src/pi/command-shape.js +12 -1
  242. package/dist/src/pi/pi-args.js +6 -4
  243. package/dist/src/pi/pi-backend.js +6 -6
  244. package/dist/src/pi/pi-bin.js +54 -63
  245. package/dist/src/pi/pi-listed-models.js +53 -0
  246. package/dist/src/pi/pi-models.js +63 -30
  247. package/dist/src/pi/pi-run-result.js +1 -1
  248. package/dist/src/pi/pi-settings.js +1 -1
  249. package/dist/src/pi/pi-stream.js +6 -6
  250. package/dist/src/pi/pi-watchdogs.js +18 -19
  251. package/dist/src/pi-extension/bounded-output.js +5 -3
  252. package/dist/src/pi-extension/context-budget.js +2 -2
  253. package/dist/src/pi-extension/context-shake.js +1 -1
  254. package/dist/src/pi-extension/context-window.js +0 -1
  255. package/dist/src/pi-extension/full-output.js +3 -10
  256. package/dist/src/pi-extension/strip-thinking.js +0 -1
  257. package/dist/src/pi-extension/trim-system-prompt.js +3 -4
  258. package/dist/src/pi-extension/write-guard.js +37 -0
  259. package/dist/src/process/launch-services.js +1 -2
  260. package/dist/src/process/node-bin.js +16 -0
  261. package/dist/src/process/process-group.js +8 -18
  262. package/dist/src/process/process.js +41 -2
  263. package/dist/src/process/run-marker.js +26 -6
  264. package/dist/src/process/supervisor.js +22 -21
  265. package/dist/src/prompt/dialect.js +17 -2
  266. package/dist/src/prompt/gate-notes.js +203 -0
  267. package/dist/src/prompt/gate-prompts.js +150 -217
  268. package/dist/src/prompt/notes-backlog.js +184 -0
  269. package/dist/src/prompt/principles.js +1 -2
  270. package/dist/src/prompt/prompt-followup.js +19 -14
  271. package/dist/src/prompt/prompt.js +131 -204
  272. package/dist/src/redeploy/redeploy-policy.js +24 -19
  273. package/dist/src/redeploy/redeploy-probes.js +1 -1
  274. package/dist/src/redeploy/redeploy.js +2 -3
  275. package/dist/src/redeploy/redeployer.js +20 -20
  276. package/dist/src/redeploy/self-reload.js +2 -1
  277. package/dist/src/report/report-data.js +188 -124
  278. package/dist/src/report/report-fold.js +130 -54
  279. package/dist/src/report/report-main-red.js +55 -0
  280. package/dist/src/report/report-render.js +136 -65
  281. package/dist/src/report/report-types.js +1 -0
  282. package/dist/src/report/report.js +23 -9
  283. package/dist/src/request-timeouts.js +2 -2
  284. package/dist/src/review/diff-rules.js +53 -0
  285. package/dist/src/review/known-flakes.js +112 -1
  286. package/dist/src/review/post-merge-review-accept.js +51 -0
  287. package/dist/src/review/post-merge-review-render.js +112 -0
  288. package/dist/src/review/post-merge-review.js +239 -0
  289. package/dist/src/review/protected-edits.js +24 -10
  290. package/dist/src/review/quarantine-render.js +65 -0
  291. package/dist/src/review/review-followup.js +8 -8
  292. package/dist/src/review/review-precheck.js +66 -23
  293. package/dist/src/review/review-run.js +395 -0
  294. package/dist/src/review/review-verdict.js +2 -2
  295. package/dist/src/review/review.js +180 -401
  296. package/dist/src/review/secret-leaks.js +36 -16
  297. package/dist/src/review/suite-rerun.js +0 -1
  298. package/dist/src/roles/legacy-role-migration.js +80 -7
  299. package/dist/src/roles/role-catalog.js +127 -12
  300. package/dist/src/roles/role-guidance.js +65 -20
  301. package/dist/src/roles/role-labels.js +7 -5
  302. package/dist/src/roles/role-render.js +108 -56
  303. package/dist/src/roles/role-view.js +95 -56
  304. package/dist/src/roles/roles.js +33 -33
  305. package/dist/src/roles/runner-ids.js +97 -0
  306. package/dist/src/run/acceptance-greps.js +119 -0
  307. package/dist/src/run/ci-parity.js +121 -0
  308. package/dist/src/run/feedback-filings.js +51 -0
  309. package/dist/src/run/qa-coverage.js +125 -0
  310. package/dist/src/{tick/tick-apply.js → run/run-apply.js} +95 -94
  311. package/dist/src/run/run-detail-data.js +175 -0
  312. package/dist/src/run/run-detail-render.js +91 -0
  313. package/dist/src/run/run-detail.js +168 -0
  314. package/dist/src/{tick/tick-finalize.js → run/run-finalize.js} +73 -60
  315. package/dist/src/{tick/tick-outcome.js → run/run-outcome.js} +6 -7
  316. package/dist/src/{tick/tick-prompt.js → run/run-prompt.js} +122 -73
  317. package/dist/src/{tick/tick-resume.js → run/run-resume.js} +55 -26
  318. package/dist/src/{tick/tick-stage.js → run/run-stage.js} +170 -83
  319. package/dist/src/{tick/tick-timing.js → run/run-timing.js} +43 -41
  320. package/dist/src/{tick/tick-usage.js → run/run-usage.js} +45 -37
  321. package/dist/src/{tick/tick-verdict.js → run/run-verdict.js} +105 -57
  322. package/dist/src/run/selfcheck-digest.js +40 -0
  323. package/dist/src/run/stage-backlog-checks.js +249 -0
  324. package/dist/src/run/stage-check.js +178 -0
  325. package/dist/src/run/stage-checks-shared.js +65 -0
  326. package/dist/src/run/stage-name-checks.js +204 -0
  327. package/dist/src/run/stage-path-checks.js +231 -0
  328. package/dist/src/run/stage-prose-checks.js +261 -0
  329. package/dist/src/runner/leftover.js +216 -0
  330. package/dist/src/{loop → runner}/model-fallback.js +12 -12
  331. package/dist/src/runner/revision.js +191 -0
  332. package/dist/src/{loop/loop-fallback.js → runner/runner-fallback.js} +8 -8
  333. package/dist/src/runner/runner-outcome.js +109 -0
  334. package/dist/src/{loop/loop-pi.js → runner/runner-pi.js} +140 -131
  335. package/dist/src/{loop/loop-recovery.js → runner/runner-recovery.js} +39 -18
  336. package/dist/src/{loop/loop-tick.js → runner/runner-run.js} +236 -245
  337. package/dist/src/{loop/loop-state.js → runner/runner-state-load.js} +61 -114
  338. package/dist/src/runner/runner-state.js +103 -0
  339. package/dist/src/{loop/loop.js → runner/runner.js} +123 -120
  340. package/dist/src/scheduling/backoff.js +38 -38
  341. package/dist/src/scheduling/claim-entries.js +49 -0
  342. package/dist/src/scheduling/claims.js +35 -22
  343. package/dist/src/scheduling/once-round.js +22 -22
  344. package/dist/src/scheduling/quiet-hours.js +5 -6
  345. package/dist/src/scheduling/scheduling.js +74 -64
  346. package/dist/src/scheduling/work-merged-cache.js +2 -2
  347. package/dist/src/status/status-budget.js +55 -47
  348. package/dist/src/status/status-data.js +49 -17
  349. package/dist/src/status/status-inbox.js +2 -2
  350. package/dist/src/status/status-install.js +0 -1
  351. package/dist/src/status/status-polls.js +43 -28
  352. package/dist/src/status/status-roles.js +50 -13
  353. package/dist/src/team/error-storm.js +8 -8
  354. package/dist/src/team/failure-spread.js +2 -2
  355. package/dist/src/team/orchestrator-info.js +15 -9
  356. package/dist/src/team/reclaim.js +7 -8
  357. package/dist/src/team/team-hold.js +7 -7
  358. package/dist/src/team/team-polls.js +91 -50
  359. package/dist/src/team/team-state.js +8 -2
  360. package/dist/src/text/datetime.js +65 -18
  361. package/dist/src/text/format.js +48 -11
  362. package/dist/src/text/markdown.js +8 -5
  363. package/dist/src/text/phrases.js +60 -23
  364. package/dist/src/text/suggest.js +1 -1
  365. package/dist/src/text/text.js +78 -13
  366. package/dist/src/ui/badges.js +99 -38
  367. package/dist/src/ui/gui/gui-client-backlog.js +314 -0
  368. package/dist/src/ui/gui/gui-client-boot.js +89 -61
  369. package/dist/src/ui/gui/gui-client-composer.js +8 -9
  370. package/dist/src/ui/gui/gui-client-drawer-config.js +254 -0
  371. package/dist/src/ui/gui/gui-client-drawer.js +155 -289
  372. package/dist/src/ui/gui/gui-client-feed.js +271 -0
  373. package/dist/src/ui/gui/gui-client-history.js +437 -68
  374. package/dist/src/ui/gui/gui-client-labels.js +6 -3
  375. package/dist/src/ui/gui/gui-client-markdown.js +1 -2
  376. package/dist/src/ui/gui/gui-client-model.js +27 -36
  377. package/dist/src/ui/gui/gui-client-operator.js +60 -52
  378. package/dist/src/ui/gui/gui-client-pending.js +405 -24
  379. package/dist/src/ui/gui/gui-client-report-chart.js +492 -0
  380. package/dist/src/ui/gui/gui-client-report.js +105 -430
  381. package/dist/src/ui/gui/{gui-client-loops.js → gui-client-roles.js} +43 -44
  382. package/dist/src/ui/gui/gui-client-settings.js +163 -5
  383. package/dist/src/ui/gui/gui-client-team.js +36 -465
  384. package/dist/src/ui/gui/gui-client.js +38 -26
  385. package/dist/src/ui/gui/gui-icons.js +0 -2
  386. package/dist/src/ui/gui/gui-page.js +10 -7
  387. package/dist/src/ui/gui/gui-styles.js +38 -34
  388. package/dist/src/ui/progress-data.js +9 -11
  389. package/dist/src/ui/{tick-progress-model.js → run-progress-model.js} +33 -32
  390. package/dist/src/ui/status-model.js +133 -125
  391. package/dist/src/ui/status-payload.js +364 -49
  392. package/dist/src/ui/status-render.js +109 -82
  393. package/dist/src/ui/team-alerts.js +728 -62
  394. package/dist/src/ui/tone.js +12 -6
  395. package/dist/src/ui/transcript-run.js +58 -0
  396. package/dist/src/ui/transcript-tail.js +28 -20
  397. package/dist/src/ui/transcript.js +63 -23
  398. package/dist/src/ui/tui/tui-app.js +0 -1
  399. package/dist/src/ui/tui/tui-backlog.js +53 -14
  400. package/dist/src/ui/tui/tui-editor-answer.js +27 -31
  401. package/dist/src/ui/tui/tui-editor-attach.js +4 -12
  402. package/dist/src/ui/tui/tui-editor-backends.js +127 -0
  403. package/dist/src/ui/tui/tui-editor-brief.js +14 -36
  404. package/dist/src/ui/tui/tui-editor-budget.js +36 -39
  405. package/dist/src/ui/tui/tui-editor-check.js +29 -0
  406. package/dist/src/ui/tui/tui-editor-checks.js +28 -0
  407. package/dist/src/ui/tui/tui-editor-claude-reserve.js +24 -0
  408. package/dist/src/ui/tui/tui-editor-concurrency.js +25 -0
  409. package/dist/src/ui/tui/tui-editor-config.js +99 -0
  410. package/dist/src/ui/tui/tui-editor-entry.js +39 -64
  411. package/dist/src/ui/tui/tui-editor-fallback.js +31 -0
  412. package/dist/src/ui/tui/tui-editor-instances.js +46 -0
  413. package/dist/src/ui/tui/tui-editor-instructions.js +117 -0
  414. package/dist/src/ui/tui/tui-editor-interval.js +29 -16
  415. package/dist/src/ui/tui/tui-editor-model.js +29 -0
  416. package/dist/src/ui/tui/tui-editor-notes.js +9 -29
  417. package/dist/src/ui/tui/tui-editor-notify.js +25 -0
  418. package/dist/src/ui/tui/tui-editor-pause.js +48 -59
  419. package/dist/src/ui/tui/tui-editor-prompt.js +11 -16
  420. package/dist/src/ui/tui/tui-editor-provider-caps.js +27 -0
  421. package/dist/src/ui/tui/tui-editor-queued.js +5 -9
  422. package/dist/src/ui/tui/tui-editor-quiet.js +95 -41
  423. package/dist/src/ui/tui/tui-editor-retire.js +5 -13
  424. package/dist/src/ui/tui/tui-editor-role-enabled.js +126 -0
  425. package/dist/src/ui/tui/tui-editor-role-key.js +71 -0
  426. package/dist/src/ui/tui/tui-editor-role-model.js +28 -0
  427. package/dist/src/ui/tui/tui-editor-search.js +45 -33
  428. package/dist/src/ui/tui/tui-editor-session.js +101 -34
  429. package/dist/src/ui/tui/tui-editor-slots.js +24 -0
  430. package/dist/src/ui/tui/tui-editors.js +44 -2
  431. package/dist/src/ui/tui/tui-frame.js +68 -11
  432. package/dist/src/ui/tui/tui-history.js +8 -9
  433. package/dist/src/ui/tui/tui-input.js +13 -11
  434. package/dist/src/ui/tui/tui-keymap.js +0 -1
  435. package/dist/src/ui/tui/tui-keys.js +33 -9
  436. package/dist/src/ui/tui/tui-pane-activity.js +35 -19
  437. package/dist/src/ui/tui/tui-pane-backlog.js +198 -126
  438. package/dist/src/ui/tui/tui-pane-brief.js +0 -1
  439. package/dist/src/ui/tui/tui-pane-ctx.js +2 -2
  440. package/dist/src/ui/tui/tui-pane-health.js +168 -8
  441. package/dist/src/ui/tui/tui-pane-history.js +446 -82
  442. package/dist/src/ui/tui/tui-pane-pending.js +539 -15
  443. package/dist/src/ui/tui/tui-pane-queued.js +78 -57
  444. package/dist/src/ui/tui/tui-pane-report.js +54 -16
  445. package/dist/src/ui/tui/tui-pane-session.js +34 -6
  446. package/dist/src/ui/tui/tui-pane-transcript.js +111 -37
  447. package/dist/src/ui/tui/tui-pane.js +91 -28
  448. package/dist/src/ui/tui/tui-panes.js +153 -34
  449. package/dist/src/ui/tui/tui.js +18 -15
  450. package/dist/src/verdict/fix-claim.js +129 -33
  451. package/dist/src/verdict/no-change.js +4 -4
  452. package/dist/src/verdict/refusal.js +40 -16
  453. package/dist/src/verdict/reply-contract.js +77 -31
  454. package/dist/src/version.js +0 -1
  455. package/package.json +2 -1
  456. package/dist/src/loop/leftover.js +0 -130
  457. package/dist/src/loop/revision.js +0 -70
  458. package/dist/src/roles/loop-ids.js +0 -77
  459. package/dist/src/tick/qa-coverage.js +0 -108
  460. package/dist/src/tick/stage-check.js +0 -419
  461. package/dist/src/tick/telemetry-digest.js +0 -25
  462. package/dist/src/tick/tick-detail-data.js +0 -108
  463. package/dist/src/tick/tick-detail.js +0 -163
  464. package/dist/src/ui/gui/gui-client-sound.js +0 -53
@@ -1,128 +1,69 @@
1
- // vocabulary: plain
2
- /** The read-only query commands' bodies (status, diff, backlog, questions, role), split out of
1
+ /** The read-only query commands' bodies (status, diff, queue, backlog), split out of
3
2
  * cli.ts's dispatcher: main() routes to a cmd* here exactly as it routes to operator-commands's
4
3
  * cmdAbort or config-commands's cmdConfig, and cli.ts keeps only routing and the help/version
5
4
  * surfaces. Each command owns its argument gate, ready-repo gating decision, and output shape,
6
5
  * with the wording and gating order moved verbatim from main()'s cases. */
7
- import { fail, say, sayJson, sayJsonOrRender, warnIfUncommitted } from "./cli-output.js";
8
- import { flagValue, parseCountFlag, parsePositionFlag, parseRoleFlag, rejectRepeatFlag } from "./cli-args.js";
9
- import { JSON_FLAG, READY_FLAG, rejectEqualsForm, rejectUnknownArgs, ROLE_FLAG, STAT_FLAG } from "./cli-flag-specs.js";
10
- import { requireReadyRepo } from "./cli-repo-gate.js";
11
- import { defaultConfig, knownRoleIds, knownRoleIdsCached, loadConfigSafe } from "../config/config.js";
12
- import { gotSuffix, parsePositiveInt } from "../text/text.js";
13
- import { snapshot } from "../status/status-data.js";
6
+ import { fail, say, sayJson, sayJsonOrRender } from "./cli-output.js";
7
+ import { cancelQueuedMerge } from "../merge/merge-cancel.js";
8
+ import { moveQueuedMergeToTop } from "../merge/merge-top.js";
9
+ import { followView } from "./follow-view.js";
10
+ import { flagValue, parseGrepFlag, parseRoleListScope, rejectRepeatFlag, scanPositionArgs } from "./cli-args.js";
11
+ import { DIFF_PATH_FLAG, FOLLOW_FLAG, hasFollowFlag, JSON_FLAG, READY_FLAG, grepFlagSpec, rejectUnknownArgs, OVERLAPS_FLAG, ROLE_FLAG, STAT_FLAG } from "./cli-flag-specs.js";
12
+ import { refuseTeamWorkspace, requireReadyRepo } from "./cli-repo-gate.js";
13
+ import { scopeSnapshotToRoles, snapshot } from "../status/status-data.js";
14
14
  import { statusPayload } from "../ui/status-payload.js";
15
15
  import { renderStatus } from "../ui/status-render.js";
16
16
  import { backlogPayload } from "../backlog/backlog.js";
17
17
  import { backlogHolds, backlogInProgress } from "../backlog/backlog-eligibility.js";
18
- import { readyBacklog, renderBacklogMarkdown } from "../backlog/backlog-render.js";
19
- import { collectTeamChanges, collectRoleChange } from "../change/change-data.js";
20
- import { renderTeamChange, renderRoleChange } from "../change/change-render.js";
18
+ import { readyBacklog, grepBacklog, renderBacklogMarkdown } from "../backlog/backlog-render.js";
19
+ import { loadQuarantine } from "../review/known-flakes.js";
20
+ import { quarantineRows, renderQuarantine } from "../review/quarantine-render.js";
21
+ import { collectChangeOverlaps, scopeChangeOverlaps, scopeChangeOverlapsToPath, collectTeamChanges, collectRoleChange, collectRoleChanges, normalizeDiffPath } from "../change/change-data.js";
22
+ import { renderChangeOverlaps, renderTeamChange, renderRoleChange, renderRoleChanges } from "../change/change-render.js";
21
23
  import { readMergeQueue } from "../merge/merge-queue.js";
22
- import { renderMergeQueue } from "../merge/merge-queue-render.js";
24
+ import { renderMergeQueue, scopeMergeQueue } from "../merge/merge-queue-render.js";
25
+ import { mergeChanges } from "../merge/merge-slot.js";
26
+ import { shortSha } from "../text/format.js";
27
+ import { roleLabel } from "../roles/role-labels.js";
23
28
  import { orchestratorAlive } from "../team/orchestrator-info.js";
24
29
  import { queueInFlight } from "../status/status-polls.js";
25
- import { rolePayload } from "../roles/role-view.js";
26
- import { renderRoleMarkdown } from "../roles/role-render.js";
27
- import { DIRECTOR_ROLE, unknownRoleMessage } from "../roles/roles.js";
28
- import { saveRoleNote } from "../agent/role-notes-write.js";
29
- import { roleNotesPath } from "../paths.js";
30
- import { errorMessage } from "../text/text.js";
31
- import { readPromptFileArg, promptFileValue } from "./cli-command-args.js";
32
- import { commitOperatorBacklogEdit } from "../backlog/backlog-commit.js";
33
- import { stampedSubject } from "../git/commit-message.js";
34
- import { wakeQuestionAsker } from "../operator/operator-intent.js";
35
- import { answerEditorBuffer, answerQuestion, answerQuestionFromText, decisionFromEditorBuffer, sayAnswered, sayQuestionList } from "./question-commands.js";
36
- import { answeredQuestionAt, answeredQuestionCount, answeredQuestionList, answeredQuestionPayload, openQuestionAt, questionListPayload, renderShownQuestion } from "../backlog/question-answer.js";
37
- import { editInEditor } from "./cli-editor.js";
38
- import { canonicalRoleId } from "../roles/legacy-role-ids.js";
39
- import { baseRoleOf } from "../roles/loop-ids.js";
40
- import { setConfigKey, unsetConfigKey } from "../config/config-write.js";
41
- /** `questions answer`'s `--file <path>` flag: the decision comes from the file, lines kept. */
42
- const FILE_SPEC = { names: ["--file"], value: true, valueName: "<path>" };
43
- /** `role`'s notebook-write flags: clear the note, or replace it from a file (`-` for stdin). */
44
- const CLEAR_NOTES_SPEC = { names: ["--clear-notes"] };
45
- const NOTES_FILE_SPEC = { names: ["--notes-file"], value: true, valueName: "<path>" };
46
- /** `role`'s instructions-override flags: clear it, or replace it from a file (`-` for stdin). */
47
- const CLEAR_INSTRUCTIONS_SPEC = { names: ["--clear-instructions"] };
48
- const INSTRUCTIONS_FILE_SPEC = { names: ["--instructions-file"], value: true, valueName: "<path>" };
49
- /** `role`'s quiet-hours write flags: clear the window, or set it to a daily local-time window. */
50
- const CLEAR_QUIET_HOURS_SPEC = { names: ["--clear-quiet-hours"] };
51
- const QUIET_HOURS_SPEC = { names: ["--quiet-hours"], value: true, valueName: "<window>" };
52
- /** `role`'s daily-cap write flags: clear the cap, or set it to a USD amount (0 disables). */
53
- const CLEAR_CAP_SPEC = { names: ["--clear-cap"] };
54
- const CAP_SPEC = { names: ["--cap"], value: true, valueName: "<usd>" };
55
- /** `role`'s min-run-interval write flags: clear it, or set it to a second count (0 is
56
- * back-to-back). */
57
- const CLEAR_MIN_RUN_INTERVAL_SPEC = { names: ["--clear-min-run-interval"] };
58
- const MIN_RUN_INTERVAL_SPEC = { names: ["--min-run-interval"], value: true, valueName: "<seconds>" };
59
- /** `role`'s instances write flags: clear the count, or set it to a parallel-runner count
60
- * (an integer from 1 to 8; only feature and bugfix split). */
61
- const CLEAR_INSTANCES_SPEC = { names: ["--clear-instances"] };
62
- const INSTANCES_SPEC = { names: ["--instances"], value: true, valueName: "<n>" };
63
- /** `role`'s enabled-state write flags: enable or disable the role, written to
64
- * roles.<base>.enabled. */
65
- const ENABLE_SPEC = { names: ["--enable"] };
66
- const DISABLE_SPEC = { names: ["--disable"] };
67
- /** `role`'s model-triple write flags: clear the field, or set it, written to
68
- * roles.<base>.provider/.model/.thinking. model and provider take an id; thinking takes a
69
- * pi `--thinking` level (off|minimal|low|medium|high|xhigh|max). */
70
- const CLEAR_PROVIDER_SPEC = { names: ["--clear-provider"] };
71
- const PROVIDER_SPEC = { names: ["--provider"], value: true, valueName: "<id>" };
72
- const CLEAR_MODEL_SPEC = { names: ["--clear-model"] };
73
- const MODEL_SPEC = { names: ["--model"], value: true, valueName: "<id>" };
74
- const CLEAR_THINKING_SPEC = { names: ["--clear-thinking"] };
75
- const THINKING_SPEC = { names: ["--thinking"], value: true, valueName: "<level>" };
76
- /** Peel `tumwater role <id>`'s positional id off the argument list: the first token that is
77
- * neither a flag nor the value of a --role, --notes-file, --instructions-file,
78
- * --quiet-hours, --cap, --min-run-interval, --instances, --provider, --model or
79
- * --thinking flag. Returns the id (null when absent) and the remaining arguments — flags
80
- * only, ready for rejectUnknownArgs and sayJsonOrRender. The id may also arrive as
81
- * --role <id>, the flag spelling every other role-targeting command shares; the
82
- * collector's caller rejects both spellings at once rather than silently picking one.
83
- * Lives here because it exists only for this one
84
- * command's mixed positional/flag vocabulary. */
85
- function peelRolePositional(args) {
86
- const valueIdx = new Set();
87
- for (const flag of ["--role", "--notes-file", "--instructions-file", "--quiet-hours", "--cap", "--min-run-interval", "--instances", "--provider", "--model", "--thinking"]) {
88
- const at = args.indexOf(flag);
89
- if (at >= 0)
90
- valueIdx.add(at + 1);
91
- }
92
- let id = null;
93
- const rest = [];
94
- for (let i = 0; i < args.length; i++) {
95
- const arg = args[i] ?? ""; // Unreachable fallback: the loop bound guarantees a token here.
96
- // A non-flag token that is not a value-taking flag's value is the positional,
97
- // taken once; everything else — flags and their values alike — rides in rest for
98
- // rejectUnknownArgs and flagValue to claim.
99
- if (!arg.startsWith("-") && id === null && !valueIdx.has(i)) {
100
- id = arg;
101
- continue;
102
- }
103
- rest.push(arg);
104
- }
105
- return { id, rest };
106
- }
107
- /** `tumwater status`: the team snapshot as rendered text or as the JSON document GET
108
- * /api/status serves. */
109
- export async function cmdStatus(root, args) {
110
- rejectUnknownArgs("status", args, [JSON_FLAG]);
30
+ import { roleInScope } from "../roles/runner-ids.js";
31
+ /** `tumwater status [-f/--follow]`: the team snapshot as rendered text or as the JSON
32
+ * document GET /api/status serves. `-f/--follow` repaints the table every couple of seconds
33
+ * until Ctrl+C or the process that started it exits, so a backgrounded follow does not
34
+ * outlive its parent. */
35
+ export async function cmdStatus(root, args, seams = {}) {
36
+ rejectUnknownArgs("status", args, [ROLE_FLAG, JSON_FLAG, FOLLOW_FLAG]);
37
+ const follow = hasFollowFlag(args);
38
+ if (follow && args.includes("--json"))
39
+ fail("status --json prints one document; -f/--follow repaints a live view — drop one of them");
111
40
  await requireReadyRepo(root);
41
+ const scope = parseRoleListScope(root, args);
42
+ if (follow) {
43
+ await followStatus(root, scope, seams);
44
+ return;
45
+ }
112
46
  if (args.includes("--json")) {
113
47
  // Machine-readable team state — the document GET /api/status serves minus the
114
48
  // serving process's own `serverBuildSha`, printed with no server. A query, not a
115
49
  // health verdict: exit 0 on any successful read and let scripts interpret fields
116
50
  // themselves ("running": false is data, not failure).
117
- sayJson(statusPayload(root));
51
+ sayJson(statusPayload(root, Date.now(), scope));
118
52
  }
119
53
  else {
120
- say(renderStatus(root, snapshot(root), process.stdout.isTTY ? process.stdout.columns : undefined));
54
+ const snap = snapshot(root);
55
+ say(renderStatus(root, scope ? scopeSnapshotToRoles(snap, scope) : snap, process.stdout.isTTY ? process.stdout.columns : undefined, scope !== null));
121
56
  }
122
57
  }
123
- /** `tumwater diff`: one line per loop holding pending work, or the full per-role view with
124
- * `--role <id>`. */
125
- export async function cmdDiff(root, args) {
58
+ /** `tumwater status -f`'s live loop: the team snapshot repainted until the parent exits. */
59
+ async function followStatus(root, scope, seams) {
60
+ const tty = seams.tty ?? process.stdout.isTTY;
61
+ await followView(() => renderStatus(root, scope ? scopeSnapshotToRoles(snapshot(root), scope) : snapshot(root), tty ? process.stdout.columns : undefined, scope !== null), seams);
62
+ }
63
+ /** `tumwater diff`: one line per role holding pending work, the files two or more roles touch
64
+ * with `--overlaps` (`--role` keeps those a listed role holds, `--path` those at or under it),
65
+ * or the full per-role view of each role listed with `--role <id>[,<id>...]`. */
66
+ export async function cmdDiff(root, args, seams = {}) {
126
67
  // The repo half of requireReadyRepo still gates: a directory tumwater cannot read yet
127
68
  // (no git, not a repository, no tumwater.json, no commits) has no team to ask about,
128
69
  // and the change view's own degradation would misreport that as "main branch <name>
@@ -130,529 +71,281 @@ export async function cmdDiff(root, args) {
130
71
  // command. Past the gate an absent worktree still degrades to a `no worktree for
131
72
  // <role>` line (exit 0), so the command answers in any initialized directory —
132
73
  // report's rationale.
133
- rejectUnknownArgs("diff", args, [ROLE_FLAG, JSON_FLAG, STAT_FLAG]);
74
+ rejectUnknownArgs("diff", args, [ROLE_FLAG, JSON_FLAG, STAT_FLAG, OVERLAPS_FLAG, DIFF_PATH_FLAG, FOLLOW_FLAG]);
134
75
  await requireReadyRepo(root);
135
- // Absent --role is the team-wide form: one line per loop holding pending work
136
- // (parseRoleFlag returns null only for an absent flag — an empty or unknown value
137
- // already failed above). A named role keeps the full per-role view.
138
- const role = parseRoleFlag(args, knownRoleIdsCached(root));
139
- if (role === null) {
76
+ rejectRepeatFlag(args.filter((a) => a === "--path").length > 1, "--path");
77
+ const follow = hasFollowFlag(args);
78
+ if (follow && args.includes("--json"))
79
+ fail("diff --json prints one document; -f/--follow repaints a live view — drop one of them");
80
+ if (args.includes("--overlaps")) {
140
81
  if (args.includes("--stat"))
141
- fail("diff --stat needs --role <id> (the team roster prints no patch)");
142
- sayJsonOrRender(args, await collectTeamChanges(root), renderTeamChange);
143
- }
144
- else {
145
- const change = await collectRoleChange(root, role, { stat: args.includes("--stat") });
146
- sayJsonOrRender(args, change, renderRoleChange);
147
- }
148
- }
149
- /** `tumwater queue`: the durable merge queue in execution order, as text or as the
150
- * `{entries, unreadable}` document. */
151
- export async function cmdQueue(root, args) {
152
- // The repo half of requireReadyRepo gates: like every repo-bound query, a directory
153
- // tumwater cannot read yet has no queue to show. Past the gate an absent queue dir reads
154
- // as an empty queue (a `merge queue empty` line, exit 0), so no merge ever queued is
155
- // data, not an error.
156
- rejectUnknownArgs("queue", args, [JSON_FLAG]);
157
- await requireReadyRepo(root);
158
- // The JSON branch prints the reader's own document — raw sha and numeric enqueuedAt, not
159
- // the render's short sha and age — and the render is the collector's view with one clock
160
- // read, so `tumwater queue` and `tumwater status --json`'s mergeQueue cannot drift.
161
- // The in-flight marker rides along only while an orchestrator runs and the marker names a
162
- // queued entry (queueInFlight) — status --json's mergeQueue.inFlight, so the two agree.
163
- const view = readMergeQueue(root);
164
- const inFlight = queueInFlight(root, view.entries, orchestratorAlive(root));
165
- sayJsonOrRender(args, inFlight ? { ...view, inFlight } : view, () => renderMergeQueue(view, Date.now(), inFlight));
166
- }
167
- /** `tumwater backlog`: the planned/open entries as Markdown or as the GUI's JSON payload. */
168
- export async function cmdBacklog(root, args) {
169
- // No requireReadyRepo gate: the entry readers degrade to [] on a missing file, so the
170
- // command prints three empty sections in any directory (report's rationale, not config's).
171
- rejectUnknownArgs("backlog", args, [JSON_FLAG, READY_FLAG]);
172
- // Machine-readable backlog — the three entry arrays the Markdown view renders and the
173
- // GUI's /api/backlog serves (status --json's "print the endpoint's payload" pattern),
174
- // plus `holds.plans`/`holds.bugs` parallel to the entry arrays so a skipped entry's
175
- // reason is data, not inference: a pretty-printed JSON document in every exit-0 case,
176
- // never prose. The payload is a thunk, so whichever branch runs reads the three entry
177
- // files through their stat caches once — the Markdown renderer consumes the same arrays
178
- // the JSON document prints.
179
- // --ready filters both branches through readyBacklog; the Markdown branch adds a count of
180
- // the held entries it hid.
181
- const ready = args.includes("--ready");
182
- let hidden = 0;
183
- sayJsonOrRender(args, () => {
184
- const view = { ...backlogPayload(root), holds: backlogHolds(root), inProgress: backlogInProgress(root) };
185
- if (!ready)
186
- return view;
187
- const kept = readyBacklog(view);
188
- hidden = view.plans.length + view.bugs.length - kept.plans.length - kept.bugs.length;
189
- return kept;
190
- }, (v) => renderBacklogMarkdown(v) + (hidden > 0
191
- ? `\n\n_(${hidden} held entries hidden; run \`tumwater backlog\` to see them)_` : ""));
192
- }
193
- /** `tumwater questions`: list open questions, or answer one with `answer <n> <decision>`. */
194
- export async function cmdQuestions(root, args) {
195
- // No requireReadyRepo gate, like backlog: the reader degrades to an empty list on a
196
- // missing QUESTIONS.md, so the command inspects any directory instead of refusing.
197
- // The answer form's decision is free-form prose, so peelPositionals cannot route the
198
- // tokens: a decision word may begin with a single dash (`questions answer 1 "-50% spend
199
- // cap"`), and peelPositionals would hand it to the flag gate as an unknown flag — the
200
- // same masking parsePromptArgs avoids for prompt text by owning only `--`-prefixed
201
- // tokens. So the questions parse scans args itself: a `--json` token before the
202
- // question's number positional is the one flag (repeats refused, equals form named by
203
- // rejectEqualsForm), any other `--`-prefixed token before it is refused as unknown, and
204
- // every remaining token in order is prose — subcommand, position number, then the
205
- // decision words. Past the number positional, tokens are decision words even when one
206
- // is spelled `--json`: unquoted decision prose (`questions answer 1 keep --json output`)
207
- // reaches the command as separate argv tokens, and a flag scan that matched `--json`
208
- // anywhere silently ate the token out of the recorded decision and flipped the command
209
- // into JSON mode. Unknown `--`-prefixed tokens stay refused even there, so a misspelled
210
- // flag is still an error rather than silent decision text.
211
- const words = [];
212
- let jsonFlag = false;
213
- let numbered = false;
214
- let file;
215
- let answeredFlag = false;
216
- let limit;
217
- let grep;
218
- let show;
219
- for (let i = 0; i < args.length; i++) {
220
- const arg = args[i];
221
- if (!numbered && arg === "--show") {
222
- rejectRepeatFlag(show !== undefined, "--show");
223
- show = parsePositionFlag("--show", args[++i], "--show 2");
224
- continue;
225
- }
226
- if (!numbered && arg === "--answered") {
227
- rejectRepeatFlag(answeredFlag, "--answered");
228
- answeredFlag = true;
229
- continue;
230
- }
231
- if (!numbered && arg === "-n") {
232
- rejectRepeatFlag(limit !== undefined, "-n");
233
- limit = parseCountFlag("-n", args[++i]);
234
- continue;
235
- }
236
- if (!numbered && arg === "--grep") {
237
- rejectRepeatFlag(grep !== undefined, "--grep");
238
- const text = args[++i];
239
- if (text === undefined || text === "")
240
- fail("--grep needs text (usage: tumwater questions --answered --grep <text>)");
241
- grep = text;
242
- continue;
243
- }
244
- if (arg === "--file") {
245
- rejectRepeatFlag(file !== undefined, "--file");
246
- file = promptFileValue(args[++i]);
247
- continue;
248
- }
249
- if (!numbered && arg === "--json") {
250
- rejectRepeatFlag(jsonFlag, "--json");
251
- jsonFlag = true;
252
- continue;
253
- }
254
- if (arg.startsWith("--") && arg !== "--json") {
255
- rejectEqualsForm(arg, [JSON_FLAG, FILE_SPEC, { names: ["--show"], value: true, valueName: "<n>" }, { names: ["--grep"], value: true, valueName: "<text>" }]);
256
- fail(`unknown argument: ${arg} (valid flags for tumwater questions: --json, --file, --answered, --show, --grep)`);
257
- }
258
- if (!numbered) {
259
- // The position is a plain-decimal positive integer (parsePositiveInt's rule, the same
260
- // one --cancel's position honors): bare Number() admitted hex ("0x2"), exponent
261
- // ("1e2"), and whitespace-padded spellings as a valid question number, answering a
262
- // question the operator never named.
263
- if (parsePositiveInt(arg) !== null)
264
- numbered = true;
265
- }
266
- words.push(arg);
267
- }
268
- if (limit !== undefined && !answeredFlag)
269
- fail("-n needs --answered (it limits the answered questions listed)");
270
- if (grep !== undefined && !answeredFlag)
271
- fail("--grep needs --answered (it searches the answered questions)");
272
- if (show !== undefined) {
273
- if (grep !== undefined)
274
- fail("--grep does not apply to --show");
275
- if (words.length > 0)
276
- fail("--show prints one open question; it takes no subcommand");
277
- if (file !== undefined)
278
- fail("--file only applies to questions answer <n>");
279
- if (answeredFlag) {
280
- if (limit !== undefined)
281
- fail("-n does not apply to --show");
282
- const a = answeredQuestionAt(root, show);
283
- if (a === null) {
284
- const count = answeredQuestionCount(root);
285
- const range = count > 0 ? ` — valid: 1-${count}` : "";
286
- fail(`no answered question at position ${show} (${count} in QUESTIONS.md)${range}`);
287
- }
288
- if (jsonFlag)
289
- sayJson(a);
290
- else
291
- say(renderShownQuestion(a));
82
+ fail("diff --overlaps prints paths, not patches; drop --stat");
83
+ // Overlaps are computed across every role; --role only filters which entries print.
84
+ const scope = parseRoleListScope(root, args);
85
+ const path = diffPathArg(args);
86
+ // The read and its zero-arg render are factored into one thunk so the follow's frame
87
+ // and the snapshot print exactly the same text from the same reader.
88
+ const readOverlaps = async () => {
89
+ let view = await collectChangeOverlaps(root);
90
+ if (scope === null && path === undefined)
91
+ return { view, render: () => renderChangeOverlaps(view) };
92
+ if (scope !== null)
93
+ view = scopeChangeOverlaps(view, scope);
94
+ if (path !== undefined)
95
+ view = scopeChangeOverlapsToPath(view, path);
96
+ return { view, render: () => renderChangeOverlaps(view, scope ?? undefined, path) };
97
+ };
98
+ if (follow) {
99
+ await followView(async () => (await readOverlaps()).render(), seams);
292
100
  return;
293
101
  }
294
- const q = openQuestionAt(root, show);
295
- if (q === null) {
296
- const count = questionListPayload(root).questions.length;
297
- const range = count > 0 ? ` — valid: 1-${count}` : "";
298
- fail(`no open question at position ${show} (${count} in QUESTIONS.md)${range}`);
299
- }
300
- if (jsonFlag)
301
- sayJson(q);
302
- else
303
- say(renderShownQuestion(q));
102
+ const { view, render } = await readOverlaps();
103
+ sayJsonOrRender(args, view, () => render());
304
104
  return;
305
105
  }
306
- if (answeredFlag) {
307
- if (words.length > 0)
308
- fail("--answered lists answered questions; it takes no subcommand");
309
- if (file !== undefined)
310
- fail("--file only applies to questions answer <n>");
311
- if (jsonFlag)
312
- sayJson(answeredQuestionPayload(root, limit ?? 10, grep));
313
- else
314
- say(answeredQuestionList(root, limit ?? 10, grep));
106
+ // Absent --role is the team-wide form: one line per role holding pending work
107
+ // (parseRoleListScope returns null only for an absent flag — an empty or unknown piece
108
+ // already failed above). One named role keeps the single per-role view; a list prints each
109
+ // listed role's view in order.
110
+ const roles = parseRoleListScope(root, args);
111
+ const path = roles === null ? undefined : diffPathArg(args);
112
+ // The roster/role reads below are factored into a thunk (view + zero-arg render) so the
113
+ // follow's frame and the snapshot share one reader and print byte-identical text.
114
+ const readChange = async () => {
115
+ if (roles === null) {
116
+ if (args.includes("--stat"))
117
+ fail("diff --stat needs --role <id> (the team roster prints no patch)");
118
+ if (args.includes("--path"))
119
+ fail("diff --path needs --role <id> (the team roster prints no patch)");
120
+ const view = await collectTeamChanges(root);
121
+ return { view, render: () => renderTeamChange(view) };
122
+ }
123
+ if (roles.length === 1) {
124
+ const change = await collectRoleChange(root, roles[0], { stat: args.includes("--stat"), path });
125
+ return { view: change, render: () => renderRoleChange(change) };
126
+ }
127
+ const view = await collectRoleChanges(root, roles, { stat: args.includes("--stat"), path });
128
+ return { view, render: () => renderRoleChanges(view) };
129
+ };
130
+ if (follow) {
131
+ await followView(async () => (await readChange()).render(), seams);
315
132
  return;
316
133
  }
317
- if (words.length === 0 && file !== undefined)
318
- fail("--file only applies to questions answer <n>");
319
- if (words.length === 0) {
320
- sayQuestionList(root, jsonFlag);
134
+ const { view, render } = await readChange();
135
+ sayJsonOrRender(args, view, () => render());
136
+ }
137
+ /** The normalised `--path` value of `diff --role` (undefined when absent): leading `./` pieces
138
+ * and trailing `/` characters stripped, failing on an empty, absolute or `..`-bearing result. */
139
+ function diffPathArg(args) {
140
+ const raw = flagValue(args, "--path");
141
+ if (raw === null || raw === undefined)
142
+ return undefined;
143
+ const norm = normalizeDiffPath(raw);
144
+ if (norm === null)
145
+ fail("diff --path takes a repo-relative path such as src/cli.ts (as diff --overlaps prints it)");
146
+ return norm;
147
+ }
148
+ /** `tumwater queue [-f/--follow]`: the durable merge queue in execution order, as text or as
149
+ * the `{entries, unreadable}` document. `-f/--follow` repaints the queue every couple of
150
+ * seconds until Ctrl+C or the process that started it exits, so a backgrounded follow does
151
+ * not outlive its parent (the same rule `status -f` uses). */
152
+ export async function cmdQueue(root, args, seams = {}) {
153
+ // `tumwater queue --cancel <n>` is the read-only command's one destructive mode: route it
154
+ // to its own branch before the read gate, so --cancel never leaks into the view parsing.
155
+ if (args.includes("--cancel")) {
156
+ await cmdQueueCancel(root, args);
321
157
  return;
322
158
  }
323
- if (words[0] !== "answer")
324
- fail(`unknown questions subcommand: ${words[0]} (use "answer <n> <decision>", or run \`tumwater questions\` with no subcommand to list them)`);
325
- // The offending token carries the same ` (got "...")` suffix every sibling number parser
326
- // adds (parsePositiveInt's flag callers, --cancel); the bare `answer` form names none.
327
- const raw = words.length > 1 ? words[1] : undefined;
328
- const n = raw === undefined ? null : parsePositiveInt(raw);
329
- if (n === null)
330
- fail(`questions answer needs a positive question number: questions answer <n> "<decision>"${raw === undefined ? "" : gotSuffix(raw)}`);
331
- if (file !== undefined) {
332
- if (words.length > 2)
333
- fail(`unexpected argument ${JSON.stringify(words[2])} — with --file the decision comes from the file`);
334
- const answered = answerQuestionFromText(root, n, readPromptFileArg(file));
335
- await commitAndReportAnswer(root, n, jsonFlag, answered.title, answered.decision);
159
+ if (args.includes("--top")) {
160
+ await cmdQueueTop(root, args);
336
161
  return;
337
162
  }
338
- const decision = words.slice(2).join(" ").trim();
339
- if (words.length === 2) {
340
- const open = questionListPayload(root).questions;
341
- const question = open[n - 1];
342
- if (question === undefined) {
343
- const range = open.length > 0 ? ` — valid: 1-${open.length}` : "";
344
- fail(`no question at position ${n} (${open.length} open)${range}`);
345
- }
346
- const edited = decisionFromEditorBuffer(editInEditor(answerEditorBuffer(question.title, question.body), `answer-${n}.md`, "pass a decision or --file <path>"));
347
- if (edited.trim() === "")
348
- fail("questions answer needs a decision: the editor left it blank; nothing changed");
349
- const answered = answerQuestionFromText(root, n, edited, question.title);
350
- await commitAndReportAnswer(root, n, jsonFlag, answered.title, answered.decision);
163
+ // The repo half of requireReadyRepo gates: like every repo-bound query, a directory
164
+ // tumwater cannot read yet has no queue to show. Past the gate an absent queue dir reads
165
+ // as an empty queue (a `merge queue empty` line, exit 0), so no merge ever queued is
166
+ // data, not an error.
167
+ rejectUnknownArgs("queue", args, [ROLE_FLAG, JSON_FLAG, FOLLOW_FLAG]);
168
+ await requireReadyRepo(root);
169
+ const roles = parseRoleListScope(root, args);
170
+ const follow = hasFollowFlag(args);
171
+ if (follow && args.includes("--json"))
172
+ fail("queue --json prints one document; -f/--follow repaints a live view — drop one of them");
173
+ // Scoped read shared by the follow and the one-shot branches, so the render's age and the
174
+ // in-flight marker share one clock read and each frame re-reads the queue. --role keeps
175
+ // the listed roles' entries (a base id taking its instances too) under their whole-queue
176
+ // positions; the in-flight marker is judged against the kept entries only, and
177
+ // `unreadable` stays whole because an unreadable file has no role to attribute it to.
178
+ const readQueue = () => {
179
+ const whole = readMergeQueue(root);
180
+ const scoped = roles ? scopeMergeQueue(whole, roles, (r) => roleInScope(r, roles)) : undefined;
181
+ const view = scoped ? scoped.view : whole;
182
+ return { view, inFlight: queueInFlight(root, view.entries, orchestratorAlive(root)), scope: scoped?.scope };
183
+ };
184
+ if (follow) {
185
+ await followView(() => {
186
+ const { view, inFlight, scope } = readQueue();
187
+ return renderMergeQueue(view, Date.now(), inFlight, scope);
188
+ }, seams);
351
189
  return;
352
190
  }
353
- if (decision === "")
354
- fail('questions answer needs a decision: questions answer <n> "<decision>"');
355
- // answerQuestion collapses the free-form decision to one line before writing it; report
356
- // that written text in the --json payload so it matches the file, not the caller's spacing.
357
- const { title, decision: writtenDecision } = answerQuestion(root, n, decision);
358
- await commitAndReportAnswer(root, n, jsonFlag, title, writtenDecision);
359
- }
360
- /** Commit the answered question, report it in the requested output mode, and wake the
361
- * asking role in prose mode — the tail every `questions answer` branch ends with. */
362
- async function commitAndReportAnswer(root, n, jsonFlag, title, decision) {
363
- warnIfUncommitted(await commitOperatorBacklogEdit(root, "QUESTIONS.md", stampedSubject("operator", `Answer question: ${title}`)), "QUESTIONS.md"); // vocabulary-ok: commit prefix stays
364
- sayAnswered(n, title, jsonFlag, decision);
365
- if (!jsonFlag)
366
- sayAskerWake(root, title);
191
+ const { view, inFlight, scope } = readQueue();
192
+ sayJsonOrRender(args, inFlight ? { ...view, inFlight } : view, () => renderMergeQueue(view, Date.now(), inFlight, scope));
367
193
  }
368
- /** Prose-mode follow-up to `questions answer`: wake the asking role and print the confirmation. */
369
- function sayAskerWake(root, title) {
370
- const wake = wakeQuestionAsker(root, title);
371
- if (wake !== null)
372
- say(wake);
373
- }
374
- /** Refuse an unknown role id with the shared unknownRoleMessage wording before a per-role
375
- * config write resolves the id. Every `tumwater role` write branch starts with the same
376
- * prelude — load the config (falling back to defaultConfig so built-in roles resolve) and
377
- * throw the unknown-role error unless the id appears — so this keeps that check in one place.
378
- * The caller then derives `baseRoleOf(id)` for the write key. */
379
- function requireKnownRole(root, id) {
380
- const cfg = loadConfigSafe(root).config ?? defaultConfig();
381
- if (!knownRoleIds(cfg).includes(id))
382
- throw new Error(unknownRoleMessage(id, knownRoleIds(cfg)));
383
- }
384
- /** The config-key write the six per-role hold branches (quiet hours, daily cap, min run
385
- * interval, provider, model and thinking) share: with `clear`, or an empty typed value, unset
386
- * `key` under `base`; else set it. `serialize` shapes the value `setConfigKey` stores (it
387
- * JSON-encodes nothing itself) and `display` gives the human phrase for the confirmation
388
- * line. The caller returns right after. The instructions branch keeps its own block: its
389
- * value is a file's text, so an empty value is a legitimate write, not a clear. */
390
- function writeRoleConfigKey(root, base, clear, raw, key, noun, serialize, display) {
391
- if (clear || raw.trim() === "") {
392
- const result = unsetConfigKey(root, key);
393
- if (!result.ok)
394
- fail(result.error);
395
- say(`${base}: ${noun} cleared`);
194
+ /** The `tumwater queue --cancel <n>` branch (PLANS.md): remove the Nth queued merge `tumwater
195
+ * queue` lists and delete its role's merge pin, so next-run leftover recovery cannot silently
196
+ * re-enqueue the change. Parsed as cancelAndAnnounce parses `--cancel` (backlog-cli.ts):
197
+ * `--cancel <n>` the one valued flag, `--json` the one boolean (at most once), `--help`/-h
198
+ * tolerated, anything else refused. --role and -f/--follow are refused by name — a live view
199
+ * repaints a read, and position numbers come from the whole queue, whose numbering is
200
+ * `tumwater queue`'s. An entry a live merge marker names for this sha is refused (wait for it
201
+ * to land, then revert); an out-of-range position fails naming the current depth. */
202
+ async function cmdQueueCancel(root, args) {
203
+ const { json, position, rest } = scanPositionArgs(args, "--cancel", "--cancel 2");
204
+ // The vocabulary a cancel invocation accepts is exactly --cancel and --json, both consumed
205
+ // above; --role and -f/--follow are refused with the queue-specific wording the tests pin.
206
+ if (rest.includes("--role"))
207
+ fail("queue --cancel cancels by whole-queue position from `tumwater queue`; --role scopes a read only — drop it");
208
+ if (rest.includes("--follow") || rest.includes("-f"))
209
+ fail("queue --cancel removes one entry; -f/--follow repaints a live view — drop it");
210
+ rejectUnknownArgs("queue", rest, []);
211
+ if (position === null)
212
+ fail("--cancel needs a position (usage: tumwater queue --cancel <n>)");
213
+ await requireReadyRepo(root);
214
+ // Refuse the entry a live merge marker names before touching it: it is mid-flight (being
215
+ // reviewed or merged), so cancelling it would race the slot that owns it.
216
+ const view = readMergeQueue(root);
217
+ const inFlight = queueInFlight(root, view.entries, orchestratorAlive(root));
218
+ const target = view.entries[position - 1];
219
+ if (inFlight && target && (target.sha === inFlight.sha || mergeChanges(inFlight).some((c) => c.sha === target.sha))) {
220
+ fail(`queue entry ${position} is being merged right now — wait for it to land, then \`tumwater revert ${shortSha(target.sha)}\``);
221
+ }
222
+ const outcome = await cancelQueuedMerge(root, position);
223
+ if (outcome.status === "out-of-range") {
224
+ fail(`no queued merge at position ${position} (merge queue holds ${outcome.depth})`);
225
+ }
226
+ if (json) {
227
+ sayJson({ cancelled: { role: outcome.role, commit: outcome.sha, summary: outcome.summary } });
396
228
  }
397
229
  else {
398
- const result = setConfigKey(root, key, serialize(raw));
399
- if (!result.ok)
400
- fail(result.error);
401
- say(`${base}: ${noun} set to ${display(raw)}`);
230
+ say(`cancelled queue entry ${position}: ${roleLabel(outcome.role)} ${shortSha(outcome.sha)} — ${outcome.summary}`);
402
231
  }
403
232
  }
404
- /** `tumwater role <id>`: one loop's standing prompt and next tick's assembled prompt. */
405
- export async function cmdRole(root, args) {
406
- // No requireReadyRepo gate, like backlog: role-view degrades (missing state files →
407
- // fresh defaults, a missing queue directory an empty inbox), so the command inspects
408
- // a repo the team never started in — and a torn one — instead of refusing.
409
- const { id: positional, rest } = peelRolePositional(args);
410
- rejectUnknownArgs("role", rest, [ROLE_FLAG, JSON_FLAG, CLEAR_NOTES_SPEC, NOTES_FILE_SPEC, CLEAR_INSTRUCTIONS_SPEC, INSTRUCTIONS_FILE_SPEC, CLEAR_QUIET_HOURS_SPEC, QUIET_HOURS_SPEC, ENABLE_SPEC, DISABLE_SPEC, CLEAR_CAP_SPEC, CAP_SPEC, CLEAR_MIN_RUN_INTERVAL_SPEC, MIN_RUN_INTERVAL_SPEC, CLEAR_INSTANCES_SPEC, INSTANCES_SPEC, CLEAR_PROVIDER_SPEC, PROVIDER_SPEC, CLEAR_MODEL_SPEC, MODEL_SPEC, CLEAR_THINKING_SPEC, THINKING_SPEC]);
411
- // The id is required: positional (`tumwater role <id>`) or --role <id> (the flag
412
- // spelling every other role-targeting command shares). Both at once is a mistake, not
413
- // a silent pick of one.
414
- const flagId = flagValue(rest, "--role") ?? null;
415
- const typedId = positional ?? flagId;
416
- if (!typedId)
417
- fail("tumwater role needs a role id");
418
- const id = canonicalRoleId(typedId);
419
- if (positional !== null && flagId !== null && flagId !== positional)
420
- fail("give the role id once — as the positional or as --role <id>, not both");
421
- const clear = rest.includes("--clear-notes");
422
- const notesFile = flagValue(rest, "--notes-file");
423
- const clearInst = rest.includes("--clear-instructions");
424
- const instFile = flagValue(rest, "--instructions-file");
425
- const clearQuiet = rest.includes("--clear-quiet-hours");
426
- const clearCap = rest.includes("--clear-cap");
427
- const clearInterval = rest.includes("--clear-min-run-interval");
428
- const clearInstances = rest.includes("--clear-instances");
429
- const clearProvider = rest.includes("--clear-provider");
430
- const clearModel = rest.includes("--clear-model");
431
- const clearThinking = rest.includes("--clear-thinking");
432
- const notesWrite = clear || rest.includes("--notes-file");
433
- const instructionsWrite = clearInst || rest.includes("--instructions-file");
434
- const quietWrite = clearQuiet || rest.includes("--quiet-hours");
435
- const capWrite = clearCap || rest.includes("--cap");
436
- const intervalWrite = clearInterval || rest.includes("--min-run-interval");
437
- const instancesWrite = clearInstances || rest.includes("--instances");
438
- const providerWrite = clearProvider || rest.includes("--provider");
439
- const modelWrite = clearModel || rest.includes("--model");
440
- const thinkingWrite = clearThinking || rest.includes("--thinking");
441
- const enable = rest.includes("--enable");
442
- const disable = rest.includes("--disable");
443
- const writes = [];
444
- if (notesWrite)
445
- writes.push("notes");
446
- if (instructionsWrite)
447
- writes.push("instructions");
448
- if (quietWrite)
449
- writes.push("quiet-hours");
450
- if (instancesWrite)
451
- writes.push("instances");
452
- if (capWrite)
453
- writes.push("cap");
454
- if (intervalWrite)
455
- writes.push("min-run-interval");
456
- if (providerWrite)
457
- writes.push("provider");
458
- if (modelWrite)
459
- writes.push("model");
460
- if (thinkingWrite)
461
- writes.push("thinking");
462
- if (enable)
463
- writes.push("enable");
464
- if (disable)
465
- writes.push("disable");
466
- if (writes.length > 1)
467
- fail(`${writes.join(" and ")} writes cannot share one invocation`);
468
- if (notesWrite) {
469
- if (clear && rest.includes("--notes-file"))
470
- fail("give --clear-notes or --notes-file, not both");
471
- if (rest.includes("--json"))
472
- fail("--json does not apply to a notes write");
473
- if (id === DIRECTOR_ROLE)
474
- fail("the director has no notes");
475
- requireKnownRole(root, id);
476
- const text = clear ? "" : readPromptFileArg(notesFile || fail("--notes-file needs a path"));
477
- try {
478
- say(`${id}: ${saveRoleNote(roleNotesPath(root, id), text)}`);
479
- }
480
- catch (err) {
481
- fail(errorMessage(err));
482
- }
483
- return;
233
+ /** The `tumwater queue --top <n>` branch (PLANS.md): move the Nth queued merge `tumwater
234
+ * queue` lists to the front of the execution order by renaming its queue file to a stamp
235
+ * below the queue's minimum, so it merges next when ready. `cmdQueueCancel`'s twin: `--top
236
+ * <n>` the one valued flag, `--json` the one boolean (at most once), `--help`/-h tolerated,
237
+ * anything else refused. --role and -f/--follow are refused by name — a live view repaints
238
+ * a read, and position numbers come from the whole queue, whose numbering is
239
+ * `tumwater queue`'s. An entry a live merge marker names for this sha is refused — it is past
240
+ * the queue, so there is nothing to top (unlike `--cancel`, reverting it would undo the change
241
+ * the operator wanted first; the guidance says only that it is already heading in); an
242
+ * out-of-range position fails naming the current depth; position 1 prints the already-first
243
+ * line and exits 0; a concurrently-drained entry prints the gone line and exits 0. */
244
+ async function cmdQueueTop(root, args) {
245
+ const { json, position, rest } = scanPositionArgs(args, "--top", "--top 2");
246
+ // The vocabulary a top invocation accepts is exactly --top and --json, both consumed
247
+ // above; --role and -f/--follow are refused with the queue-specific wording the tests pin.
248
+ if (rest.includes("--role"))
249
+ fail("queue --top moves by whole-queue position from `tumwater queue`; --role scopes a read only — drop it");
250
+ if (rest.includes("--follow") || rest.includes("-f"))
251
+ fail("queue --top moves one entry; -f/--follow repaints a live view — drop it");
252
+ rejectUnknownArgs("queue", rest, []);
253
+ if (position === null)
254
+ fail("--top needs a position (usage: tumwater queue --top <n>)");
255
+ await requireReadyRepo(root);
256
+ // Refuse the entry a live merge marker names before touching it: it is mid-flight (being
257
+ // reviewed or merged), so topping it would race the slot that owns it.
258
+ const view = readMergeQueue(root);
259
+ const inFlight = queueInFlight(root, view.entries, orchestratorAlive(root));
260
+ const target = view.entries[position - 1];
261
+ if (inFlight && target && (target.sha === inFlight.sha || mergeChanges(inFlight).some((c) => c.sha === target.sha))) {
262
+ fail(`queue entry ${position} is being merged right now — it's already heading into the merge, so there's nothing to top`);
263
+ }
264
+ const outcome = moveQueuedMergeToTop(root, position);
265
+ if (outcome.status === "out-of-range") {
266
+ fail(`no queued merge at position ${position} (merge queue holds ${outcome.depth})`);
484
267
  }
485
- // The instructions-override write: the file-shaped twin of the notes flags, but the value
486
- // lands in roles.<base>.instructions (tick-prompt reads config.roles[base]) through the
487
- // same setConfigKey/unsetConfigKey core `config set`/`config unset` use, so validateConfig
488
- // enforces the 4096-char cap and the write is atomic. The director is allowed here (its
489
- // override reaches its own runs), unlike the notes branch's director refusal.
490
- if (instructionsWrite) {
491
- if (clearInst && rest.includes("--instructions-file"))
492
- fail("give --clear-instructions or --instructions-file, not both");
493
- if (rest.includes("--json"))
494
- fail("--json does not apply to an instructions write");
495
- requireKnownRole(root, id);
496
- const base = baseRoleOf(id);
497
- if (clearInst) {
498
- const result = unsetConfigKey(root, `roles.${base}.instructions`);
499
- if (!result.ok)
500
- fail(result.error);
501
- say(`${base}: instructions cleared`);
268
+ if (json) {
269
+ // Every non-failure outcome, `gone` included, exits 0 with a parseable document; the
270
+ // drained entry carries no role/sha/summary, so its JSON names only the position, and
271
+ // an already-first no-op names its own key rather than masquerading as a top.
272
+ if (outcome.status === "gone") {
273
+ sayJson({ gone: { position } });
274
+ }
275
+ else if (outcome.status === "already-first") {
276
+ sayJson({ "already-first": { role: outcome.role, commit: outcome.sha, summary: outcome.summary } });
502
277
  }
503
278
  else {
504
- const text = readPromptFileArg(instFile || fail("--instructions-file needs a path"));
505
- const result = setConfigKey(root, `roles.${base}.instructions`, JSON.stringify(text));
506
- if (!result.ok)
507
- fail(result.error);
508
- say(`${base}: instructions saved (${text.length} chars)`);
279
+ sayJson({ topped: { role: outcome.role, commit: outcome.sha, summary: outcome.summary } });
509
280
  }
510
281
  return;
511
282
  }
512
- // The quiet-hours write: the third write kind, the terminal twin of `config set
513
- // quietHoursPerRole.<base> <window>`. The window lands in quietHoursPerRole.<base> through
514
- // the same setConfigKey/unsetConfigKey core config set/unset use, so checkQuietHours (with
515
- // the typed key as its label) and validateConfig own validation and the write is atomic.
516
- // The director is exempt — its quietHoursPerRole.director entry is inert (the gate and
517
- // doctor both exempt it), like the notes branch's director refusal.
518
- if (quietWrite) {
519
- if (clearQuiet && rest.includes("--quiet-hours"))
520
- fail("give --clear-quiet-hours or --quiet-hours, not both");
521
- if (rest.includes("--json"))
522
- fail("--json does not apply to a quiet-hours write");
523
- requireKnownRole(root, id);
524
- if (id === DIRECTOR_ROLE)
525
- fail("the director has no quiet hours");
526
- const base = baseRoleOf(id);
527
- const window = flagValue(rest, "--quiet-hours") ?? "";
528
- writeRoleConfigKey(root, base, clearQuiet, window, `quietHoursPerRole.${base}`, "quiet hours", (v) => JSON.stringify(v), (v) => v);
283
+ if (outcome.status === "gone") {
284
+ say(`queue entry ${position} was already taken by the merge before it could be moved to the front`);
529
285
  return;
530
286
  }
531
- // The daily-cap write: the terminal twin of `config set maxDailyCostUsdPerRole.<base>
532
- // <usd>`, the two remaining per-role holds' write the dashboard and the TUI already own.
533
- // The value lands in maxDailyCostUsdPerRole.<base> through the same
534
- // setConfigKey/unsetConfigKey core `config set`/`config unset` use, so checkDailyBudgetUsd
535
- // (with the typed key as its label) and validateConfig own validation and the write is
536
- // atomic. The director is exempt — it has no cap, like the notes and quiet-hours branches'
537
- // director refusals.
538
- if (capWrite) {
539
- if (clearCap && rest.includes("--cap"))
540
- fail("give --clear-cap or --cap, not both");
541
- if (rest.includes("--json"))
542
- fail("--json does not apply to a cap write");
543
- requireKnownRole(root, id);
544
- if (id === DIRECTOR_ROLE)
545
- fail("the director has no daily cap");
546
- const base = baseRoleOf(id);
547
- const cap = flagValue(rest, "--cap") ?? "";
548
- writeRoleConfigKey(root, base, clearCap, cap, `maxDailyCostUsdPerRole.${base}`, "daily cap", (v) => v, (v) => v);
549
- return;
287
+ if (outcome.status === "already-first") {
288
+ say(`queue entry ${position} is already first: ${roleLabel(outcome.role)} ${shortSha(outcome.sha)} — ${outcome.summary}`);
550
289
  }
551
- // The min-run-interval write: the terminal twin of `config set
552
- // roles.<base>.minRunIntervalSeconds <seconds>`, the fourth per-role hold's write. The
553
- // value lands in roles.<base>.minRunIntervalSeconds through the same
554
- // setConfigKey/unsetConfigKey core `config set`/`config unset` use, so validateConfig's
555
- // roles.<id>.minRunIntervalSeconds DURATION_NON_NEGATIVE check owns validation and the
556
- // write is atomic. Unlike the notes/quiet-hours/cap branches there is no director refusal:
557
- // the director ticks on the same clock.
558
- if (intervalWrite) {
559
- if (clearInterval && rest.includes("--min-run-interval"))
560
- fail("give --clear-min-run-interval or --min-run-interval, not both");
561
- if (rest.includes("--json"))
562
- fail("--json does not apply to a min-run-interval write");
563
- requireKnownRole(root, id);
564
- const base = baseRoleOf(id);
565
- const secs = flagValue(rest, "--min-run-interval") ?? "";
566
- writeRoleConfigKey(root, base, clearInterval, secs, `roles.${base}.minRunIntervalSeconds`, "min run interval", (v) => v, (v) => `${v}s`);
567
- return;
568
- }
569
- // The instances write: the terminal twin of `config set roles.<base>.instances <n>`, the
570
- // last per-role hold's write. The count lands in roles.<base>.instances through the same
571
- // setConfigKey/unsetConfigKey core `config set`/`config unset` use, so validateConfig's
572
- // roles.<id>.instances INSTANCE_COUNT check owns the bound and the write is atomic. As
573
- // with min run interval there is no director (or non-instance-role) refusal of its own:
574
- // validateConfig's `!INSTANCE_ROLES.has(id)` branch answers for the director and every
575
- // custom role, and an unset/empty value clears the key.
576
- if (instancesWrite) {
577
- if (clearInstances && rest.includes("--instances"))
578
- fail("give --clear-instances or --instances, not both");
579
- if (rest.includes("--json"))
580
- fail("--json does not apply to an instances write");
581
- const cfg = loadConfigSafe(root).config ?? defaultConfig();
582
- if (!knownRoleIds(cfg).includes(id))
583
- throw new Error(unknownRoleMessage(id, knownRoleIds(cfg)));
584
- const base = baseRoleOf(id);
585
- const n = flagValue(rest, "--instances") ?? "";
586
- writeRoleConfigKey(root, base, clearInstances, n, `roles.${base}.instances`, "instances", (v) => v, (v) => v);
587
- return;
588
- }
589
- // The model-triple writes: the terminal twins of `config set roles.<base>.provider|model|thinking
590
- // <value>` and `config unset` — the three fields configForRole resolves through seamOverrides.
591
- // `tumwater role <id>` renders them as its Model: line. Each lands in roles.<base>.<field>.
592
- // Through setConfigKey/unsetConfigKey, validateConfig's checkModelTripleField ("tier-name"
593
- // shape) owns validation (model accepts a tier name; thinking must be a THINKING_LEVELS level)
594
- // and each write is atomic. No director refusal: accepted exactly as --min-run-interval is.
595
- if (providerWrite) {
596
- if (clearProvider && rest.includes("--provider"))
597
- fail("give --clear-provider or --provider, not both");
598
- if (rest.includes("--json"))
599
- fail("--json does not apply to a provider write");
600
- const cfg = loadConfigSafe(root).config ?? defaultConfig();
601
- if (!knownRoleIds(cfg).includes(id))
602
- throw new Error(unknownRoleMessage(id, knownRoleIds(cfg)));
603
- const base = baseRoleOf(id);
604
- const provider = flagValue(rest, "--provider") ?? "";
605
- writeRoleConfigKey(root, base, clearProvider, provider, `roles.${base}.provider`, "provider", (v) => JSON.stringify(v), (v) => v);
606
- return;
607
- }
608
- if (modelWrite) {
609
- if (clearModel && rest.includes("--model"))
610
- fail("give --clear-model or --model, not both");
611
- if (rest.includes("--json"))
612
- fail("--json does not apply to a model write");
613
- const cfg = loadConfigSafe(root).config ?? defaultConfig();
614
- if (!knownRoleIds(cfg).includes(id))
615
- throw new Error(unknownRoleMessage(id, knownRoleIds(cfg)));
616
- const base = baseRoleOf(id);
617
- const model = flagValue(rest, "--model") ?? "";
618
- writeRoleConfigKey(root, base, clearModel, model, `roles.${base}.model`, "model", (v) => JSON.stringify(v), (v) => v);
619
- return;
620
- }
621
- if (thinkingWrite) {
622
- if (clearThinking && rest.includes("--thinking"))
623
- fail("give --clear-thinking or --thinking, not both");
624
- if (rest.includes("--json"))
625
- fail("--json does not apply to a thinking write");
626
- const cfg = loadConfigSafe(root).config ?? defaultConfig();
627
- if (!knownRoleIds(cfg).includes(id))
628
- throw new Error(unknownRoleMessage(id, knownRoleIds(cfg)));
629
- const base = baseRoleOf(id);
630
- const level = flagValue(rest, "--thinking") ?? "";
631
- writeRoleConfigKey(root, base, clearThinking, level, `roles.${base}.thinking`, "thinking", (v) => JSON.stringify(v), (v) => v);
632
- return;
290
+ else {
291
+ say(`moved queue entry ${position} to the front: ${roleLabel(outcome.role)} ${shortSha(outcome.sha)} — ${outcome.summary}`);
633
292
  }
634
- // The enable/disable write: the scoped write for the enabled state `tumwater role <id>`
635
- // already renders, the terminal twin of `config set roles.<base>.enabled true|false`. The
636
- // value lands in roles.<base>.enabled through the same setConfigKey core config set uses, so
637
- // validateConfig's checkBoolean (the roles.<id>. prefix) owns the type and the write stays
638
- // atomic and applies live. The director cannot be disabled: with it disabled loopIds omits
639
- // it and no loop dequeues the operator's inbox, so that refusal is explicit (enabling stays
640
- // allowed). knownRoleIds holds every catalog and custom role whether or not it is enabled, so
641
- // a currently-disabled id resolves; an instance id writes its base role's key.
642
- if (enable || disable) {
643
- if (rest.includes("--json"))
644
- fail("--json does not apply to an enable/disable write");
645
- requireKnownRole(root, id);
646
- if (disable && id === DIRECTOR_ROLE)
647
- fail("the director cannot be disabled");
648
- const base = baseRoleOf(id);
649
- const result = setConfigKey(root, `roles.${base}.enabled`, enable ? "true" : "false");
650
- if (!result.ok)
651
- fail(result.error);
652
- say(`${base}: ${enable ? "enabled" : "disabled"}`);
293
+ }
294
+ /** `tumwater backlog`: the planned/open entries as Markdown or as the GUI's JSON payload. */
295
+ export async function cmdBacklog(root, args, seams = {}) {
296
+ // No requireReadyRepo gate: the entry readers degrade to [] on a missing file, so the
297
+ // command prints three empty sections in any directory (report's rationale, not config's);
298
+ // the team-workspace refusal still applies.
299
+ // --grep filters the three entry arrays to the matches, the finished-entry lists' one
300
+ // matcher (entryMatches) on the active backlog; it composes with --ready (grep first, so
301
+ // the hidden count and the kept entries count only the matches) and with --json. The
302
+ // missing-value wording lives in BACKLOG_GREP_VALUE_ERROR, the flag gate's and this
303
+ // parser's shared string. -f/--follow repaints the same view every couple of seconds
304
+ // until Ctrl+C or the parent process exits, composing with --ready and --grep; it refuses
305
+ // --json (a live view is one repainted document, not one snapshot).
306
+ const BACKLOG_GREP_VALUE_ERROR = "backlog --grep needs text (usage: tumwater backlog --grep <text>)";
307
+ rejectUnknownArgs("backlog", args, [JSON_FLAG, READY_FLAG, grepFlagSpec(BACKLOG_GREP_VALUE_ERROR), FOLLOW_FLAG]);
308
+ refuseTeamWorkspace(root);
309
+ const { rest, pattern } = parseGrepFlag(args, BACKLOG_GREP_VALUE_ERROR);
310
+ const ready = rest.includes("--ready");
311
+ const follow = hasFollowFlag(rest);
312
+ if (follow && rest.includes("--json"))
313
+ fail("backlog --json prints one document; -f/--follow repaints a live view — drop one of them");
314
+ // Machine-readable backlog — the three entry arrays the Markdown view renders and the
315
+ // GUI's /api/backlog serves (status --json's "print the endpoint's payload" pattern),
316
+ // plus `holds.plans`/`holds.bugs` parallel to the entry arrays so a skipped entry's
317
+ // reason is data, not inference: a pretty-printed JSON document in every exit-0 case,
318
+ // never prose. The payload is read once per branch (snapshot or frame) so the Markdown
319
+ // renderer and the JSON document consume the same arrays.
320
+ // --ready filters both branches through readyBacklog; the Markdown branch adds a count of
321
+ // the held entries it hid. The --json/--ready reads go over `rest` (the --grep value token
322
+ // removed), so a flag-shaped pattern like `--grep --json` greps for "--json" rather than
323
+ // flipping this branch into JSON mode.
324
+ const readBacklog = () => {
325
+ const view = { ...backlogPayload(root), holds: backlogHolds(root), inProgress: backlogInProgress(root) };
326
+ const filtered = pattern === null ? view : grepBacklog(view, pattern);
327
+ if (!ready)
328
+ return { view: filtered, hidden: 0 };
329
+ const kept = readyBacklog(filtered);
330
+ return { view: kept, hidden: filtered.plans.length + filtered.bugs.length - kept.plans.length - kept.bugs.length };
331
+ };
332
+ const renderBacklogText = (r) => renderBacklogMarkdown(r.view) + (r.hidden > 0
333
+ ? `\n\n_(${r.hidden} held entries hidden; run \`tumwater backlog\` to see them)_` : "");
334
+ if (follow) {
335
+ await followView(() => renderBacklogText(readBacklog()), seams);
653
336
  return;
654
337
  }
655
- // The unknown-role answer (exit 1, unknownRoleMessage's wording) lives in the
656
- // collector; here the id just has to be non-null for the thunk's types.
657
- sayJsonOrRender(rest, () => rolePayload(root, id), renderRoleMarkdown);
338
+ const result = readBacklog();
339
+ sayJsonOrRender(rest, result.view, () => renderBacklogText(result));
340
+ }
341
+ // `tumwater quarantine [--json]` — the read-only view of the review gate's flake
342
+ // quarantine. No requireReadyRepo gate: loadQuarantine degrades an absent or malformed
343
+ // file to the empty list (the cmdBacklog rationale), so the command prints the empty line
344
+ // in any directory, and it never writes quarantine.json (a test leaves the list only
345
+ // through releaseQuarantine). --json serves the same positioned rows to scripts.
346
+ export async function cmdQuarantine(root, args) {
347
+ rejectUnknownArgs("quarantine", args, [JSON_FLAG]);
348
+ refuseTeamWorkspace(root);
349
+ const rows = quarantineRows(loadQuarantine(root));
350
+ sayJsonOrRender(args, { tests: rows }, () => renderQuarantine(rows, Date.now()));
658
351
  }